2026-07-15 11:28:08
发布于:浙江
/**
- 俄罗斯方块核心逻辑
- 包含:游戏状态管理、方块生成、碰撞检测、行消除、渲染循环
*/
// --- 配置与常量 ---
const COLS = 10;
const ROWS = 20;
const BLOCK_SIZE = 30; // 像素大小,会根据Canvas动态调整,这里作为基准
const COLORS = {
neon: [
null,
'#00f0f0', // I - Cyan
'#0000f0', // J - Blue
'#f0a000', // L - Orange
'#f0f000', // O - Yellow
'#00f000', // S - Green
'#a000f0', // T - Purple
'#f00000' // Z - Red
],
classic: [
null,
'#00FFFF', '#0000FF', '#FFA500', '#FFFF00', '#00FF00', '#800080', '#FF0000'
],
pastel: [
null,
'#A0E7E5', '#B4F8C8', '#FBE7C6', '#FFAEBC', '#D4C4FB', '#F6C6EA', '#FFC3A0'
]
};
// 方块形状定义 (SRS标准)
const SHAPES = [
[],
[[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]], // I
[[2,0,0], [2,2,2], [0,0,0]], // J
[[0,0,3], [3,3,3], [0,0,0]], // L
[[4,4], [4,4]], // O
[[0,5,5], [5,5,0], [0,0,0]], // S
[[0,6,0], [6,6,6], [0,0,0]], // T
[[7,7,0], [0,7,7], [0,0,0]] // Z
];
// --- 游戏状态类 ---
class Game {
constructor() {
this.canvas = document.getElementById('game-canvas');
this.ctx = this.canvas.getContext('2d');
this.nextCanvas = document.getElementById('next-canvas');
this.nextCtx = this.nextCanvas.getContext('2d');
// 适配高分屏
this.scale = window.devicePixelRatio || 1;
this.resizeCanvas();
this.board = this.createBoard();
this.score = 0;
this.lines = 0;
this.level = 1;
this.highScore = localStorage.getItem('tetris_high_score') || 0;
this.dropCounter = 0;
this.dropInterval = 1000;
this.lastTime = 0;
this.player = {
pos: {x: 0, y: 0},
matrix: null,
score: 0
};
this.nextPieceMatrix = null;
this.isPaused = false;
this.isGameOver = false;
this.isPlaying = false;
this.settings = {
sound: true,
ghost: true,
theme: 'neon'
};
this.bindEvents();
this.updateUI();
// 初始绘制空 board
this.draw();
}
resizeCanvas() {
// 固定逻辑分辨率,CSS控制显示大小
this.canvas.width = COLS * BLOCK_SIZE * this.scale;
this.canvas.height = ROWS * BLOCK_SIZE * this.scale;
this.ctx.scale(this.scale, this.scale);
this.nextCanvas.width = 4 * BLOCK_SIZE * this.scale;
this.nextCanvas.height = 4 * BLOCK_SIZE * this.scale;
this.nextCtx.scale(this.scale, this.scale);
}
createBoard() {
return Array.from({length: ROWS}, () => Array(COLS).fill(0));
}
// --- 核心逻辑 ---
reset() {
this.board = this.createBoard();
this.score = 0;
this.lines = 0;
this.level = 1;
this.dropInterval = 1000;
this.isGameOver = false;
this.isPaused = false;
this.isPlaying = true;
this.playerReset();
this.updateUI();
this.hideOverlay();
this.hideGameOverModal();
this.lastTime = 0;
this.dropCounter = 0;
requestAnimationFrame(this.update.bind(this));
}
playerReset() {
if (this.nextPieceMatrix === null) {
this.nextPieceMatrix = this.createPiece(Math.floor(Math.random() * 7) + 1);
}
this.player.matrix = this.nextPieceMatrix;
this.nextPieceMatrix = this.createPiece(Math.floor(Math.random() * 7) + 1);
// 居中生成
this.player.pos.y = 0;
this.player.pos.x = (COLS / 2 | 0) - (this.player.matrix.length / 2 | 0);
// 检查生成即碰撞 -> Game Over
if (this.collide(this.board, this.player)) {
this.gameOver();
}
this.drawNextPiece();
}
createPiece(type) {
// 深拷贝形状矩阵
return SHAPES[type].map(row => [...row]);
}
collide(board, player) {
const m = player.matrix;
const o = player.pos;
for (let y = 0; y < m.length; ++y) {
for (let x = 0; x < m[y].length; ++x) {
if (m[y][x] !== 0 &&
(board[y + o.y] && board[y + o.y][x + o.x]) !== 0) {
return true;
}
}
}
return false;
}
draw() {
// 清空画布
this.ctx.fillStyle = '#020617'; // 对应 bg-slate-950
this.ctx.fillRect(0, 0, this.canvas.width / this.scale, this.canvas.height / this.scale);
// 绘制已固定的方块
this.drawMatrix(this.board, {x: 0, y: 0}, this.ctx);
// 绘制幽灵方块 (Ghost Piece)
if (this.settings.ghost && !this.isGameOver && this.isPlaying) {
const ghostPos = {...this.player.pos};
while (!this.collide(this.board, {pos: ghostPos, matrix: this.player.matrix})) {
ghostPos.y++;
}
ghostPos.y--; // 回退一步到合法位置
this.ctx.globalAlpha = 0.2;
this.drawMatrix(this.player.matrix, ghostPos, this.ctx, true);
this.ctx.globalAlpha = 1.0;
}
// 绘制当前下落方块
if (!this.isGameOver) {
this.drawMatrix(this.player.matrix, this.player.pos, this.ctx);
}
}
drawMatrix(matrix, offset, context, isGhost = false) {
matrix.forEach((row, y) => {
row.forEach((value, x) => {
if (value !== 0) {
const color = this.getColor(value, isGhost);
context.fillStyle = color;
// 绘制方块主体
context.fillRect(
(x + offset.x) * BLOCK_SIZE,
(y + offset.y) * BLOCK_SIZE,
BLOCK_SIZE,
BLOCK_SIZE
);
// 绘制高光/边框效果 (Neon风格)
if (!isGhost) {
context.lineWidth = 2;
context.strokeStyle = 'rgba(255,255,255,0.3)';
context.strokeRect(
(x + offset.x) * BLOCK_SIZE,
(y + offset.y) * BLOCK_SIZE,
BLOCK_SIZE,
BLOCK_SIZE
);
// 内部阴影
context.fillStyle = 'rgba(0,0,0,0.1)';
context.fillRect(
(x + offset.x) * BLOCK_SIZE + 2,
(y + offset.y) * BLOCK_SIZE + 2,
BLOCK_SIZE - 4,
BLOCK_SIZE - 4
);
} else {
context.strokeStyle = 'rgba(255,255,255,0.5)';
context.lineWidth = 1;
context.strokeRect(
(x + offset.x) * BLOCK_SIZE,
(y + offset.y) * BLOCK_SIZE,
BLOCK_SIZE,
BLOCK_SIZE
);
}
}
});
});
}
getColor(value, isGhost) {
const theme = this.settings.theme;
const baseColor = COLORS[theme][value] || '#fff';
if (isGhost) {
// 简单的变亮处理作为幽灵色
return baseColor;
}
return baseColor;
}
drawNextPiece() {
this.nextCtx.fillStyle = '#0f172a';
this.nextCtx.fillRect(0, 0, this.nextCanvas.width / this.scale, this.nextCanvas.height / this.scale);
if (this.nextPieceMatrix) {
// 计算居中偏移
const boxSize = 4;
const offsetX = (boxSize - this.nextPieceMatrix.length) / 2;
const offsetY = (boxSize - this.nextPieceMatrix.length) / 2;
this.drawMatrix(this.nextPieceMatrix, {x: offsetX, y: offsetY}, this.nextCtx);
}
}
merge(board, player) {
player.matrix.forEach((row, y) => {
row.forEach((value, x) => {
if (value !== 0) {
board[y + player.pos.y][x + player.pos.x] = value;
}
});
});
}
rotate(matrix, dir) {
for (let y = 0; y < matrix.length; ++y) {
for (let x = 0; x < y; ++x) {
[matrix[x][y], matrix[y][x]] = [matrix[y][x], matrix[x][y]];
}
}
if (dir > 0) {
matrix.forEach(row => row.reverse());
} else {
matrix.reverse();
}
}
playerRotate(dir) {
const pos = this.player.pos.x;
let offset = 1;
this.rotate(this.player.matrix, dir);
// 墙踢 (Wall Kick) 简单实现
while (this.collide(this.board, this.player)) {
this.player.pos.x += offset;
offset = -(offset + (offset > 0 ? 1 : -1));
if (offset > this.player.matrix.length) {
this.rotate(this.player.matrix, -dir);
this.player.pos.x = pos;
return;
}
}
}
playerDrop() {
this.player.pos.y++;
if (this.collide(this.board, this.player)) {
this.player.pos.y--;
this.merge(this.board, this.player);
this.arenaSweep();
this.playerReset();
}
this.dropCounter = 0;
}
playerHardDrop() {
while (!this.collide(this.board, this.player)) {
this.player.pos.y++;
}
this.player.pos.y--;
this.merge(this.board, this.player);
this.arenaSweep();
this.playerReset();
this.dropCounter = 0;
}
playerMove(offset) {
this.player.pos.x += offset;
if (this.collide(this.board, this.player)) {
this.player.pos.x -= offset;
}
}
arenaSweep() {
let rowCount = 0;
outer: for (let y = this.board.length - 1; y > 0; --y) {
for (let x = 0; x < this.board[y].length; ++x) {
if (this.board[y][x] === 0) {
continue outer;
}
}
const row = this.board.splice(y, 1).fill(0);
this.board.unshift(row);
++y;
rowCount++;
}
if (rowCount > 0) {
// 计分规则: 1行100, 2行300, 3行500, 4行800 * 等级
const lineScores = [0, 100, 300, 500, 800];
this.score += lineScores[rowCount] * this.level;
this.lines += rowCount;
// 每10行升一级
this.level = Math.floor(this.lines / 10) + 1;
// 速度随等级增加,最低100ms
this.dropInterval = Math.max(100, 1000 - (this.level - 1) * 100);
this.updateUI();
// 播放消除音效逻辑 (此处省略具体音频文件加载,仅做结构预留)
if(this.settings.sound) {
// playSound('clear');
}
}
}
update(time = 0) {
if (!this.isPlaying || this.isPaused) return;
const deltaTime = time - this.lastTime;
this.lastTime = time;
this.dropCounter += deltaTime;
if (this.dropCounter > this.dropInterval) {
this.playerDrop();
}
this.draw();
requestAnimationFrame(this.update.bind(this));
}
// --- UI 与 交互 ---
updateUI() {
document.getElementById('score-display').innerText = this.score;
document.getElementById('level-display').innerText = this.level;
document.getElementById('lines-display').innerText = this.lines;
document.getElementById('high-score-display').innerText = this.highScore;
}
showOverlay(title, btnText) {
const overlay = document.getElementById('overlay');
document.getElementById('overlay-title').innerText = title;
document.getElementById('start-btn-text').innerText = btnText;
overlay.classList.remove('hidden');
}
hideOverlay() {
document.getElementById('overlay').classList.add('hidden');
}
showGameOverModal() {
const modal = document.getElementById('game-over-modal');
document.getElementById('final-score').innerText = this.score;
modal.classList.remove('hidden');
if (this.score > this.highScore) {
this.highScore = this.score;
localStorage.setItem('tetris_high_score', this.highScore);
}
}
hideGameOverModal() {
document.getElementById('game-over-modal').classList.add('hidden');
}
gameOver() {
this.isGameOver = true;
this.isPlaying = false;
this.showGameOverModal();
}
togglePause() {
if (this.isGameOver) return;
this.isPaused = !this.isPaused;
if (this.isPaused) {
this.showOverlay("PAUSED", "Resume");
} else {
this.hideOverlay();
this.lastTime = performance.now(); // 重置时间防止跳帧
requestAnimationFrame(this.update.bind(this));
}
}
bindEvents() {
// 键盘事件
document.addEventListener('keydown', event => {
if (!this.isPlaying && event.keyCode !== 13) return; // 13 is Enter
// 防止方向键滚动页面
if([32, 37, 38, 39, 40].indexOf(event.keyCode) > -1) {
event.preventDefault();
}
if (this.isGameOver) {
if (event.keyCode === 13) this.reset();
return;
}
switch(event.keyCode) {
case 37: // Left
if(!this.isPaused) this.playerMove(-1);
break;
case 39: // Right
if(!this.isPaused) this.playerMove(1);
break;
case 40: // Down
if(!this.isPaused) this.playerDrop();
break;
case 38: // Up (Rotate)
if(!this.isPaused) this.playerRotate(1);
break;
case 32: // Space (Hard Drop)
if(!this.isPaused) this.playerHardDrop();
break;
case 80: // P (Pause)
this.togglePause();
break;
}
});
// 按钮事件
document.getElementById('start-btn').addEventListener('click', () => {
if (this.isPaused) {
this.togglePause();
} else {
this.reset();
}
});
document.getElementById('restart-btn').addEventListener('click', () => {
this.reset();
});
// 移动端控制
const bindTouch = (id, action) => {
const btn = document.getElementById(id);
btn.addEventListener('touchstart', (e) => {
e.preventDefault();
if(this.isPlaying && !this.isPaused && !this.isGameOver) action();
});
btn.addEventListener('mousedown', (e) => { // 兼容鼠标点击测试
if(this.isPlaying && !this.isPaused && !this.isGameOver) action();
});
};
bindTouch('btn-left', () => this.playerMove(-1));
bindTouch('btn-right', () => this.playerMove(1));
bindTouch('btn-down', () => this.playerDrop());
bindTouch('btn-rotate', () => this.playerRotate(1));
bindTouch('btn-drop', () => this.playerHardDrop());
// 设置面板事件
document.getElementById('sound-toggle').addEventListener('change', (e) => {
this.settings.sound = e.target.checked;
});
document.getElementById('ghost-toggle').addEventListener('change', (e) => {
this.settings.ghost = e.target.checked;
if(!this.isPaused && this.isPlaying) this.draw(); // 立即重绘
});
document.getElementById('theme-select').addEventListener('change', (e) => {
this.settings.theme = e.target.value;
if(!this.isPaused && this.isPlaying) {
this.draw();
this.drawNextPiece();
}
});
// 初始显示开始界面
this.showOverlay("TETRIS", "Start Game");
}
}
// 启动游戏实例
window.onload = () => {
const game = new Game();
};
这里空空如也













有帮助,赞一个