更新 AI03 课程内容,替换为 AICODE-03 和 AICODE-06 课程体系
- 删除旧的 AI03 课程文件(第4-5课) - 新增 AICODE-03 课程体系(AI是怎么想的、AI训练师、我是大作家等) - 新增 AICODE-06 课程体系(从扣子到代码、个人主页、涂鸦PK等)
This commit is contained in:
460
.claude/lesson/AI03/AICODE-06/demo/demo-level0.html
Normal file
460
.claude/lesson/AI03/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>
|
||||
576
.claude/lesson/AI03/AICODE-06/demo/demo-level1-bomb.html
Normal file
576
.claude/lesson/AI03/AICODE-06/demo/demo-level1-bomb.html
Normal file
@@ -0,0 +1,576 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>魔幻俄罗斯方块 · Level 1 示例:炸弹方块</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: 130px; }
|
||||
|
||||
.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; }
|
||||
|
||||
.bomb-hint {
|
||||
font-size: 11px;
|
||||
color: #ffd700;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.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: 8px; }
|
||||
.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; }
|
||||
|
||||
/* 爆炸动画层 */
|
||||
#explosion-canvas {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<canvas id="explosion-canvas"></canvas>
|
||||
|
||||
<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 bomb-hint">
|
||||
<h3>💣 炸弹规则</h3>
|
||||
出现概率:20%<br>
|
||||
落地爆炸:<br>
|
||||
炸掉 3×3 范围<br>
|
||||
爆炸后触发消行<br>
|
||||
额外奖励 200 分
|
||||
</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,
|
||||
SCORE_TABLE: [0, 100, 300, 500, 800],
|
||||
// 炸弹方块配置
|
||||
BOMB_CHANCE: 0.20, // 出现概率 20%
|
||||
BOMB_RADIUS: 1, // 爆炸半径(炸 3×3 = 半径1)
|
||||
BOMB_BONUS: 200, // 爆炸奖励分
|
||||
};
|
||||
|
||||
// =====================
|
||||
// 方块定义
|
||||
// =====================
|
||||
const TETROMINOES = [
|
||||
{ shape: [[1,1,1,1]], color: '#00f5ff' },
|
||||
{ shape: [[1,1],[1,1]], color: '#ffd700' },
|
||||
{ shape: [[0,1,0],[1,1,1]], color: '#bf5fff' },
|
||||
{ shape: [[0,1,1],[1,1,0]], color: '#39ff14' },
|
||||
{ shape: [[1,1,0],[0,1,1]], color: '#ff3131' },
|
||||
{ shape: [[1,0,0],[1,1,1]], color: '#ff8c00' },
|
||||
{ shape: [[0,0,1],[1,1,1]], color: '#0080ff' },
|
||||
];
|
||||
|
||||
// 炸弹方块(特殊形状:单格)
|
||||
const BOMB_PIECE = {
|
||||
shape: [[1]],
|
||||
color: '#ff4500',
|
||||
isBomb: true,
|
||||
};
|
||||
|
||||
// =====================
|
||||
// 状态
|
||||
// =====================
|
||||
let board, current, next;
|
||||
let score, level, lines;
|
||||
let running, paused;
|
||||
let lastTs, dropAcc, dropInterval;
|
||||
let rafId;
|
||||
let particles = []; // 爆炸粒子
|
||||
|
||||
const canvas = document.getElementById('board');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const nextCvs = document.getElementById('next-canvas');
|
||||
const nextCtx = nextCvs.getContext('2d');
|
||||
const expCvs = document.getElementById('explosion-canvas');
|
||||
const expCtx = expCvs.getContext('2d');
|
||||
|
||||
// 让爆炸画布覆盖全屏
|
||||
function resizeExpCanvas() {
|
||||
expCvs.width = window.innerWidth;
|
||||
expCvs.height = window.innerHeight;
|
||||
}
|
||||
resizeExpCanvas();
|
||||
window.addEventListener('resize', resizeExpCanvas);
|
||||
|
||||
// =====================
|
||||
// 启动游戏
|
||||
// =====================
|
||||
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;
|
||||
particles = [];
|
||||
|
||||
updateHUD();
|
||||
next = newPiece();
|
||||
spawnPiece();
|
||||
|
||||
if (rafId) cancelAnimationFrame(rafId);
|
||||
rafId = requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 生成方块(20% 概率炸弹)
|
||||
// =====================
|
||||
function newPiece() {
|
||||
if (Math.random() < CONFIG.BOMB_CHANCE) {
|
||||
return { ...BOMB_PIECE, shape: [[1]], x: 0, y: 0 };
|
||||
}
|
||||
const t = TETROMINOES[Math.floor(Math.random() * TETROMINOES.length)];
|
||||
return { shape: t.shape.map(r => [...r]), color: t.color, isBomb: false, x: 0, 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 tryRotate() {
|
||||
if (current.isBomb) return; // 炸弹不需要旋转
|
||||
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 lock() {
|
||||
if (current.isBomb) {
|
||||
// 炸弹落地:触发爆炸
|
||||
const bx = current.x;
|
||||
const by = current.y;
|
||||
explode(bx, by);
|
||||
} else {
|
||||
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 explode(cx, cy) {
|
||||
// 获取爆炸中心在棋盘上的坐标
|
||||
const R = CONFIG.BOMB_RADIUS;
|
||||
|
||||
// 计算爆炸中心像素坐标(用于粒子特效)
|
||||
const S = CONFIG.CELL;
|
||||
// 找到 board 画布在屏幕上的位置
|
||||
const boardRect = canvas.getBoundingClientRect();
|
||||
const pixelX = boardRect.left + cx * S + S / 2;
|
||||
const pixelY = boardRect.top + cy * S + S / 2;
|
||||
|
||||
// 清除爆炸范围内的所有格子
|
||||
for (let r = cy - R; r <= cy + R; r++) {
|
||||
for (let c = cx - R; c <= cx + R; c++) {
|
||||
if (r >= 0 && r < CONFIG.ROWS && c >= 0 && c < CONFIG.COLS) {
|
||||
board[r][c] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 爆炸奖励分
|
||||
score += CONFIG.BOMB_BONUS * level;
|
||||
|
||||
// 爆炸后触发消行检查
|
||||
clearLines();
|
||||
|
||||
// 产生粒子特效
|
||||
spawnParticles(pixelX, pixelY);
|
||||
|
||||
updateHUD();
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 消行
|
||||
// =====================
|
||||
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) return;
|
||||
|
||||
score += CONFIG.SCORE_TABLE[Math.min(cleared, 4)] * 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 spawnParticles(px, py) {
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
const speed = 3 + Math.random() * 6;
|
||||
particles.push({
|
||||
x: px, y: py,
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
life: 1.0,
|
||||
color: `hsl(${Math.floor(Math.random()*60)}, 100%, 60%)`,
|
||||
size: 3 + Math.random() * 4,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateParticles() {
|
||||
expCtx.clearRect(0, 0, expCvs.width, expCvs.height);
|
||||
particles = particles.filter(p => p.life > 0);
|
||||
for (const p of particles) {
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.vy += 0.2; // 重力
|
||||
p.life -= 0.03;
|
||||
|
||||
expCtx.globalAlpha = p.life;
|
||||
expCtx.fillStyle = p.color;
|
||||
expCtx.beginPath();
|
||||
expCtx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
|
||||
expCtx.fill();
|
||||
}
|
||||
expCtx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 主循环
|
||||
// =====================
|
||||
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();
|
||||
updateParticles();
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 键盘
|
||||
// =====================
|
||||
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) { while (!hit(current,0,1)) current.y++; lock(); }
|
||||
break;
|
||||
case 'p': case 'P': paused = !paused; break;
|
||||
}
|
||||
});
|
||||
|
||||
// =====================
|
||||
// 渲染
|
||||
// =====================
|
||||
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);
|
||||
|
||||
// 虚影(炸弹不显示虚影)
|
||||
if (!current.isBomb) {
|
||||
const ghost = ghostPiece();
|
||||
ghost.shape.forEach((row, r) =>
|
||||
row.forEach((v, c) => {
|
||||
if (!v) return;
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.1)';
|
||||
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) return;
|
||||
if (current.isBomb) {
|
||||
drawBombCell(ctx, current.x+c, current.y+r, S);
|
||||
} else {
|
||||
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 drawBombCell(context, cx, cy, S) {
|
||||
// 炸弹外观:深红底 + 💣 表情
|
||||
context.fillStyle = '#2a0a0a';
|
||||
context.fillRect(cx*S+1, cy*S+1, S-2, S-2);
|
||||
context.strokeStyle = '#ff4500';
|
||||
context.lineWidth = 2;
|
||||
context.strokeRect(cx*S+2, cy*S+2, S-4, S-4);
|
||||
context.font = `${S-8}px serif`;
|
||||
context.textAlign = 'center';
|
||||
context.textBaseline = 'middle';
|
||||
context.fillText('💣', cx*S + S/2, cy*S + S/2 + 1);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (next.isBomb) {
|
||||
nextCtx.font = '36px serif';
|
||||
nextCtx.textAlign = 'center';
|
||||
nextCtx.textBaseline = 'middle';
|
||||
nextCtx.fillText('💣', nextCvs.width/2, nextCvs.height/2);
|
||||
return;
|
||||
}
|
||||
|
||||
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 hardDrop() {
|
||||
while (!hit(current, 0, 1)) current.y++;
|
||||
lock();
|
||||
}
|
||||
|
||||
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