消消乐 · 七关闯关
by PrismDolphin13608 lines20.0 KB
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>消消乐 · 七关闯关</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;user-select:none;-webkit-tap-highlight-color:transparent}
body{background:#1a1a2e;display:flex;justify-content:center;align-items:center;min-height:100vh;font-family:'Segoe UI',sans-serif;touch-action:none}
.game{background:#16213e;padding:15px;border-radius:24px;box-shadow:0 10px 40px rgba(0,0,0,0.6);text-align:center;max-width:600px;width:100%;margin:10px;position:relative}
h1{color:#f5c842;font-size:22px;margin-bottom:2px}
.level-info{display:flex;justify-content:space-between;color:#a0f0e0;font-size:14px;font-weight:bold;padding:4px 2px}
.level-info span{background:#1f2a48;padding:2px 12px;border-radius:30px}
#levelDisplay{color:#4d96ff}
#progressDisplay{color:#ffd700}
canvas{display:block;margin:0 auto;width:100%;aspect-ratio:1/1;background:#0f0f23;border-radius:16px;touch-action:none;cursor:pointer}
.controls{display:flex;justify-content:center;gap:12px;margin-top:10px}
button{background:#4d96ff;border:none;padding:8px 20px;border-radius:30px;font-size:16px;font-weight:bold;color:#fff;cursor:pointer;box-shadow:0 4px 0 #2a5fa0;transition:0.1s}
button:active{transform:translateY(4px);box-shadow:0 0 0 #2a5fa0}
#status{color:#ffaa66;font-size:14px;min-height:24px;margin-top:4px}
/* ----- 庆祝画面(覆盖层) ----- */
.celebrate {
display: none;
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.85);
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 24px;
z-index: 20;
padding: 20px;
box-sizing: border-box;
}
.celebrate .title {
color: #ffd700;
font-size: 2.8em;
font-weight: bold;
text-shadow: 0 0 20px #ffd70088;
margin-bottom: 10px;
}
.celebrate .cat {
font-size: 6em;
line-height: 1.2;
animation: dance 0.6s infinite alternate ease-in-out;
}
@keyframes dance {
0% { transform: translateY(0) rotate(-5deg) scale(1); }
100% { transform: translateY(-20px) rotate(15deg) scale(1.1); }
}
.celebrate .sub {
color: #a0f0e0;
font-size: 1.2em;
margin-top: 10px;
}
.celebrate .btn-play-again {
margin-top: 20px;
background: #ff6b6b;
box-shadow: 0 4px 0 #a04545;
padding: 10px 30px;
font-size: 1.2em;
}
.celebrate .btn-play-again:active {
transform: translateY(4px);
box-shadow: 0 0 0 #a04545;
}
</style>
</head>
<body>
<div class="game">
<h1>💥 消消乐 · 七关闯关</h1>
<div class="level-info">
<span id="levelDisplay">🏁 第 1 关 (3×3)</span>
<span id="progressDisplay">📊 消除 0 / 20</span>
</div>
<canvas id="c"></canvas>
<div id="status">👆 滑动交换,五消合成💣,单击炸弹引爆</div>
<div class="controls">
<button id="restartBtn">🔄 重开本关</button>
<button id="nextBtn" style="display:none;">➡️ 下一关</button>
</div>
<!-- 庆祝画面 -->
<div class="celebrate" id="celebrateDiv">
<div class="title">🎉 恭喜通关!</div>
<div class="cat">🐱</div>
<div class="sub">你太棒啦!所有关卡全部完成!</div>
<button class="btn-play-again" id="playAgainBtn">🔄 再玩一次</button>
</div>
</div>
<script>
// ----- 配置 -----
const EMOJIS = ['🐱', '🐶', '⭐'];
const COLORS = ['#ff6b6b', '#6bcb77', '#ffd93d'];
const TYPES = EMOJIS.length;
const BOMB_TYPE = -2;
const BOMB_EMOJI = '💣';
const BOMB_COLOR = '#ffffff';
// ----- 关卡定义(尺寸,目标消除次数)-----
const LEVELS = [
{ size: 3, target: 20 },
{ size: 4, target: 25 },
{ size: 5, target: 30 },
{ size: 6, target: 35 },
{ size: 7, target: 40 },
{ size: 8, target: 45 },
{ size: 9, target: 50 }
];
const TOTAL_LEVELS = LEVELS.length;
// ----- DOM -----
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const levelSpan = document.getElementById('levelDisplay');
const progressSpan = document.getElementById('progressDisplay');
const statusMsg = document.getElementById('status');
const restartBtn = document.getElementById('restartBtn');
const nextBtn = document.getElementById('nextBtn');
const celebrateDiv = document.getElementById('celebrateDiv');
const playAgainBtn = document.getElementById('playAgainBtn');
// ----- 游戏状态 -----
let grid = [];
let ROWS = 3, COLS = 3;
let levelIndex = 0;
let target = 20;
let eliminatedCount = 0;
let isProcessing = false;
let selectedR = -1, selectedC = -1;
let timerId = null;
let bombClickable = true;
// ----- 初始化棋盘(无三消)-----
function initGrid() {
grid = [];
for (let r = 0; r < ROWS; r++) {
grid[r] = [];
for (let c = 0; c < COLS; c++) {
let type;
do {
type = Math.floor(Math.random() * TYPES);
} while (
(r >= 2 && grid[r-1][c] === type && grid[r-2][c] === type) ||
(c >= 2 && grid[r][c-1] === type && grid[r][c-2] === type)
);
grid[r][c] = type;
}
}
}
// ----- 绘制棋盘 -----
function draw() {
const size = Math.min(canvas.width / COLS, canvas.height / ROWS);
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const type = grid[r][c];
if (type === -1) continue;
const x = c * size, y = r * size;
let color = (type === BOMB_TYPE) ? BOMB_COLOR : COLORS[type];
let emoji = (type === BOMB_TYPE) ? BOMB_EMOJI : EMOJIS[type];
ctx.fillStyle = color;
ctx.shadowBlur = 4;
ctx.shadowColor = 'rgba(255,255,255,0.08)';
ctx.beginPath();
ctx.roundRect(x+1, y+1, size-2, size-2, 4);
ctx.fill();
ctx.shadowBlur = 0;
const fontSize = size * 0.5;
ctx.font = `${fontSize}px 'Segoe UI Emoji', 'Apple Color Emoji', sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = (type === BOMB_TYPE) ? '#000' : '#fff';
ctx.fillText(emoji, x + size/2, y + size/2 + 1);
}
}
if (selectedR >= 0 && selectedC >= 0 && grid[selectedR] && grid[selectedR][selectedC] !== -1) {
const size = Math.min(canvas.width / COLS, canvas.height / ROWS);
const x = selectedC * size, y = selectedR * size;
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(x+2, y+2, size-4, size-4, 4);
ctx.stroke();
}
}
CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
if (w < 2*r) r = w/2;
if (h < 2*r) r = h/2;
this.moveTo(x+r, y);
this.lineTo(x+w-r, y);
this.quadraticCurveTo(x+w, y, x+w, y+r);
this.lineTo(x+w, y+h-r);
this.quadraticCurveTo(x+w, y+h, x+w-r, y+h);
this.lineTo(x+r, y+h);
this.quadraticCurveTo(x, y+h, x, y+h-r);
this.lineTo(x, y+r);
this.quadraticCurveTo(x, y, x+r, y);
this.closePath();
return this;
};
// ----- 检测三消(返回坐标集合)-----
function getMatches() {
const s = new Set();
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS-2; c++) {
const t = grid[r][c];
if (t === -1 || t === BOMB_TYPE) continue;
if (grid[r][c+1] === t && grid[r][c+2] === t) {
let e = c+2;
while (e+1 < COLS && grid[r][e+1] === t) e++;
for (let i = c; i <= e; i++) s.add(r+','+i);
}
}
}
for (let c = 0; c < COLS; c++) {
for (let r = 0; r < ROWS-2; r++) {
const t = grid[r][c];
if (t === -1 || t === BOMB_TYPE) continue;
if (grid[r+1][c] === t && grid[r+2][c] === t) {
let e = r+2;
while (e+1 < ROWS && grid[e+1][c] === t) e++;
for (let i = r; i <= e; i++) s.add(i+','+c);
}
}
}
return s;
}
// ----- 查找长连线(≥5)中心点(用于放炸弹)-----
function findLongLine(matches) {
const rows = {}, cols = {};
for (const key of matches) {
const [r, c] = key.split(',').map(Number);
if (!rows[r]) rows[r] = [];
rows[r].push(c);
if (!cols[c]) cols[c] = [];
cols[c].push(r);
}
for (const r in rows) {
const cs = rows[r].sort((a,b)=>a-b);
let start = 0;
for (let i = 1; i <= cs.length; i++) {
if (i === cs.length || cs[i] !== cs[i-1] + 1) {
const len = i - start;
if (len >= 5) {
const mid = Math.floor((cs[start] + cs[i-1]) / 2);
return { row: parseInt(r), col: mid };
}
start = i;
}
}
}
for (const c in cols) {
const rs = cols[c].sort((a,b)=>a-b);
let start = 0;
for (let i = 1; i <= rs.length; i++) {
if (i === rs.length || rs[i] !== rs[i-1] + 1) {
const len = i - start;
if (len >= 5) {
const mid = Math.floor((rs[start] + rs[i-1]) / 2);
return { row: mid, col: parseInt(c) };
}
start = i;
}
}
}
return null;
}
// ----- 消除处理:标记空位,延迟填充,计数-----
function eliminateAndRefill(matchSet, bombExplosion = false) {
const positions = [];
for (const key of matchSet) {
const [r, c] = key.split(',').map(Number);
if (grid[r][c] !== -1) {
positions.push([r, c]);
grid[r][c] = -1;
}
}
const count = positions.length;
eliminatedCount += count;
updateDisplay();
if (eliminatedCount >= target) {
statusMsg.textContent = '🎉 你真棒!过关啦!';
nextBtn.style.display = 'inline-block';
isProcessing = true;
draw();
return;
}
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (grid[r][c] === -1) {
grid[r][c] = Math.floor(Math.random() * TYPES);
}
}
}
const newMatches = getMatches();
if (newMatches.size > 0) {
setTimeout(() => {
if (timerId) clearTimeout(timerId);
eliminateAndRefill(newMatches, false);
}, 50);
} else {
if (!hasValidMoves() && eliminatedCount < target) {
statusMsg.textContent = '⛔ 无有效移动,点击重开';
} else {
statusMsg.textContent = '✅ 继续消除';
}
draw();
isProcessing = false;
}
timerId = null;
}, 1000);
draw();
}
// ----- 炸弹引爆逻辑(单击炸弹)-----
function triggerBomb(row, col) {
if (isProcessing) return;
if (grid[row][col] !== BOMB_TYPE) return;
if (!bombClickable) return;
bombClickable = false;
const toRemove = new Set();
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
const r = row + dr, c = col + dc;
if (r >= 0 && r < ROWS && c >= 0 && c < COLS) {
if (grid[r][c] !== -1 && !(r === row && c === col)) {
toRemove.add(r+','+c);
}
}
}
}
toRemove.add(row+','+col);
const posArray = Array.from(toRemove);
for (const key of posArray) {
const [r, c] = key.split(',').map(Number);
grid[r][c] = -1;
}
eliminatedCount += posArray.length;
updateDisplay();
if (eliminatedCount >= target) {
statusMsg.textContent = '🎉 你真棒!过关啦!';
nextBtn.style.display = 'inline-block';
isProcessing = true;
draw();
bombClickable = true;
return;
}
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (grid[r][c] === -1) {
grid[r][c] = Math.floor(Math.random() * TYPES);
}
}
}
const newMatches = getMatches();
if (newMatches.size > 0) {
setTimeout(() => {
if (timerId) clearTimeout(timerId);
eliminateAndRefill(newMatches, false);
}, 50);
} else {
if (!hasValidMoves() && eliminatedCount < target) {
statusMsg.textContent = '⛔ 无有效移动,点击重开';
} else {
statusMsg.textContent = '✅ 继续消除';
}
draw();
isProcessing = false;
}
timerId = null;
}, 1000);
draw();
bombClickable = true;
}
// ----- 交换处理(滑动交换相邻方块)-----
function swapAndProcess(r1, c1, r2, c2) {
if (isProcessing) return;
if (Math.abs(r1-r2) + Math.abs(c1-c2) !== 1) return;
const type1 = grid[r1][c1];
const type2 = grid[r2][c2];
if (type1 === -1 || type2 === -1 || type1 === BOMB_TYPE || type2 === BOMB_TYPE) return;
[grid[r1][c1], grid[r2][c2]] = [grid[r2][c2], grid[r1][c1]];
draw();
const matches = getMatches();
if (matches.size === 0) {
[grid[r1][c1], grid[r2][c2]] = [grid[r2][c2], grid[r1][c1]];
draw();
statusMsg.textContent = '❌ 无法消除';
return;
}
const bombPos = findLongLine(matches);
if (bombPos) {
const { row, col } = bombPos;
if (grid[row][col] !== BOMB_TYPE) {
grid[row][col] = BOMB_TYPE;
const key = row+','+col;
if (matches.has(key)) matches.delete(key);
statusMsg.textContent = '💣 合成炸弹!单击引爆';
}
}
if (matches.size === 0) {
draw();
if (!hasValidMoves() && eliminatedCount < target) {
statusMsg.textContent = '⛔ 无有效移动,点击重开';
} else {
statusMsg.textContent = '💣 炸弹已合成,单击引爆';
}
isProcessing = false;
return;
}
isProcessing = true;
eliminateAndRefill(matches, false);
}
// ----- 检查是否有有效移动(忽略炸弹)-----
function hasValidMoves() {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (grid[r][c] === -1 || grid[r][c] === BOMB_TYPE) continue;
if (c+1 < COLS && grid[r][c+1] !== -1 && grid[r][c+1] !== BOMB_TYPE) {
[grid[r][c], grid[r][c+1]] = [grid[r][c+1], grid[r][c]];
const m = getMatches();
[grid[r][c], grid[r][c+1]] = [grid[r][c+1], grid[r][c]];
if (m.size > 0) return true;
}
if (r+1 < ROWS && grid[r+1][c] !== -1 && grid[r+1][c] !== BOMB_TYPE) {
[grid[r][c], grid[r+1][c]] = [grid[r+1][c], grid[r][c]];
const m = getMatches();
[grid[r][c], grid[r+1][c]] = [grid[r+1][c], grid[r][c]];
if (m.size > 0) return true;
}
}
}
return false;
}
// ----- 更新显示-----
function updateDisplay() {
levelSpan.textContent = `🏁 第 ${levelIndex+1} 关 (${ROWS}×${COLS})`;
progressSpan.textContent = `📊 消除 ${eliminatedCount} / ${target}`;
}
// ----- 加载关卡-----
function loadLevel(index) {
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
const level = LEVELS[index];
ROWS = level.size;
COLS = level.size;
target = level.target;
eliminatedCount = 0;
levelIndex = index;
isProcessing = false;
bombClickable = true;
selectedR = selectedC = -1;
nextBtn.style.display = 'none';
celebrateDiv.style.display = 'none'; // 隐藏庆祝画面
canvas.width = 600;
canvas.height = 600;
initGrid();
let attempts = 0;
while (!hasValidMoves() && attempts < 100) {
initGrid();
attempts++;
}
updateDisplay();
statusMsg.textContent = `👆 滑动交换,五消合成💣,单击炸弹引爆`;
draw();
}
// ----- 下一关(或通关庆祝)-----
function nextLevel() {
if (levelIndex + 1 < TOTAL_LEVELS) {
loadLevel(levelIndex + 1);
} else {
// 所有关卡通关 → 显示庆祝画面
celebrateDiv.style.display = 'flex';
nextBtn.style.display = 'none';
statusMsg.textContent = '🎉 恭喜通关所有关卡!你真棒!';
// 禁用游戏操作(已通过 isProcessing 及隐藏按钮)
}
}
// ----- 重开当前关-----
function restartLevel() {
loadLevel(levelIndex);
}
// ----- 再玩一次(从第一关重新开始)-----
function playAgain() {
celebrateDiv.style.display = 'none';
loadLevel(0);
}
// ----- 触摸/鼠标事件处理-----
let startX = 0, startY = 0, sr = -1, sc = -1, dragging = false;
function getGridPos(clientX, clientY) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (clientX - rect.left) * scaleX;
const y = (clientY - rect.top) * scaleY;
if (x < 0 || x >= canvas.width || y < 0 || y >= canvas.height) return null;
const col = Math.floor(x / (canvas.width / COLS));
const row = Math.floor(y / (canvas.height / ROWS));
if (row >= ROWS || col >= COLS) return null;
return { row, col };
}
function onStart(e) {
e.preventDefault();
if (isProcessing) return;
const pos = getGridPos(e.clientX, e.clientY);
if (!pos) return;
const { row, col } = pos;
if (grid[row][col] === BOMB_TYPE) {
triggerBomb(row, col);
return;
}
sr = row; sc = col;
startX = e.clientX; startY = e.clientY;
dragging = true;
selectedR = sr; selectedC = sc;
draw();
}
function onMove(e) {
e.preventDefault();
if (!dragging || isProcessing) return;
const pos = getGridPos(e.clientX, e.clientY);
if (!pos) return;
const dr = pos.row - sr;
const dc = pos.col - sc;
if (Math.abs(dr) + Math.abs(dc) === 1) {
swapAndProcess(sr, sc, pos.row, pos.col);
dragging = false;
selectedR = selectedC = -1;
draw();
} else {
selectedR = pos.row; selectedC = pos.col;
draw();
}
}
function onEnd(e) {
e.preventDefault();
if (dragging) {
dragging = false;
selectedR = selectedC = -1;
draw();
}
}
canvas.addEventListener('mousedown', onStart);
canvas.addEventListener('mousemove', onMove);
canvas.addEventListener('mouseup', onEnd);
canvas.addEventListener('mouseleave', onEnd);
canvas.addEventListener('touchstart', e => {
const t = e.touches[0];
if (t) onStart({ clientX: t.clientX, clientY: t.clientY, preventDefault: () => e.preventDefault() });
}, { passive: false });
canvas.addEventListener('touchmove', e => {
const t = e.touches[0];
if (t) onMove({ clientX: t.clientX, clientY: t.clientY, preventDefault: () => e.preventDefault() });
}, { passive: false });
canvas.addEventListener('touchend', e => {
onEnd({ clientX: startX, clientY: startY, preventDefault: () => e.preventDefault() });
}, { passive: false });
// ----- 按钮绑定-----
restartBtn.addEventListener('click', restartLevel);
nextBtn.addEventListener('click', nextLevel);
playAgainBtn.addEventListener('click', playAgain);
// ----- 启动游戏-----
loadLevel(0);
</script>
</body>
</html>Game Source: 消消乐 · 七关闯关
Creator: PrismDolphin13
Libraries: none
Complexity: complex (608 lines, 20.0 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-prismdolphin13-mspqvp59" to link back to the original. Then publish at arcadelab.ai/publish.