初始提交:网页版俄罗斯方块游戏

- 使用 HTML + CSS + JavaScript 实现
- 实现完整的游戏逻辑:方块移动、旋转、消行、计分
- 支持等级系统和加速机制
- 响应式设计,支持移动端
- 下一个方块预览功能
- 游戏暂停/继续功能
This commit is contained in:
OpenClaw Xiami
2026-02-28 15:56:49 +00:00
commit 867611ab45
3 changed files with 626 additions and 0 deletions

60
index.html Normal file
View File

@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>俄罗斯方块 - Tetris</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="game-container">
<div class="header">
<h1>🎮 俄罗斯方块</h1>
</div>
<div class="game-area">
<div class="info-panel">
<div class="info-box">
<h3>分数</h3>
<div id="score" class="info-value">0</div>
</div>
<div class="info-box">
<h3>等级</h3>
<div id="level" class="info-value">1</div>
</div>
<div class="info-box">
<h3>消除行数</h3>
<div id="lines" class="info-value">0</div>
</div>
<div class="info-box">
<h3>下一个</h3>
<canvas id="next-canvas" width="100" height="100"></canvas>
</div>
</div>
<div class="canvas-container">
<canvas id="game-canvas" width="300" height="600"></canvas>
<div id="game-over" class="game-over hidden">
<h2>游戏结束</h2>
<p>最终分数: <span id="final-score">0</span></p>
<button id="restart-btn" class="btn">重新开始</button>
</div>
</div>
<div class="controls-panel">
<h3>操作说明</h3>
<ul>
<li>← → 左右移动</li>
<li>↑ 旋转</li>
<li>↓ 加速下落</li>
<li>空格 直接落下</li>
<li>P 暂停/继续</li>
</ul>
<button id="start-btn" class="btn">开始游戏</button>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

386
script.js Normal file
View File

@@ -0,0 +1,386 @@
// 游戏常量
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;

180
style.css Normal file
View File

@@ -0,0 +1,180 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.game-container {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 900px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
font-size: 2.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.game-area {
display: flex;
gap: 30px;
justify-content: center;
flex-wrap: wrap;
}
.info-panel {
display: flex;
flex-direction: column;
gap: 15px;
}
.info-box {
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
text-align: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.info-box h3 {
font-size: 0.9rem;
color: #666;
margin-bottom: 5px;
}
.info-value {
font-size: 1.5rem;
font-weight: bold;
color: #667eea;
}
#next-canvas {
background: #fff;
border: 2px solid #667eea;
border-radius: 8px;
}
.canvas-container {
position: relative;
}
#game-canvas {
background: #1a1a2e;
border: 3px solid #667eea;
border-radius: 10px;
display: block;
}
.game-over {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.98);
padding: 40px;
border-radius: 15px;
text-align: center;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
z-index: 10;
}
.game-over.hidden {
display: none;
}
.game-over h2 {
font-size: 2rem;
color: #e74c3c;
margin-bottom: 15px;
}
.game-over p {
font-size: 1.2rem;
margin-bottom: 20px;
color: #333;
}
.controls-panel {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
min-width: 200px;
}
.controls-panel h3 {
color: #667eea;
margin-bottom: 15px;
text-align: center;
}
.controls-panel ul {
list-style: none;
}
.controls-panel li {
padding: 8px 0;
color: #555;
font-size: 0.9rem;
}
.btn {
width: 100%;
padding: 12px 24px;
margin-top: 20px;
font-size: 1rem;
font-weight: bold;
color: white;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.btn:active {
transform: translateY(0);
}
@media (max-width: 900px) {
.game-area {
flex-direction: column;
align-items: center;
}
.info-panel {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
.controls-panel {
min-width: 100%;
text-align: center;
}
}