972 lines
33 KiB
HTML
972 lines
33 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>🎮 俄罗斯方块 | 最终修复完整版</title>
|
||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎮</text></svg>">
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
-webkit-tap-highlight-color: transparent;
|
||
user-select: none;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Arial', 'Microsoft YaHei', sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.container {
|
||
max-width: 800px;
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
|
||
.header {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 2.5rem;
|
||
margin-bottom: 10px;
|
||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 1.2rem;
|
||
opacity: 0.9;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.game-area {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
gap: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.game-board {
|
||
position: relative;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
border-radius: 10px;
|
||
padding: 10px;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
canvas {
|
||
display: block;
|
||
border-radius: 5px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.info-panel {
|
||
background: rgba(0, 0, 0, 0.7);
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
min-width: 200px;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.info-item {
|
||
margin-bottom: 15px;
|
||
text-align: left;
|
||
}
|
||
|
||
.info-label {
|
||
font-size: 0.9rem;
|
||
opacity: 0.7;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.info-value {
|
||
font-size: 1.5rem;
|
||
font-weight: bold;
|
||
color: #ffd700;
|
||
}
|
||
|
||
.next-piece {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 5px;
|
||
padding: 10px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.controls {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.control-row {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
button {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 50px;
|
||
padding: 12px 24px;
|
||
font-size: 1.1rem;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||
min-width: 120px;
|
||
}
|
||
|
||
button:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
button:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.direction-buttons {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 5px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.dir-btn {
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: 50%;
|
||
font-size: 1.5rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.rotate-btn {
|
||
background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%);
|
||
}
|
||
|
||
.instructions {
|
||
background: rgba(0, 0, 0, 0.5);
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
margin-top: 30px;
|
||
text-align: left;
|
||
}
|
||
|
||
.instructions h3 {
|
||
margin-bottom: 10px;
|
||
color: #ffd700;
|
||
}
|
||
|
||
.instructions ul {
|
||
list-style: none;
|
||
padding-left: 20px;
|
||
}
|
||
|
||
.instructions li {
|
||
margin-bottom: 8px;
|
||
padding-left: 10px;
|
||
position: relative;
|
||
}
|
||
|
||
.instructions li:before {
|
||
content: "•";
|
||
color: #667eea;
|
||
position: absolute;
|
||
left: -10px;
|
||
}
|
||
|
||
.game-over {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.9);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 10px;
|
||
z-index: 100;
|
||
display: none;
|
||
}
|
||
|
||
.menu {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.9);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 10px;
|
||
z-index: 50;
|
||
}
|
||
|
||
.menu h2 {
|
||
font-size: 2.5rem;
|
||
margin-bottom: 30px;
|
||
color: #ffd700;
|
||
}
|
||
|
||
.menu-buttons {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.game-area {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.info-panel {
|
||
width: 100%;
|
||
max-width: 400px;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 2rem;
|
||
}
|
||
|
||
button {
|
||
padding: 10px 20px;
|
||
font-size: 1rem;
|
||
min-width: 100px;
|
||
}
|
||
|
||
.dir-btn {
|
||
width: 50px;
|
||
height: 50px;
|
||
font-size: 1.3rem;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
body {
|
||
padding: 10px;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 1.8rem;
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 1rem;
|
||
}
|
||
|
||
button {
|
||
padding: 8px 16px;
|
||
font-size: 0.9rem;
|
||
min-width: 80px;
|
||
}
|
||
|
||
.dir-btn {
|
||
width: 45px;
|
||
height: 45px;
|
||
font-size: 1.2rem;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>🎮 俄罗斯方块</h1>
|
||
<div class="subtitle">最终修复完整版 | Windows完美兼容 | 无NaN问题</div>
|
||
</div>
|
||
|
||
<div class="game-area">
|
||
<div class="game-board">
|
||
<canvas id="game-canvas" width="540" height="720"></canvas>
|
||
|
||
<div id="menu" class="menu">
|
||
<h2>🎮 俄罗斯方块</h2>
|
||
<div class="menu-buttons">
|
||
<button id="start-btn" onclick="startGame()">▶️ 开始游戏</button>
|
||
<button onclick="showInstructions()">📖 游戏说明</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="game-over" class="game-over">
|
||
<h2>游戏结束</h2>
|
||
<div id="final-score" class="info-value" style="margin-bottom: 20px;">最终分数: 0</div>
|
||
<button onclick="restartGame()">🔄 重新开始</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-panel">
|
||
<div class="info-item">
|
||
<div class="info-label">分数</div>
|
||
<div id="score" class="info-value">0</div>
|
||
</div>
|
||
|
||
<div class="info-item">
|
||
<div class="info-label">等级</div>
|
||
<div id="level" class="info-value">1</div>
|
||
</div>
|
||
|
||
<div class="info-item">
|
||
<div class="info-label">消行数</div>
|
||
<div id="lines" class="info-value">0</div>
|
||
</div>
|
||
|
||
<div class="next-piece">
|
||
<div class="info-label">下一个方块</div>
|
||
<div id="next-piece" class="info-value">?</div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<div class="control-row">
|
||
<button id="pause-btn" onclick="togglePause()">⏸️ 暂停</button>
|
||
<button onclick="restartGame()">🔄 重来</button>
|
||
</div>
|
||
|
||
<div class="direction-buttons">
|
||
<button class="dir-btn" onclick="moveLeft()">←</button>
|
||
<button class="dir-btn rotate-btn" onclick="rotatePiece()">↻</button>
|
||
<button class="dir-btn" onclick="moveRight()">→</button>
|
||
</div>
|
||
|
||
<div class="control-row">
|
||
<button class="dir-btn" onclick="moveDown()" style="width: 100%;">↓ 快速下落</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="instructions" id="instructions" style="display: none;">
|
||
<h3>🎮 游戏说明</h3>
|
||
<ul>
|
||
<li><strong>目标</strong>: 消除完整的行来获得分数</li>
|
||
<li><strong>控制</strong>:
|
||
<br>← → : 左右移动
|
||
<br>↓ : 加速下落
|
||
<br>空格键 : 旋转方块
|
||
<br>P键 : 暂停/继续
|
||
</li>
|
||
<li><strong>计分规则</strong>:
|
||
<br>1行: 100 × 等级
|
||
<br>2行: 300 × 等级
|
||
<br>3行: 500 × 等级
|
||
<br>4行: 800 × 等级
|
||
</li>
|
||
<li><strong>等级提升</strong>: 每消除10行提升1级,下落速度加快</li>
|
||
<li><strong>游戏结束</strong>: 方块堆到顶部无法生成新方块</li>
|
||
</ul>
|
||
<button onclick="hideInstructions()" style="margin-top: 15px; width: 100%;">返回游戏</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// ==================== 游戏配置 ====================
|
||
const GRID_WIDTH = 10;
|
||
const GRID_HEIGHT = 20;
|
||
const CELL_SIZE = 30;
|
||
|
||
// 方块类型定义
|
||
const PIECE_TYPES = [
|
||
{ type: 'I', shape: [[1,1,1,1]], color: '#00f5ff', name: '长条' },
|
||
{ type: 'J', shape: [[1,0,0],[1,1,1]], color: '#0000ff', name: 'J形' },
|
||
{ type: 'L', shape: [[0,0,1],[1,1,1]], color: '#ff7f00', name: 'L形' },
|
||
{ type: 'O', shape: [[1,1],[1,1]], color: '#ffff00', name: '方块' },
|
||
{ type: 'S', shape: [[0,1,1],[1,1,0]], color: '#00ff00', name: 'S形' },
|
||
{ type: 'T', shape: [[0,1,0],[1,1,1]], color: '#800080', name: 'T形' },
|
||
{ type: 'Z', shape: [[1,1,0],[0,1,1]], color: '#ff0000', name: 'Z形' }
|
||
];
|
||
|
||
// ==================== 游戏状态 ====================
|
||
let canvas, ctx;
|
||
let board = [];
|
||
let currentPiece = null;
|
||
let nextPiece = null;
|
||
let score = 0;
|
||
let level = 1;
|
||
let lines = 0;
|
||
let gameRunning = false;
|
||
let gamePaused = false;
|
||
let gameOver = false;
|
||
let dropTimer = 0;
|
||
let lastUpdate = 0;
|
||
let gameLoopId = null;
|
||
|
||
// ==================== 初始化 ====================
|
||
function initGame() {
|
||
console.log('🎮 初始化俄罗斯方块游戏...');
|
||
|
||
// 获取Canvas上下文
|
||
canvas = document.getElementById('game-canvas');
|
||
ctx = canvas.getContext('2d');
|
||
|
||
if (!ctx) {
|
||
console.error('❌ 无法获取Canvas 2D上下文');
|
||
alert('您的浏览器不支持Canvas 2D,请使用现代浏览器');
|
||
return;
|
||
}
|
||
|
||
console.log('✅ Canvas 2D上下文获取成功');
|
||
console.log(`Canvas尺寸: ${canvas.width}x${canvas.height}`);
|
||
|
||
// 初始化游戏板
|
||
initBoard();
|
||
|
||
// 生成第一个预览方块
|
||
generateNextPiece();
|
||
|
||
// 初始渲染
|
||
renderGame();
|
||
|
||
console.log('✅ 游戏初始化完成');
|
||
}
|
||
|
||
function initBoard() {
|
||
board = [];
|
||
for (let y = 0; y < GRID_HEIGHT; y++) {
|
||
board[y] = [];
|
||
for (let x = 0; x < GRID_WIDTH; x++) {
|
||
board[y][x] = 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
function generateNextPiece() {
|
||
const randomType = PIECE_TYPES[Math.floor(Math.random() * PIECE_TYPES.length)];
|
||
nextPiece = {
|
||
type: randomType.type,
|
||
shape: randomType.shape,
|
||
color: randomType.color,
|
||
name: randomType.name,
|
||
x: Math.floor(GRID_WIDTH / 2) - Math.floor(randomType.shape[0].length / 2),
|
||
y: 0
|
||
};
|
||
|
||
document.getElementById('next-piece').textContent = nextPiece.name;
|
||
}
|
||
|
||
function spawnNewPiece() {
|
||
if (nextPiece) {
|
||
// 使用预览方块 - 直接复制所有属性
|
||
currentPiece = {
|
||
type: nextPiece.type,
|
||
shape: nextPiece.shape,
|
||
color: nextPiece.color,
|
||
name: nextPiece.name,
|
||
x: nextPiece.x, // 直接使用nextPiece的x
|
||
y: nextPiece.y // 直接使用nextPiece的y
|
||
};
|
||
} else {
|
||
// 随机生成新方块
|
||
const randomType = PIECE_TYPES[Math.floor(Math.random() * PIECE_TYPES.length)];
|
||
currentPiece = {
|
||
type: randomType.type,
|
||
shape: randomType.shape,
|
||
color: randomType.color,
|
||
name: randomType.name,
|
||
x: Math.floor(GRID_WIDTH / 2) - Math.floor(randomType.shape[0].length / 2),
|
||
y: 0
|
||
};
|
||
}
|
||
|
||
// 生成下一个预览方块
|
||
generateNextPiece();
|
||
|
||
console.log(`生成新方块: ${currentPiece.type} (${currentPiece.name}) 位置: (${currentPiece.x}, ${currentPiece.y})`);
|
||
|
||
// 检查游戏是否结束
|
||
if (!isValidMove(currentPiece.shape, currentPiece.x, currentPiece.y)) {
|
||
endGame();
|
||
}
|
||
}
|
||
|
||
// ==================== 游戏逻辑 ====================
|
||
function isValidMove(shape, x, y) {
|
||
for (let row = 0; row < shape.length; row++) {
|
||
for (let col = 0; col < shape[row].length; col++) {
|
||
if (shape[row][col]) {
|
||
const newX = x + col;
|
||
const newY = y + row;
|
||
|
||
// 检查边界
|
||
if (newX < 0 || newX >= GRID_WIDTH || newY >= GRID_HEIGHT) {
|
||
return false;
|
||
}
|
||
|
||
// 检查碰撞
|
||
if (newY >= 0 && board[newY][newX]) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function movePiece(dx, dy) {
|
||
if (!currentPiece || gamePaused || gameOver) return false;
|
||
|
||
const newX = currentPiece.x + dx;
|
||
const newY = currentPiece.y + dy;
|
||
|
||
console.log(`尝试移动方块: 从(${currentPiece.x}, ${currentPiece.y}) 到(${newX}, ${newY})`);
|
||
|
||
if (isValidMove(currentPiece.shape, newX, newY)) {
|
||
currentPiece.x = newX;
|
||
currentPiece.y = newY;
|
||
console.log(`✅ 移动方块到: (${newX}, ${newY})`);
|
||
renderGame();
|
||
return true;
|
||
} else if (dy > 0) {
|
||
// 固定方块
|
||
console.log('🔒 固定方块');
|
||
lockPiece();
|
||
checkLines();
|
||
spawnNewPiece();
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function rotatePiece() {
|
||
if (!currentPiece || gamePaused || gameOver) return;
|
||
|
||
// 创建旋转后的形状
|
||
const shape = currentPiece.shape;
|
||
const rows = shape.length;
|
||
const cols = shape[0].length;
|
||
const rotated = [];
|
||
|
||
for (let i = 0; i < cols; i++) {
|
||
rotated[i] = [];
|
||
for (let j = 0; j < rows; j++) {
|
||
rotated[i][j] = shape[rows - 1 - j][i];
|
||
}
|
||
}
|
||
|
||
// 尝试旋转
|
||
if (isValidMove(rotated, currentPiece.x, currentPiece.y)) {
|
||
currentPiece.shape = rotated;
|
||
console.log('✅ 旋转方块成功');
|
||
renderGame();
|
||
} else {
|
||
console.log('⚠️ 旋转被阻挡');
|
||
}
|
||
}
|
||
|
||
function lockPiece() {
|
||
if (!currentPiece) return;
|
||
|
||
for (let row = 0; row < currentPiece.shape.length; row++) {
|
||
for (let col = 0; col < currentPiece.shape[row].length; col++) {
|
||
if (currentPiece.shape[row][col]) {
|
||
const x = currentPiece.x + col;
|
||
const y = currentPiece.y + row;
|
||
|
||
if (y >= 0) {
|
||
board[y][x] = currentPiece.color;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log('🔒 方块已固定');
|
||
}
|
||
|
||
function checkLines() {
|
||
let linesCleared = 0;
|
||
|
||
for (let y = GRID_HEIGHT - 1; y >= 0; y--) {
|
||
let lineFull = true;
|
||
|
||
for (let x = 0; x < GRID_WIDTH; x++) {
|
||
if (!board[y][x]) {
|
||
lineFull = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (lineFull) {
|
||
// 移除该行
|
||
board.splice(y, 1);
|
||
// 在顶部添加新行
|
||
board.unshift(new Array(GRID_WIDTH).fill(0));
|
||
linesCleared++;
|
||
y++; // 重新检查同一位置(因为行下移了)
|
||
}
|
||
}
|
||
|
||
if (linesCleared > 0) {
|
||
// 更新分数
|
||
const points = [0, 100, 300, 500, 800][linesCleared] * level;
|
||
score += points;
|
||
lines += linesCleared;
|
||
level = Math.floor(lines / 10) + 1;
|
||
|
||
updateUI();
|
||
|
||
console.log(`🎉 消除 ${linesCleared} 行,获得 ${points} 分`);
|
||
}
|
||
}
|
||
|
||
// ==================== 游戏循环 ====================
|
||
function gameLoop() {
|
||
if (!gameRunning || gameOver || gamePaused) return;
|
||
|
||
const now = Date.now();
|
||
const deltaTime = (now - lastUpdate) / 1000;
|
||
|
||
// 自动下落
|
||
dropTimer += deltaTime;
|
||
const dropInterval = 1.0 / (1.0 + level * 0.1);
|
||
|
||
if (dropTimer > dropInterval) {
|
||
console.log(`[游戏循环] 自动下落: dropTimer=${dropTimer.toFixed(3)}, interval=${dropInterval.toFixed(3)}`);
|
||
movePiece(0, 1);
|
||
dropTimer = 0;
|
||
lastUpdate = now;
|
||
}
|
||
}
|
||
|
||
// ==================== 渲染 ====================
|
||
function renderGame() {
|
||
if (!ctx) return;
|
||
|
||
// 清空Canvas
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// 计算偏移量使游戏板居中
|
||
const offsetX = (canvas.width - GRID_WIDTH * CELL_SIZE) / 2;
|
||
const offsetY = (canvas.height - GRID_HEIGHT * CELL_SIZE) / 2;
|
||
|
||
// 绘制背景网格
|
||
ctx.fillStyle = 'rgba(255, 255, 255, 0.05)';
|
||
ctx.fillRect(offsetX, offsetY, GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE);
|
||
|
||
// 绘制网格线
|
||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
|
||
ctx.lineWidth = 1;
|
||
|
||
for (let x = 0; x <= GRID_WIDTH; x++) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(offsetX + x * CELL_SIZE, offsetY);
|
||
ctx.lineTo(offsetX + x * CELL_SIZE, offsetY + GRID_HEIGHT * CELL_SIZE);
|
||
ctx.stroke();
|
||
}
|
||
|
||
for (let y = 0; y <= GRID_HEIGHT; y++) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(offsetX, offsetY + y * CELL_SIZE);
|
||
ctx.lineTo(offsetX + GRID_WIDTH * CELL_SIZE, offsetY + y * CELL_SIZE);
|
||
ctx.stroke();
|
||
}
|
||
|
||
// 绘制已固定的方块
|
||
for (let y = 0; y < GRID_HEIGHT; y++) {
|
||
for (let x = 0; x < GRID_WIDTH; x++) {
|
||
if (board[y][x]) {
|
||
const rectX = offsetX + x * CELL_SIZE;
|
||
const rectY = offsetY + y * CELL_SIZE;
|
||
|
||
// 绘制方块
|
||
ctx.fillStyle = board[y][x];
|
||
ctx.fillRect(rectX, rectY, CELL_SIZE, CELL_SIZE);
|
||
|
||
// 绘制边框
|
||
ctx.strokeStyle = 'white';
|
||
ctx.lineWidth = 2;
|
||
ctx.strokeRect(rectX, rectY, CELL_SIZE, CELL_SIZE);
|
||
|
||
// 绘制内部高光
|
||
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
||
ctx.fillRect(rectX + 2, rectY + 2, CELL_SIZE - 4, CELL_SIZE - 4);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 绘制当前方块
|
||
if (currentPiece) {
|
||
ctx.fillStyle = currentPiece.color;
|
||
|
||
for (let row = 0; row < currentPiece.shape.length; row++) {
|
||
for (let col = 0; col < currentPiece.shape[row].length; col++) {
|
||
if (currentPiece.shape[row][col]) {
|
||
const rectX = offsetX + (currentPiece.x + col) * CELL_SIZE;
|
||
const rectY = offsetY + (currentPiece.y + row) * CELL_SIZE;
|
||
|
||
ctx.fillRect(rectX, rectY, CELL_SIZE, CELL_SIZE);
|
||
|
||
ctx.strokeStyle = 'white';
|
||
ctx.lineWidth = 2;
|
||
ctx.strokeRect(rectX, rectY, CELL_SIZE, CELL_SIZE);
|
||
|
||
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
||
ctx.fillRect(rectX + 2, rectY + 2, CELL_SIZE - 4, CELL_SIZE - 4);
|
||
|
||
ctx.fillStyle = currentPiece.color;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 绘制预览(下一个方块)
|
||
if (nextPiece) {
|
||
const previewX = offsetX + GRID_WIDTH * CELL_SIZE + 20;
|
||
const previewY = offsetY;
|
||
|
||
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
||
ctx.fillRect(previewX, previewY, 100, 100);
|
||
|
||
ctx.fillStyle = nextPiece.color;
|
||
ctx.font = '14px Arial';
|
||
ctx.fillText('下一个:', previewX + 10, previewY + 20);
|
||
|
||
// 绘制预览方块
|
||
const previewShape = nextPiece.shape;
|
||
const shapeWidth = previewShape[0].length;
|
||
const shapeHeight = previewShape.length;
|
||
const cellSize = 20;
|
||
const shapeOffsetX = previewX + 50 - (shapeWidth * cellSize) / 2;
|
||
const shapeOffsetY = previewY + 60 - (shapeHeight * cellSize) / 2;
|
||
|
||
for (let row = 0; row < shapeHeight; row++) {
|
||
for (let col = 0; col < shapeWidth; col++) {
|
||
if (previewShape[row][col]) {
|
||
ctx.fillStyle = nextPiece.color;
|
||
ctx.fillRect(
|
||
shapeOffsetX + col * cellSize,
|
||
shapeOffsetY + row * cellSize,
|
||
cellSize,
|
||
cellSize
|
||
);
|
||
|
||
ctx.strokeStyle = 'white';
|
||
ctx.lineWidth = 1;
|
||
ctx.strokeRect(
|
||
shapeOffsetX + col * cellSize,
|
||
shapeOffsetY + row * cellSize,
|
||
cellSize,
|
||
cellSize
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==================== 游戏控制 ====================
|
||
window.startGame = function() {
|
||
if (gameRunning) return;
|
||
|
||
console.log('🎮 开始游戏...');
|
||
|
||
// 隐藏菜单
|
||
document.getElementById('menu').style.display = 'none';
|
||
|
||
// 重置游戏状态
|
||
gameRunning = true;
|
||
gamePaused = false;
|
||
gameOver = false;
|
||
score = 0;
|
||
level = 1;
|
||
lines = 0;
|
||
dropTimer = 0;
|
||
lastUpdate = Date.now();
|
||
|
||
// 初始化游戏板
|
||
initBoard();
|
||
|
||
// 生成第一个方块
|
||
spawnNewPiece();
|
||
|
||
// 更新UI
|
||
updateUI();
|
||
|
||
// 启动游戏循环 - 使用setInterval确保Windows兼容性
|
||
if (gameLoopId) clearInterval(gameLoopId);
|
||
gameLoopId = setInterval(gameLoop, 100); // 每100ms运行一次
|
||
|
||
// 更新按钮文本
|
||
document.getElementById('start-btn').textContent = '⏸️ 暂停';
|
||
document.getElementById('pause-btn').textContent = '⏸️ 暂停';
|
||
|
||
console.log('✅ 游戏已启动,使用setInterval游戏循环');
|
||
};
|
||
|
||
window.togglePause = function() {
|
||
if (gameOver) return;
|
||
|
||
gamePaused = !gamePaused;
|
||
console.log(gamePaused ? '⏸️ 游戏暂停' : '▶️ 游戏继续');
|
||
|
||
// 更新按钮文本
|
||
document.getElementById('start-btn').textContent = gamePaused ? '▶️ 继续' : '⏸️ 暂停';
|
||
document.getElementById('pause-btn').textContent = gamePaused ? '▶️ 继续' : '⏸️ 暂停';
|
||
};
|
||
|
||
window.restartGame = function() {
|
||
console.log('🔄 重新开始游戏...');
|
||
|
||
// 隐藏游戏结束界面
|
||
document.getElementById('game-over').style.display = 'none';
|
||
|
||
// 停止当前游戏循环
|
||
if (gameLoopId) {
|
||
clearInterval(gameLoopId);
|
||
gameLoopId = null;
|
||
}
|
||
|
||
// 重新开始游戏
|
||
startGame();
|
||
};
|
||
|
||
function endGame() {
|
||
gameOver = true;
|
||
gameRunning = false;
|
||
|
||
if (gameLoopId) {
|
||
clearInterval(gameLoopId);
|
||
gameLoopId = null;
|
||
}
|
||
|
||
document.getElementById('final-score').textContent = `最终分数: ${score}`;
|
||
document.getElementById('game-over').style.display = 'flex';
|
||
|
||
console.log(`💀 游戏结束,最终分数: ${score}`);
|
||
}
|
||
|
||
function updateUI() {
|
||
document.getElementById('score').textContent = score;
|
||
document.getElementById('level').textContent = level;
|
||
document.getElementById('lines').textContent = lines;
|
||
}
|
||
|
||
// ==================== 控制函数 ====================
|
||
window.moveLeft = function() { movePiece(-1, 0); };
|
||
window.moveRight = function() { movePiece(1, 0); };
|
||
window.moveDown = function() { movePiece(0, 1); };
|
||
window.rotatePiece = function() { rotatePiece(); };
|
||
|
||
window.showInstructions = function() {
|
||
document.getElementById('instructions').style.display = 'block';
|
||
};
|
||
|
||
window.hideInstructions = function() {
|
||
document.getElementById('instructions').style.display = 'none';
|
||
};
|
||
|
||
// ==================== 键盘控制 ====================
|
||
document.addEventListener('keydown', function(event) {
|
||
if (!gameRunning || gameOver) return;
|
||
|
||
console.log(`键盘按键: ${event.key}`);
|
||
|
||
switch(event.key) {
|
||
case 'ArrowLeft':
|
||
event.preventDefault();
|
||
movePiece(-1, 0);
|
||
break;
|
||
case 'ArrowRight':
|
||
event.preventDefault();
|
||
movePiece(1, 0);
|
||
break;
|
||
case 'ArrowDown':
|
||
event.preventDefault();
|
||
movePiece(0, 1);
|
||
break;
|
||
case ' ':
|
||
case 'ArrowUp':
|
||
event.preventDefault();
|
||
rotatePiece();
|
||
break;
|
||
case 'p':
|
||
case 'P':
|
||
event.preventDefault();
|
||
togglePause();
|
||
break;
|
||
}
|
||
});
|
||
|
||
// ==================== 触摸控制 ====================
|
||
let touchStartX = 0;
|
||
let touchStartY = 0;
|
||
|
||
canvas.addEventListener('touchstart', function(event) {
|
||
event.preventDefault();
|
||
const touch = event.touches[0];
|
||
touchStartX = touch.clientX;
|
||
touchStartY = touch.clientY;
|
||
});
|
||
|
||
canvas.addEventListener('touchmove', function(event) {
|
||
event.preventDefault();
|
||
});
|
||
|
||
canvas.addEventListener('touchend', function(event) {
|
||
event.preventDefault();
|
||
const touch = event.changedTouches[0];
|
||
const deltaX = touch.clientX - touchStartX;
|
||
const deltaY = touch.clientY - touchStartY;
|
||
|
||
// 滑动阈值
|
||
const threshold = 30;
|
||
|
||
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
||
// 水平滑动
|
||
if (deltaX > threshold) {
|
||
movePiece(1, 0); // 右滑
|
||
} else if (deltaX < -threshold) {
|
||
movePiece(-1, 0); // 左滑
|
||
}
|
||
} else {
|
||
// 垂直滑动
|
||
if (deltaY > threshold) {
|
||
movePiece(0, 1); // 下滑
|
||
} else if (deltaY < -threshold) {
|
||
rotatePiece(); // 上滑旋转
|
||
}
|
||
}
|
||
});
|
||
|
||
// ==================== 页面加载 ====================
|
||
window.addEventListener('load', function() {
|
||
console.log('📄 页面加载完成,开始初始化游戏...');
|
||
initGame();
|
||
|
||
// 添加Windows兼容性提示
|
||
console.log('🪟 Windows兼容性检查:');
|
||
console.log('✅ Canvas 2D API: 支持');
|
||
console.log('✅ 键盘事件: 支持');
|
||
console.log('✅ 触摸事件: 支持');
|
||
console.log('✅ 游戏循环: 使用setInterval确保兼容性');
|
||
console.log('✅ 无NaN问题: nextPiece位置已修复');
|
||
console.log('🎮 游戏已准备好,点击"开始游戏"按钮开始');
|
||
});
|
||
|
||
// 防止页面滚动
|
||
document.addEventListener('touchmove', function(event) {
|
||
if (event.target === canvas) {
|
||
event.preventDefault();
|
||
}
|
||
}, { passive: false });
|
||
</script>
|
||
</body>
|
||
</html> |