<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>2048 Shooter</title>
<style>
* { user-select: none; -webkit-tap-highlight-color: transparent; box-sizing: border-box; }
:root {
--bg-deep: #050a12;
--panel-bg: rgba(8, 14, 22, 0.95);
--border-sharp: #2e4a6e;
--glow-accent: #facc15;
}
body {
margin: 0; height: 100vh; width: 100vw; overflow: hidden;
background: radial-gradient(circle at 30% 10%, #0b1220, #010308);
font-family: system-ui, -apple-system, sans-serif;
display: flex; flex-direction: column; align-items: center; justify-content: center;
color: white;
}
.game-container { width: 95%; max-width: 450px; height: 98vh; display: flex; flex-direction: column; gap: 8px; position: relative; justify-content: center; }
.stats-panel { display: flex; justify-content: space-between; gap: 6px; flex-shrink: 0; }
.stat-card { flex: 1; background: var(--panel-bg); border: 1px solid var(--border-sharp); padding: 8px 2px; text-align: center; border-radius: 4px; }
.stat-label { font-size: 10px; color: #89a9dd; letter-spacing: 1px; text-transform: uppercase; }
.stat-digit { font-size: 20px; font-weight: 800; color: #facc15; }
.items-bar { display: flex; justify-content: space-between; gap: 6px; flex-shrink: 0; }
.item-btn {
flex: 1; background: #16253b; border: 1px solid #3b82f6; color: #fff;
padding: 10px 2px; cursor: pointer; font-size: 12px; font-weight: bold;
display: flex; flex-direction: column; align-items: center; gap: 4px; border-radius: 4px;
}
.item-btn.active { background: #facc15; color: #000; border-color: #fff; }
.item-btn:disabled { opacity: 0.2; filter: grayscale(1); }
.count-tag { font-size: 9px; background: rgba(0,0,0,0.4); padding: 1px 6px; border-radius: 10px; color: #facc15; }
.canvas-wrapper { position: relative; flex-grow: 1; display: flex; align-items: center; justify-content: center; overflow: hidden; background: rgba(0,0,0,0.2); border-radius: 8px; }
canvas { display: block; background: #050a12; border: 2px solid #2e4a6e; touch-action: none; max-height: 100%; max-width: 100%; }
#gameOverMask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.92); display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 100; backdrop-filter: blur(10px); }
.restart-btn { background: #facc15; color: #000; border: none; padding: 12px 40px; font-size: 18px; font-weight: bold; margin-top: 25px; border-radius: 4px; cursor: pointer; }
.hint { font-size: 12px; color: #64748b; text-align: center; flex-shrink: 0; padding: 5px 0; min-height: 22px; }
</style>
</head>
<body>
<div class="game-container">
<div class="stats-panel">
<div class="stat-card"><div class="stat-label">Score</div><div class="stat-digit" id="score">0</div></div>
<div class="stat-card"><div class="stat-label">Next</div><div class="stat-digit" id="next" style="color:#2dd4bf">2</div></div>
<div class="stat-card"><div class="stat-label">Best</div><div class="stat-digit" id="best">0</div></div>
</div>
<div class="items-bar">
<button class="item-btn" id="btnHammer">🔨 碎裂 <span class="count-tag" id="countHammer">3</span></button>
<button class="item-btn" id="btnSwap">⇄ 交換 <span class="count-tag" id="countSwap">3</span></button>
<button class="item-btn" id="btnUndo">↩ 撤銷 <span class="count-tag" id="countUndo">3</span></button>
</div>
<div class="canvas-wrapper">
<canvas id="gameCanvas"></canvas>
<div id="gameOverMask">
<div style="color:#facc15; font-size:36px; font-weight:900;">GAME OVER</div>
<div style="margin-top:10px; color:white;">最終得分: <span id="finalScoreDisplay" style="color:#facc15; font-size:28px; font-weight:bold;">0</span></div>
<button class="restart-btn" onclick="location.reload()">再試一次</button>
</div>
</div>
<div class="hint" id="modeHint">● 滑動瞄準,抬手發射</div>
</div>
<script>
(function(){
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const ROWS = 9, COLS = 5;
let cellSize, tiles = [], bullet = null, isFlying = false, isProcessing = false;
let score = 0, best = localStorage.getItem('2048_best') || 0;
let nextVal = 2, activeTool = null, swapTarget = null;
let previewX = -100;
let historyStack = null;
let itemCounts = { hammer: 3, swap: 3, undo: 3 };
const colors = {
2: "#3B82F6", 4: "#2D9CDB", 8: "#2DC2BD", 16: "#20B2AA",
32: "#F97316", 64: "#F25E3A", 128: "#FFA63F", 256: "#FFB347",
512: "#FFC362", 1024: "#E55C4B", 2048: "#D93947"
};
class Block {
constructor(x, y, val) {
this.x = x; this.y = y; this.targetY = y; this.val = val;
}
update() {
if (Math.abs(this.y - this.targetY) > 1) this.y += (this.targetY - this.y) * 0.3;
else this.y = this.targetY;
}
draw(isGhost = false) {
const s = cellSize - 6;
ctx.save();
ctx.globalAlpha = isGhost ? 0.6 : 1.0;
ctx.fillStyle = colors[this.val] || "#475569";
if(ctx.roundRect) {
ctx.beginPath(); ctx.roundRect(this.x - s/2, this.y - s/2, s, s, 6); ctx.fill();
} else {
ctx.fillRect(this.x - s/2, this.y - s/2, s, s);
}
ctx.strokeStyle = isGhost ? "#fff" : "rgba(255,255,255,0.3)";
ctx.lineWidth = isGhost ? 2 : 1;
ctx.strokeRect(this.x - s/2, this.y - s/2, s, s);
ctx.fillStyle = "white";
ctx.font = `bold ${cellSize * 0.35}px sans-serif`;
ctx.textAlign = "center"; ctx.textBaseline = "middle";
ctx.fillText(this.val, this.x, this.y);
ctx.restore();
}
}
const initLayout = () => {
const wrapper = document.querySelector('.canvas-wrapper');
cellSize = Math.min((wrapper.clientWidth-10)/COLS, (wrapper.clientHeight-10)/ROWS);
canvas.width = cellSize * COLS;
canvas.height = cellSize * ROWS;
};
const updateUI = () => {
document.getElementById('score').innerText = score;
document.getElementById('best').innerText = best;
document.getElementById('next').innerText = nextVal;
for (let key in itemCounts) {
document.getElementById('count' + key.charAt(0).toUpperCase() + key.slice(1)).innerText = itemCounts[key];
document.getElementById('btn' + key.charAt(0).toUpperCase() + key.slice(1)).disabled = (itemCounts[key] <= 0);
}
};
const executePhysics = async () => {
if (isProcessing) return;
isProcessing = true;
let changed = true;
while (changed) {
changed = false;
// 重力
for (let c = 0; c < COLS; c++) {
let colX = c * cellSize + cellSize/2;
let colBlocks = tiles.filter(t => Math.abs(t.x - colX) < 5).sort((a,b) => a.y - b.y);
colBlocks.forEach((t, i) => {
let ty = i * cellSize + cellSize/2;
if (Math.abs(t.targetY - ty) > 1) { t.targetY = ty; changed = true; }
});
}
if (changed) await new Promise(r => setTimeout(r, 80));
// 合併
for (let i = 0; i < tiles.length; i++) {
for (let j = 0; j < tiles.length; j++) {
if (i === j) continue;
const a = tiles[i], b = tiles[j];
const isV = Math.abs(a.x - b.x) < 5 && Math.abs(a.targetY - (b.targetY - cellSize)) < 5;
const isH = Math.abs(a.targetY - b.targetY) < 5 && Math.abs(Math.abs(a.x - b.x) - cellSize) < 5;
if ((isV || isH) && a.val === b.val) {
if (Math.random() > 0.5) { b.val *= 2; score += b.val; tiles.splice(i, 1); }
else { a.val *= 2; score += a.val; tiles.splice(j, 1); }
changed = true; break;
}
}
if (changed) break;
}
}
isProcessing = false;
updateUI();
if (score > best) { best = score; localStorage.setItem('2048_best', best); }
// 【修復關鍵】:檢查是否結束,並正確顯示當前最新分數
if (tiles.some(t => t.targetY > canvas.height - cellSize * 1.2)) {
document.getElementById('finalScoreDisplay').innerText = score; // 直接注入最新變數
document.getElementById('gameOverMask').style.display = 'flex';
}
};
const getPos = (e) => {
const rect = canvas.getBoundingClientRect();
const touch = e.touches?.[0] || e.changedTouches?.[0] || e;
const x = (touch.clientX - rect.left) * (canvas.width / rect.width);
const y = (touch.clientY - rect.top) * (canvas.height / rect.height);
return { x, y, colX: Math.floor(x / cellSize) * cellSize + cellSize/2 };
};
const handleInput = (e) => {
if (isProcessing || isFlying || document.getElementById('gameOverMask').style.display === 'flex') return;
const { x, y, colX } = getPos(e);
if (activeTool) {
const target = tiles.find(t => Math.abs(t.x - colX) < cellSize/2 && Math.abs(t.y - y) < cellSize/2);
if (target) {
if (activeTool === 'hammer') {
saveHistory(); itemCounts.hammer--; tiles = tiles.filter(t => t !== target); executePhysics();
} else if (activeTool === 'swap') {
if (!swapTarget) { swapTarget = target; return; }
if (target === swapTarget) { swapTarget = null; }
else { saveHistory(); itemCounts.swap--; const v = swapTarget.val; swapTarget.val = target.val; target.val = v; executePhysics(); }
}
}
activeTool = null; swapTarget = null;
document.querySelectorAll('.item-btn').forEach(b => b.classList.remove('active'));
return;
}
saveHistory();
bullet = new Block(colX, canvas.height - cellSize/2, nextVal);
isFlying = true;
const maxV = tiles.length > 0 ? Math.max(...tiles.map(t => t.val)) : 8;
const pool = [2, 2, 2, 4, 4, 8].filter(v => v <= Math.max(8, maxV/4));
nextVal = pool[Math.floor(Math.random() * pool.length)];
updateUI();
};
const saveHistory = () => {
historyStack = { tiles: tiles.map(t => ({x: t.x, y: t.targetY, val: t.val})), score, nextVal };
};
initLayout();
canvas.addEventListener('touchstart', (e) => { e.preventDefault(); previewX = getPos(e).colX; }, {passive:false});
canvas.addEventListener('touchmove', (e) => { e.preventDefault(); previewX = getPos(e).colX; }, {passive:false});
canvas.addEventListener('touchend', (e) => { e.preventDefault(); handleInput(e); });
canvas.addEventListener('mousedown', (e) => { previewX = getPos(e).colX; });
canvas.addEventListener('mousemove', (e) => { if(e.buttons > 0) previewX = getPos(e).colX; });
canvas.addEventListener('mouseup', handleInput);
document.getElementById('btnHammer').onclick = () => { activeTool = 'hammer'; document.getElementById('btnHammer').classList.add('active'); };
document.getElementById('btnSwap').onclick = () => { activeTool = 'swap'; document.getElementById('btnSwap').classList.add('active'); };
document.getElementById('btnUndo').onclick = () => {
if (itemCounts.undo > 0 && historyStack) {
itemCounts.undo--; score = historyStack.score; nextVal = historyStack.nextVal;
tiles = historyStack.tiles.map(d => new Block(d.x, d.y, d.val));
historyStack = null; updateUI();
}
};
function animate() {
ctx.clearRect(0,0,canvas.width,canvas.height);
if (!isFlying && !isProcessing && !activeTool && previewX > 0) {
ctx.fillStyle = "rgba(255,255,255,0.05)"; ctx.fillRect(previewX-cellSize/2, 0, cellSize, canvas.height);
new Block(previewX, canvas.height-cellSize/2, nextVal).draw(true);
}
if (isFlying && bullet) {
bullet.y -= (canvas.height / 20);
if (bullet.y <= cellSize/2 || tiles.some(t => Math.abs(t.x-bullet.x)<5 && Math.abs(t.y-bullet.y)<cellSize*0.8)) {
tiles.push(bullet); isFlying = false; bullet = null; executePhysics();
}
}
tiles.forEach(t => { t.update(); t.draw(); });
if (bullet) bullet.draw();
if (swapTarget) { ctx.strokeStyle="#facc15"; ctx.lineWidth=3; ctx.strokeRect(swapTarget.x-cellSize/2, swapTarget.y-cellSize/2, cellSize, cellSize); }
requestAnimationFrame(animate);
}
updateUI(); animate();
})();
</script>
</body>
</html>