- 使用 HTML + CSS + JavaScript 实现 - 实现完整的游戏逻辑:方块移动、旋转、消行、计分 - 支持等级系统和加速机制 - 响应式设计,支持移动端 - 下一个方块预览功能 - 游戏暂停/继续功能
387 lines
9.2 KiB
JavaScript
387 lines
9.2 KiB
JavaScript
// 游戏常量
|
|
const COLS = 10;
|
|
const ROWS = 20;
|
|
const BLOCK_SIZE = 30;
|
|
|
|
// 方块形状定义
|
|
const SHAPES = [
|
|
[[1, 1, 1, 1]], // I
|
|
[[1, 1], [1, 1]], // O
|
|
[[0, 1, 0], [1, 1, 1]], // T
|
|
[[1, 0, 0], [1, 1, 1]], // L
|
|
[[0, 0, 1], [1, 1, 1]], // J
|
|
[[0, 1, 1], [1, 1, 0]], // S
|
|
[[1, 1, 0], [0, 1, 1]] // Z
|
|
];
|
|
|
|
// 方块颜色
|
|
const COLORS = [
|
|
'#00f0f0', // I - 青色
|
|
'#f0f000', // O - 黄色
|
|
'#a000f0', // T - 紫色
|
|
'#f0a000', // L - 橙色
|
|
'#0000f0', // J - 蓝色
|
|
'#00f000', // S - 绿色
|
|
'#f00000' // Z - 红色
|
|
];
|
|
|
|
// 游戏状态
|
|
let canvas, ctx, nextCanvas, nextCtx;
|
|
let board = [];
|
|
let currentPiece = null;
|
|
let nextPiece = null;
|
|
let score = 0;
|
|
let level = 1;
|
|
let lines = 0;
|
|
let gameLoop = null;
|
|
let isPaused = false;
|
|
let isGameOver = false;
|
|
let dropInterval = 1000;
|
|
|
|
// 初始化
|
|
function init() {
|
|
canvas = document.getElementById('game-canvas');
|
|
ctx = canvas.getContext('2d');
|
|
nextCanvas = document.getElementById('next-canvas');
|
|
nextCtx = nextCanvas.getContext('2d');
|
|
|
|
// 初始化游戏板
|
|
for (let r = 0; r < ROWS; r++) {
|
|
board[r] = [];
|
|
for (let c = 0; c < COLS; c++) {
|
|
board[r][c] = 0;
|
|
}
|
|
}
|
|
|
|
// 绑定事件
|
|
document.addEventListener('keydown', handleKeyPress);
|
|
document.getElementById('start-btn').addEventListener('click', startGame);
|
|
document.getElementById('restart-btn').addEventListener('click', startGame);
|
|
|
|
// 绘制初始游戏板
|
|
drawBoard();
|
|
}
|
|
|
|
// 创建新方块
|
|
function createPiece() {
|
|
const shapeIndex = Math.floor(Math.random() * SHAPES.length);
|
|
return {
|
|
shape: SHAPES[shapeIndex],
|
|
color: COLORS[shapeIndex],
|
|
x: Math.floor(COLS / 2) - Math.floor(SHAPES[shapeIndex][0].length / 2),
|
|
y: 0
|
|
};
|
|
}
|
|
|
|
// 绘制方块
|
|
function drawBlock(ctx, x, y, color, size = BLOCK_SIZE) {
|
|
ctx.fillStyle = color;
|
|
ctx.fillRect(x * size, y * size, size, size);
|
|
|
|
// 添加3D效果
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
|
ctx.fillRect(x * size, y * size, size, 2);
|
|
ctx.fillRect(x * size, y * size, 2, size);
|
|
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
|
|
ctx.fillRect(x * size + size - 2, y * size, 2, size);
|
|
ctx.fillRect(x * size, y * size + size - 2, size, 2);
|
|
}
|
|
|
|
// 绘制游戏板
|
|
function drawBoard() {
|
|
// 清空画布
|
|
ctx.fillStyle = '#1a1a2e';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// 绘制网格
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
|
|
for (let r = 0; r < ROWS; r++) {
|
|
for (let c = 0; c < COLS; c++) {
|
|
ctx.strokeRect(c * BLOCK_SIZE, r * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
|
|
}
|
|
}
|
|
|
|
// 绘制已放置的方块
|
|
for (let r = 0; r < ROWS; r++) {
|
|
for (let c = 0; c < COLS; c++) {
|
|
if (board[r][c]) {
|
|
drawBlock(ctx, c, r, board[r][c]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 绘制当前方块
|
|
function drawPiece() {
|
|
if (!currentPiece) return;
|
|
|
|
for (let r = 0; r < currentPiece.shape.length; r++) {
|
|
for (let c = 0; c < currentPiece.shape[r].length; c++) {
|
|
if (currentPiece.shape[r][c]) {
|
|
drawBlock(ctx, currentPiece.x + c, currentPiece.y + r, currentPiece.color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 绘制下一个方块
|
|
function drawNextPiece() {
|
|
if (!nextPiece) return;
|
|
|
|
nextCtx.fillStyle = '#fff';
|
|
nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
|
|
|
|
const size = 20;
|
|
const offsetX = (nextCanvas.width - nextPiece.shape[0].length * size) / 2;
|
|
const offsetY = (nextCanvas.height - nextPiece.shape.length * size) / 2;
|
|
|
|
for (let r = 0; r < nextPiece.shape.length; r++) {
|
|
for (let c = 0; c < nextPiece.shape[r].length; c++) {
|
|
if (nextPiece.shape[r][c]) {
|
|
nextCtx.fillStyle = nextPiece.color;
|
|
nextCtx.fillRect(offsetX + c * size, offsetY + r * size, size, size);
|
|
nextCtx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
|
|
nextCtx.strokeRect(offsetX + c * size, offsetY + r * size, size, size);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 检测碰撞
|
|
function collide(piece, offsetX = 0, offsetY = 0) {
|
|
for (let r = 0; r < piece.shape.length; r++) {
|
|
for (let c = 0; c < piece.shape[r].length; c++) {
|
|
if (piece.shape[r][c]) {
|
|
const newX = piece.x + c + offsetX;
|
|
const newY = piece.y + r + offsetY;
|
|
|
|
if (newX < 0 || newX >= COLS || newY >= ROWS) {
|
|
return true;
|
|
}
|
|
|
|
if (newY >= 0 && board[newY][newX]) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// 锁定方块
|
|
function lockPiece() {
|
|
for (let r = 0; r < currentPiece.shape.length; r++) {
|
|
for (let c = 0; c < currentPiece.shape[r].length; c++) {
|
|
if (currentPiece.shape[r][c]) {
|
|
if (currentPiece.y + r < 0) {
|
|
gameOver();
|
|
return;
|
|
}
|
|
board[currentPiece.y + r][currentPiece.x + c] = currentPiece.color;
|
|
}
|
|
}
|
|
}
|
|
|
|
clearLines();
|
|
currentPiece = nextPiece;
|
|
nextPiece = createPiece();
|
|
drawNextPiece();
|
|
|
|
if (collide(currentPiece)) {
|
|
gameOver();
|
|
}
|
|
}
|
|
|
|
// 消除行
|
|
function clearLines() {
|
|
let linesCleared = 0;
|
|
|
|
for (let r = ROWS - 1; r >= 0; r--) {
|
|
if (board[r].every(cell => cell !== 0)) {
|
|
board.splice(r, 1);
|
|
board.unshift(new Array(COLS).fill(0));
|
|
linesCleared++;
|
|
r++;
|
|
}
|
|
}
|
|
|
|
if (linesCleared > 0) {
|
|
updateScore(linesCleared);
|
|
}
|
|
}
|
|
|
|
// 更新分数
|
|
function updateScore(linesCleared) {
|
|
const points = [0, 100, 300, 500, 800];
|
|
score += points[linesCleared] * level;
|
|
lines += linesCleared;
|
|
|
|
// 升级
|
|
if (lines >= level * 10) {
|
|
level++;
|
|
dropInterval = Math.max(100, 1000 - (level - 1) * 100);
|
|
resetGameLoop();
|
|
}
|
|
|
|
updateDisplay();
|
|
}
|
|
|
|
// 更新显示
|
|
function updateDisplay() {
|
|
document.getElementById('score').textContent = score;
|
|
document.getElementById('level').textContent = level;
|
|
document.getElementById('lines').textContent = lines;
|
|
}
|
|
|
|
// 移动方块
|
|
function movePiece(offsetX, offsetY) {
|
|
if (!currentPiece || isPaused || isGameOver) return;
|
|
|
|
if (!collide(currentPiece, offsetX, offsetY)) {
|
|
currentPiece.x += offsetX;
|
|
currentPiece.y += offsetY;
|
|
draw();
|
|
}
|
|
}
|
|
|
|
// 旋转方块
|
|
function rotatePiece() {
|
|
if (!currentPiece || isPaused || isGameOver) return;
|
|
|
|
const rotated = currentPiece.shape[0].map((_, index) =>
|
|
currentPiece.shape.map(row => row[index]).reverse()
|
|
);
|
|
|
|
const originalShape = currentPiece.shape;
|
|
currentPiece.shape = rotated;
|
|
|
|
// 墙踢 - 尝试调整位置
|
|
const kicks = [0, -1, 1, -2, 2];
|
|
for (let kick of kicks) {
|
|
if (!collide(currentPiece, kick, 0)) {
|
|
currentPiece.x += kick;
|
|
draw();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 无法旋转,恢复原状
|
|
currentPiece.shape = originalShape;
|
|
}
|
|
|
|
// 硬降落
|
|
function hardDrop() {
|
|
if (!currentPiece || isPaused || isGameOver) return;
|
|
|
|
while (!collide(currentPiece, 0, 1)) {
|
|
currentPiece.y++;
|
|
score += 2;
|
|
}
|
|
lockPiece();
|
|
updateDisplay();
|
|
draw();
|
|
}
|
|
|
|
// 绘制
|
|
function draw() {
|
|
drawBoard();
|
|
drawPiece();
|
|
}
|
|
|
|
// 游戏循环
|
|
function gameStep() {
|
|
if (!isPaused && !isGameOver) {
|
|
if (!collide(currentPiece, 0, 1)) {
|
|
currentPiece.y++;
|
|
draw();
|
|
} else {
|
|
lockPiece();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 开始游戏循环
|
|
function startGameLoop() {
|
|
gameLoop = setInterval(gameStep, dropInterval);
|
|
}
|
|
|
|
// 重置游戏循环
|
|
function resetGameLoop() {
|
|
clearInterval(gameLoop);
|
|
startGameLoop();
|
|
}
|
|
|
|
// 开始游戏
|
|
function startGame() {
|
|
// 重置状态
|
|
for (let r = 0; r < ROWS; r++) {
|
|
for (let c = 0; c < COLS; c++) {
|
|
board[r][c] = 0;
|
|
}
|
|
}
|
|
score = 0;
|
|
level = 1;
|
|
lines = 0;
|
|
dropInterval = 1000;
|
|
isPaused = false;
|
|
isGameOver = false;
|
|
|
|
// 隐藏游戏结束界面
|
|
document.getElementById('game-over').classList.add('hidden');
|
|
|
|
// 创建方块
|
|
currentPiece = createPiece();
|
|
nextPiece = createPiece();
|
|
|
|
updateDisplay();
|
|
drawNextPiece();
|
|
draw();
|
|
startGameLoop();
|
|
}
|
|
|
|
// 游戏结束
|
|
function gameOver() {
|
|
isGameOver = true;
|
|
clearInterval(gameLoop);
|
|
document.getElementById('final-score').textContent = score;
|
|
document.getElementById('game-over').classList.remove('hidden');
|
|
}
|
|
|
|
// 键盘事件处理
|
|
function handleKeyPress(e) {
|
|
if (isGameOver) return;
|
|
|
|
switch (e.key) {
|
|
case 'ArrowLeft':
|
|
e.preventDefault();
|
|
movePiece(-1, 0);
|
|
break;
|
|
case 'ArrowRight':
|
|
e.preventDefault();
|
|
movePiece(1, 0);
|
|
break;
|
|
case 'ArrowDown':
|
|
e.preventDefault();
|
|
movePiece(0, 1);
|
|
score += 1;
|
|
updateDisplay();
|
|
break;
|
|
case 'ArrowUp':
|
|
e.preventDefault();
|
|
rotatePiece();
|
|
break;
|
|
case ' ':
|
|
e.preventDefault();
|
|
hardDrop();
|
|
break;
|
|
case 'p':
|
|
case 'P':
|
|
isPaused = !isPaused;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 启动
|
|
window.onload = init;
|