Files
AICODE2026/3-lessons/AICODE-06/demo/demo-level0.html
Rocky 4f95086d55 feat: 新增 AICODE-06 魔幻俄罗斯方块第6-7课教案及 demo
- AICODE06-06 魔幻俄罗斯方块(上):工程师思维启蒙,Level 0 需求文档 + 压力测试 + 结果溯源
- AICODE06-07 魔幻俄罗斯方块(下):增量需求文档 + 魔改升级 + 成果路演
- demo/demo-level0.html:完整基础俄罗斯方块(含虚影、暂停、升级)
- demo/demo-level1-bomb.html:炸弹方块示例(含粒子爆炸特效)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:37:58 +02:00

461 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>魔幻俄罗斯方块 · Level 0</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-family: Arial, sans-serif;
color: #fff;
}
.game-container {
display: flex;
gap: 20px;
align-items: flex-start;
}
canvas#board {
border: 2px solid #e94560;
box-shadow: 0 0 20px rgba(233, 69, 96, 0.3);
display: block;
}
.side-panel { width: 120px; }
.panel-box {
background: #16213e;
border: 1px solid #0f3460;
border-radius: 8px;
padding: 12px;
margin-bottom: 14px;
}
.panel-box h3 {
font-size: 11px;
color: #e94560;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 6px;
}
.panel-box p {
font-size: 22px;
font-weight: bold;
}
.controls {
font-size: 11px;
color: #888;
line-height: 2;
}
#overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.75);
justify-content: center;
align-items: center;
}
#overlay.show { display: flex; }
.overlay-box {
background: #16213e;
border: 2px solid #e94560;
border-radius: 12px;
padding: 32px 40px;
text-align: center;
}
.overlay-box h2 { color: #e94560; font-size: 24px; margin-bottom: 12px; }
.overlay-box p { color: #aaa; margin-bottom: 20px; }
.overlay-box span { color: #fff; font-size: 28px; font-weight: bold; }
button {
padding: 10px 28px;
background: #e94560;
color: #fff;
border: none;
border-radius: 6px;
font-size: 15px;
cursor: pointer;
margin-top: 16px;
}
button:hover { background: #c73652; }
</style>
</head>
<body>
<div class="game-container">
<canvas id="board" width="300" height="600"></canvas>
<div class="side-panel">
<div class="panel-box">
<h3>下一个</h3>
<canvas id="next-canvas" width="80" height="80"></canvas>
</div>
<div class="panel-box">
<h3>得分</h3>
<p id="score">0</p>
</div>
<div class="panel-box">
<h3>等级</h3>
<p id="level">1</p>
</div>
<div class="panel-box">
<h3>消行</h3>
<p id="lines">0</p>
</div>
<div class="panel-box controls">
<h3>操作</h3>
← → 移动<br>
↑ 旋转<br>
↓ 加速<br>
空格 直落<br>
P 暂停
</div>
</div>
</div>
<div id="overlay">
<div class="overlay-box">
<h2>游戏结束</h2>
<p>最终得分</p>
<span id="final-score">0</span>
<br>
<button onclick="startGame()">再来一次</button>
</div>
</div>
<script>
// =====================
// 配置(需求文档对应的参数)
// =====================
const CONFIG = {
COLS: 10,
ROWS: 20,
CELL: 30,
BASE_SPEED: 800, // 初始下落间隔(毫秒)
SPEED_STEP: 50, // 每级加快多少毫秒
LINES_PER_LEVEL: 10, // 每消多少行升级
// 消1/2/3/4行的基础得分
SCORE_TABLE: [0, 100, 300, 500, 800],
};
// =====================
// 7种标准方块
// =====================
const TETROMINOES = [
{ shape: [[1,1,1,1]], color: '#00f5ff' }, // I
{ shape: [[1,1],[1,1]], color: '#ffd700' }, // O
{ shape: [[0,1,0],[1,1,1]], color: '#bf5fff' }, // T
{ shape: [[0,1,1],[1,1,0]], color: '#39ff14' }, // S
{ shape: [[1,1,0],[0,1,1]], color: '#ff3131' }, // Z
{ shape: [[1,0,0],[1,1,1]], color: '#ff8c00' }, // J
{ shape: [[0,0,1],[1,1,1]], color: '#0080ff' }, // L
];
// =====================
// 游戏状态变量
// =====================
let board, current, next;
let score, level, lines;
let running, paused;
let lastTs, dropAcc, dropInterval;
let rafId;
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
const nextCvs = document.getElementById('next-canvas');
const nextCtx = nextCvs.getContext('2d');
// =====================
// 启动 / 重置游戏
// =====================
function startGame() {
document.getElementById('overlay').classList.remove('show');
board = Array.from({ length: CONFIG.ROWS }, () => Array(CONFIG.COLS).fill(0));
score = 0; level = 1; lines = 0;
running = true; paused = false;
dropInterval = CONFIG.BASE_SPEED;
dropAcc = 0; lastTs = 0;
updateHUD();
next = newPiece();
spawnPiece();
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(loop);
}
// =====================
// 生成随机方块对象
// =====================
function newPiece() {
const t = TETROMINOES[Math.floor(Math.random() * TETROMINOES.length)];
return {
shape: t.shape.map(r => [...r]),
color: t.color,
x: Math.floor(CONFIG.COLS / 2) - Math.floor(t.shape[0].length / 2),
y: 0,
};
}
// =====================
// 在顶部生成下一块
// =====================
function spawnPiece() {
current = next;
current.x = Math.floor(CONFIG.COLS / 2) - Math.floor(current.shape[0].length / 2);
current.y = 0;
next = newPiece();
drawNext();
if (hit(current, 0, 0)) endGame();
}
// =====================
// 碰撞检测
// =====================
function hit(piece, dx, dy) {
for (let r = 0; r < piece.shape.length; r++) {
for (let c = 0; c < piece.shape[r].length; c++) {
if (!piece.shape[r][c]) continue;
const nx = piece.x + c + dx;
const ny = piece.y + r + dy;
if (nx < 0 || nx >= CONFIG.COLS) return true;
if (ny >= CONFIG.ROWS) return true;
if (ny >= 0 && board[ny][nx]) return true;
}
}
return false;
}
// =====================
// 顺时针旋转矩阵
// =====================
function rotateCW(shape) {
const R = shape.length, C = shape[0].length;
const out = Array.from({ length: C }, () => Array(R).fill(0));
for (let r = 0; r < R; r++)
for (let c = 0; c < C; c++)
out[c][R - 1 - r] = shape[r][c];
return out;
}
// =====================
// 将当前方块锁定到棋盘
// =====================
function lock() {
for (let r = 0; r < current.shape.length; r++)
for (let c = 0; c < current.shape[r].length; c++)
if (current.shape[r][c] && current.y + r >= 0)
board[current.y + r][current.x + c] = current.color;
clearLines();
spawnPiece();
}
// =====================
// 消行(从下往上扫描)
// =====================
function clearLines() {
let cleared = 0;
for (let r = CONFIG.ROWS - 1; r >= 0; r--) {
if (board[r].every(cell => cell !== 0)) {
board.splice(r, 1);
board.unshift(Array(CONFIG.COLS).fill(0));
cleared++;
r++; // 重新检查移下来的行
}
}
if (cleared === 0) return;
score += CONFIG.SCORE_TABLE[cleared] * level;
lines += cleared;
const newLevel = Math.floor(lines / CONFIG.LINES_PER_LEVEL) + 1;
if (newLevel > level) {
level = newLevel;
dropInterval = Math.max(100, CONFIG.BASE_SPEED - (level - 1) * CONFIG.SPEED_STEP);
}
updateHUD();
}
// =====================
// 主循环
// =====================
function loop(ts) {
rafId = requestAnimationFrame(loop);
if (!running || paused) return;
const dt = ts - lastTs;
lastTs = ts;
dropAcc += dt;
if (dropAcc >= dropInterval) {
if (!hit(current, 0, 1)) current.y++;
else lock();
dropAcc = 0;
}
render();
}
// =====================
// 键盘控制
// =====================
document.addEventListener('keydown', e => {
if (!running) return;
switch (e.key) {
case 'ArrowLeft':
e.preventDefault();
if (!paused && !hit(current, -1, 0)) current.x--;
break;
case 'ArrowRight':
e.preventDefault();
if (!paused && !hit(current, 1, 0)) current.x++;
break;
case 'ArrowDown':
e.preventDefault();
if (!paused) {
if (!hit(current, 0, 1)) current.y++;
else lock();
dropAcc = 0;
}
break;
case 'ArrowUp':
e.preventDefault();
if (!paused) tryRotate();
break;
case ' ':
e.preventDefault();
if (!paused) hardDrop();
break;
case 'p': case 'P':
paused = !paused;
break;
}
});
function tryRotate() {
const rotated = rotateCW(current.shape);
const orig = current.shape;
current.shape = rotated;
// 踢墙:旋转后碰撞就尝试左右移一格
if (hit(current, 0, 0)) {
if (!hit(current, 1, 0)) current.x++;
else if (!hit(current, -1, 0)) current.x--;
else current.shape = orig;
}
}
function hardDrop() {
while (!hit(current, 0, 1)) current.y++;
lock();
}
// =====================
// 渲染
// =====================
function render() {
const S = CONFIG.CELL;
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 网格线
ctx.strokeStyle = 'rgba(255,255,255,0.04)';
for (let r = 0; r < CONFIG.ROWS; r++)
for (let c = 0; c < CONFIG.COLS; c++)
ctx.strokeRect(c * S, r * S, S, S);
// 棋盘上的固定方块
for (let r = 0; r < CONFIG.ROWS; r++)
for (let c = 0; c < CONFIG.COLS; c++)
if (board[r][c]) drawCell(ctx, c, r, board[r][c], S);
// 落点虚影
const ghost = ghostPiece();
ghost.shape.forEach((row, r) =>
row.forEach((v, c) => {
if (!v) return;
ctx.fillStyle = 'rgba(255,255,255,0.12)';
ctx.fillRect((ghost.x+c)*S+1, (ghost.y+r)*S+1, S-2, S-2);
})
);
// 当前方块
current.shape.forEach((row, r) =>
row.forEach((v, c) => {
if (v) drawCell(ctx, current.x+c, current.y+r, current.color, S);
})
);
// 暂停遮罩
if (paused) {
ctx.fillStyle = 'rgba(0,0,0,0.65)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#fff';
ctx.font = 'bold 28px Arial';
ctx.textAlign = 'center';
ctx.fillText('暂停', canvas.width/2, canvas.height/2);
ctx.font = '15px Arial';
ctx.fillStyle = '#aaa';
ctx.fillText('按 P 继续', canvas.width/2, canvas.height/2 + 34);
}
}
function drawCell(context, cx, cy, color, S) {
context.fillStyle = color;
context.fillRect(cx*S+1, cy*S+1, S-2, S-2);
context.fillStyle = 'rgba(255,255,255,0.18)';
context.fillRect(cx*S+1, cy*S+1, S-2, 4);
}
function ghostPiece() {
const g = { shape: current.shape, x: current.x, y: current.y };
while (!hit(g, 0, 1)) g.y++;
return g;
}
function drawNext() {
const S = 20;
nextCtx.fillStyle = '#16213e';
nextCtx.fillRect(0, 0, nextCvs.width, nextCvs.height);
const ox = Math.floor((4 - next.shape[0].length) / 2);
const oy = Math.floor((4 - next.shape.length) / 2);
next.shape.forEach((row, r) =>
row.forEach((v, c) => {
if (!v) return;
nextCtx.fillStyle = next.color;
nextCtx.fillRect((ox+c)*S, (oy+r)*S, S-1, S-1);
})
);
}
function updateHUD() {
document.getElementById('score').textContent = score;
document.getElementById('level').textContent = level;
document.getElementById('lines').textContent = lines;
}
function endGame() {
running = false;
document.getElementById('final-score').textContent = score;
document.getElementById('overlay').classList.add('show');
}
// 启动
startGame();
</script>
</body>
</html>