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>
This commit is contained in:
460
3-lessons/AICODE-06/demo/demo-level0.html
Normal file
460
3-lessons/AICODE-06/demo/demo-level0.html
Normal file
@@ -0,0 +1,460 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user