669 lines
23 KiB
HTML
669 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>俄罗斯方块 - 无CDN测试版</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
width: 100%;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
background: #0a0a1a;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
#game-container {
|
|
width: 540px;
|
|
height: 720px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 20px;
|
|
}
|
|
|
|
.game-area {
|
|
width: 300px;
|
|
height: 600px;
|
|
background: #111827;
|
|
border: 2px solid #2563eb;
|
|
border-radius: 8px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.grid {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: grid;
|
|
grid-template-columns: repeat(10, 1fr);
|
|
grid-template-rows: repeat(20, 1fr);
|
|
gap: 1px;
|
|
}
|
|
|
|
.grid-cell {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.block {
|
|
position: absolute;
|
|
width: 30px;
|
|
height: 30px;
|
|
border: 2px solid;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.block.I { background: #00f5ff; border-color: #00bcd4; }
|
|
.block.J { background: #0000ff; border-color: #0000cd; }
|
|
.block.L { background: #ff7f00; border-color: #ff8c00; }
|
|
.block.O { background: #ffff00; border-color: #ffd700; }
|
|
.block.S { background: #00ff00; border-color: #32cd32; }
|
|
.block.T { background: #800080; border-color: #9400d3; }
|
|
.block.Z { background: #ff0000; border-color: #dc143c; }
|
|
|
|
.controls-container {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
|
|
.control-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.action-button {
|
|
padding: 12px 24px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
min-width: 100px;
|
|
justify-content: center;
|
|
}
|
|
|
|
.action-button.start {
|
|
background: linear-gradient(145deg, #10b981 0%, #047857 100%);
|
|
color: white;
|
|
}
|
|
|
|
.action-button.restart {
|
|
background: linear-gradient(145deg, #f59e0b 0%, #d97706 100%);
|
|
color: white;
|
|
}
|
|
|
|
.control-button {
|
|
width: 65px;
|
|
height: 65px;
|
|
background: linear-gradient(145deg, #1e293b 0%, #0f172a 100%);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
font-size: 28px;
|
|
color: #fff;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
|
transition: all 0.1s ease;
|
|
}
|
|
|
|
.control-row:nth-child(2) {
|
|
justify-content: center;
|
|
gap: 5px;
|
|
}
|
|
|
|
.info-panel {
|
|
width: 100%;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
display: flex;
|
|
justify-content: space-around;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.info-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 5px;
|
|
}
|
|
|
|
.info-label {
|
|
color: #aaa;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.info-value {
|
|
color: #fff;
|
|
font-weight: bold;
|
|
font-size: 20px;
|
|
}
|
|
|
|
.status {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background: rgba(0, 0, 0, 0.9);
|
|
color: white;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
display: none;
|
|
z-index: 100;
|
|
}
|
|
|
|
.log {
|
|
position: fixed;
|
|
bottom: 10px;
|
|
left: 10px;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
color: #68d391;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
max-width: 300px;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="game-container">
|
|
<div class="game-area" id="game-area">
|
|
<div class="grid" id="grid"></div>
|
|
<div class="status" id="status">
|
|
<h2 id="status-title">游戏暂停</h2>
|
|
<p id="status-message">点击按钮继续游戏</p>
|
|
<button id="status-button" style="margin-top: 10px; padding: 10px 20px; background: #2563eb; color: white; border: none; border-radius: 5px; cursor: pointer;">继续游戏</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls-container">
|
|
<!-- 第一行:开始/旋转/重来按钮 -->
|
|
<div class="control-row">
|
|
<button class="action-button start" id="start-btn">
|
|
<span>▶️</span> 开始
|
|
</button>
|
|
<div class="control-button rotate" id="rotate-btn">↻ 旋转</div>
|
|
<button class="action-button restart" id="restart-btn">
|
|
<span>🔄</span> 重来
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 第二行:方向控制按钮 -->
|
|
<div class="control-row">
|
|
<div class="control-button left" id="left-btn">←</div>
|
|
<div class="control-button down" id="down-btn">↓</div>
|
|
<div class="control-button right" id="right-btn">→</div>
|
|
</div>
|
|
|
|
<!-- 游戏信息面板 -->
|
|
<div class="info-panel">
|
|
<div class="info-item">
|
|
<span class="info-label">分数</span>
|
|
<span class="info-value" id="score">0</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">等级</span>
|
|
<span class="info-value" id="level">0</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">消行</span>
|
|
<span class="info-value" id="lines">0</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">速度</span>
|
|
<span class="info-value" id="speed">1.0x</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="log" id="log">
|
|
<div>游戏日志:</div>
|
|
</div>
|
|
|
|
<script>
|
|
// 游戏状态
|
|
const game = {
|
|
gridWidth: 10,
|
|
gridHeight: 20,
|
|
board: [],
|
|
currentPiece: null,
|
|
score: 0,
|
|
level: 0,
|
|
lines: 0,
|
|
speed: 1.0,
|
|
isPaused: true,
|
|
isGameOver: false,
|
|
gameLoop: null
|
|
};
|
|
|
|
// 方块类型定义
|
|
const pieceTypes = [
|
|
{ type: 'I', shape: [[1,1,1,1]], color: '#00f5ff' },
|
|
{ type: 'J', shape: [[1,0,0],[1,1,1]], color: '#0000ff' },
|
|
{ type: 'L', shape: [[0,0,1],[1,1,1]], color: '#ff7f00' },
|
|
{ type: 'O', shape: [[1,1],[1,1]], color: '#ffff00' },
|
|
{ type: 'S', shape: [[0,1,1],[1,1,0]], color: '#00ff00' },
|
|
{ type: 'T', shape: [[0,1,0],[1,1,1]], color: '#800080' },
|
|
{ type: 'Z', shape: [[1,1,0],[0,1,1]], color: '#ff0000' }
|
|
];
|
|
|
|
// 日志函数
|
|
function log(message) {
|
|
const logEl = document.getElementById('log');
|
|
const entry = document.createElement('div');
|
|
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
|
logEl.appendChild(entry);
|
|
logEl.scrollTop = logEl.scrollHeight;
|
|
console.log(message);
|
|
}
|
|
|
|
// 显示状态
|
|
function showStatus(title, message, buttonText = '继续游戏') {
|
|
document.getElementById('status-title').textContent = title;
|
|
document.getElementById('status-message').textContent = message;
|
|
document.getElementById('status-button').textContent = buttonText;
|
|
document.getElementById('status').style.display = 'block';
|
|
}
|
|
|
|
function hideStatus() {
|
|
document.getElementById('status').style.display = 'none';
|
|
}
|
|
|
|
// 初始化游戏
|
|
function initGame() {
|
|
log('初始化游戏...');
|
|
|
|
// 清空游戏板
|
|
game.board = Array(game.gridHeight).fill().map(() => Array(game.gridWidth).fill(0));
|
|
|
|
// 重置状态
|
|
game.score = 0;
|
|
game.level = 0;
|
|
game.lines = 0;
|
|
game.speed = 1.0;
|
|
game.isPaused = false;
|
|
game.isGameOver = false;
|
|
|
|
// 生成网格
|
|
const gridEl = document.getElementById('grid');
|
|
gridEl.innerHTML = '';
|
|
for (let y = 0; y < game.gridHeight; y++) {
|
|
for (let x = 0; x < game.gridWidth; x++) {
|
|
const cell = document.createElement('div');
|
|
cell.className = 'grid-cell';
|
|
cell.dataset.x = x;
|
|
cell.dataset.y = y;
|
|
gridEl.appendChild(cell);
|
|
}
|
|
}
|
|
|
|
// 生成新方块
|
|
spawnNewPiece();
|
|
|
|
// 开始游戏循环
|
|
startGameLoop();
|
|
|
|
// 更新UI
|
|
updateUI();
|
|
hideStatus();
|
|
|
|
log('游戏初始化完成');
|
|
}
|
|
|
|
// 生成新方块
|
|
function spawnNewPiece() {
|
|
const randomType = pieceTypes[Math.floor(Math.random() * pieceTypes.length)];
|
|
game.currentPiece = {
|
|
type: randomType.type,
|
|
shape: randomType.shape,
|
|
color: randomType.color,
|
|
x: Math.floor(game.gridWidth / 2) - Math.floor(randomType.shape[0].length / 2),
|
|
y: 0
|
|
};
|
|
log(`生成新方块: ${game.currentPiece.type}`);
|
|
}
|
|
|
|
// 游戏循环
|
|
function startGameLoop() {
|
|
if (game.gameLoop) clearInterval(game.gameLoop);
|
|
|
|
game.gameLoop = setInterval(() => {
|
|
if (!game.isPaused && !game.isGameOver) {
|
|
movePiece(0, 1);
|
|
}
|
|
}, 1000 / game.speed);
|
|
}
|
|
|
|
// 移动方块
|
|
function movePiece(dx, dy) {
|
|
if (!game.currentPiece || game.isPaused || game.isGameOver) return;
|
|
|
|
const newX = game.currentPiece.x + dx;
|
|
const newY = game.currentPiece.y + dy;
|
|
|
|
if (isValidMove(game.currentPiece.shape, newX, newY)) {
|
|
game.currentPiece.x = newX;
|
|
game.currentPiece.y = newY;
|
|
log(`移动方块到: (${newX}, ${newY})`);
|
|
render();
|
|
} else if (dy > 0) {
|
|
// 如果向下移动失败,固定方块
|
|
lockPiece();
|
|
checkLines();
|
|
spawnNewPiece();
|
|
|
|
// 检查游戏是否结束
|
|
if (!isValidMove(game.currentPiece.shape, game.currentPiece.x, game.currentPiece.y)) {
|
|
gameOver();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 旋转方块
|
|
function rotatePiece() {
|
|
if (!game.currentPiece || game.isPaused || game.isGameOver) return;
|
|
|
|
const rotated = rotateMatrix(game.currentPiece.shape);
|
|
if (isValidMove(rotated, game.currentPiece.x, game.currentPiece.y)) {
|
|
game.currentPiece.shape = rotated;
|
|
log('旋转方块');
|
|
render();
|
|
}
|
|
}
|
|
|
|
// 旋转矩阵
|
|
function rotateMatrix(matrix) {
|
|
const result = [];
|
|
for (let i = 0; i < matrix[0].length; i++) {
|
|
result[i] = [];
|
|
for (let j = 0; j < matrix.length; j++) {
|
|
result[i][j] = matrix[matrix.length - 1 - j][i];
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// 检查移动是否有效
|
|
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 >= game.gridWidth || newY >= game.gridHeight) {
|
|
return false;
|
|
}
|
|
|
|
if (newY >= 0 && game.board[newY][newX]) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// 固定方块
|
|
function lockPiece() {
|
|
const { shape, x, y, color } = game.currentPiece;
|
|
|
|
for (let row = 0; row < shape.length; row++) {
|
|
for (let col = 0; col < shape[row].length; col++) {
|
|
if (shape[row][col]) {
|
|
const boardX = x + col;
|
|
const boardY = y + row;
|
|
|
|
if (boardY >= 0) {
|
|
game.board[boardY][boardX] = color;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
log('方块固定到游戏板');
|
|
}
|
|
|
|
// 检查并消除完整的行
|
|
function checkLines() {
|
|
let linesCleared = 0;
|
|
|
|
for (let y = game.gridHeight - 1; y >= 0; y--) {
|
|
if (game.board[y].every(cell => cell !== 0)) {
|
|
// 移除该行
|
|
game.board.splice(y, 1);
|
|
// 在顶部添加新行
|
|
game.board.unshift(Array(game.gridWidth).fill(0));
|
|
linesCleared++;
|
|
y++; // 重新检查同一位置(因为行下移了)
|
|
}
|
|
}
|
|
|
|
if (linesCleared > 0) {
|
|
game.lines += linesCleared;
|
|
game.score += linesCleared * 100 * game.level;
|
|
game.level = Math.floor(game.lines / 10);
|
|
game.speed = 1.0 + game.level * 0.1;
|
|
|
|
log(`消除 ${linesCleared} 行,得分: ${linesCleared * 100 * game.level}`);
|
|
startGameLoop(); // 更新游戏速度
|
|
}
|
|
}
|
|
|
|
// 游戏结束
|
|
function gameOver() {
|
|
game.isGameOver = true;
|
|
clearInterval(game.gameLoop);
|
|
showStatus('游戏结束', `最终得分: ${game.score}`, '重新开始');
|
|
log('游戏结束');
|
|
}
|
|
|
|
// 暂停/继续游戏
|
|
function togglePause() {
|
|
game.isPaused = !game.isPaused;
|
|
|
|
if (game.isPaused) {
|
|
showStatus('游戏暂停', '点击按钮继续游戏', '继续游戏');
|
|
log('游戏暂停');
|
|
} else {
|
|
hideStatus();
|
|
log('游戏继续');
|
|
}
|
|
|
|
updateUI();
|
|
}
|
|
|
|
// 渲染游戏
|
|
function render() {
|
|
// 清空之前的方块
|
|
document.querySelectorAll('.block').forEach(el => el.remove());
|
|
|
|
// 渲染已固定的方块
|
|
for (let y = 0; y < game.gridHeight; y++) {
|
|
for (let x = 0; x < game.gridWidth; x++) {
|
|
if (game.board[y][x]) {
|
|
const block = document.createElement('div');
|
|
block.className = 'block';
|
|
block.style.backgroundColor = game.board[y][x];
|
|
block.style.borderColor = game.board[y][x];
|
|
block.style.left = `${x * 30}px`;
|
|
block.style.top = `${y * 30}px`;
|
|
document.getElementById('game-area').appendChild(block);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 渲染当前方块
|
|
if (game.currentPiece) {
|
|
const { shape, x, y, color } = game.currentPiece;
|
|
|
|
for (let row = 0; row < shape.length; row++) {
|
|
for (let col = 0; col < shape[row].length; col++) {
|
|
if (shape[row][col]) {
|
|
const block = document.createElement('div');
|
|
block.className = `block ${game.currentPiece.type}`;
|
|
block.style.left = `${(x + col) * 30}px`;
|
|
block.style.top = `${(y + row) * 30}px`;
|
|
document.getElementById('game-area').appendChild(block);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 更新UI
|
|
function updateUI() {
|
|
document.getElementById('score').textContent = game.score;
|
|
document.getElementById('level').textContent = game.level;
|
|
document.getElementById('lines').textContent = game.lines;
|
|
document.getElementById('speed').textContent = game.speed.toFixed(1) + 'x';
|
|
|
|
const startBtn = document.getElementById('start-btn');
|
|
if (!game.currentPiece && !game.isGameOver) {
|
|
startBtn.innerHTML = '<span>▶️</span> 开始';
|
|
startBtn.className = 'action-button start';
|
|
} else if (game.isPaused) {
|
|
startBtn.innerHTML = '<span>▶️</span> 继续';
|
|
startBtn.className = 'action-button start';
|
|
} else if (game.isGameOver) {
|
|
startBtn.innerHTML = '<span>🔄</span> 重新开始';
|
|
startBtn.className = 'action-button restart';
|
|
} else {
|
|
startBtn.innerHTML = '<span>⏸️</span> 暂停';
|
|
startBtn.className = 'action-button start';
|
|
}
|
|
}
|
|
|
|
// 设置控件
|
|
function setupControls() {
|
|
log('设置控件事件...');
|
|
|
|
// 开始按钮
|
|
document.getElementById('start-btn').addEventListener('click', () => {
|
|
log('开始按钮点击');
|
|
if (!game.currentPiece && !game.isGameOver) {
|
|
initGame();
|
|
} else if (game.isPaused) {
|
|
togglePause();
|
|
} else if (game.isGameOver) {
|
|
initGame();
|
|
} else {
|
|
togglePause();
|
|
}
|
|
});
|
|
|
|
// 重来按钮
|
|
document.getElementById('restart-btn').addEventListener('click', () => {
|
|
log('重来按钮点击');
|
|
initGame();
|
|
});
|
|
|
|
// 方向按钮
|
|
document.getElementById('left-btn').addEventListener('click', () => {
|
|
log('左按钮点击');
|
|
movePiece(-1, 0);
|
|
});
|
|
|
|
document.getElementById('right-btn').addEventListener('click', () => {
|
|
log('右按钮点击');
|
|
movePiece(1, 0);
|
|
});
|
|
|
|
document.getElementById('down-btn').addEventListener('click', () => {
|
|
log('下按钮点击');
|
|
movePiece(0, 1);
|
|
});
|
|
|
|
document.getElementById('rotate-btn').addEventListener('click', () => {
|
|
log('旋转按钮点击');
|
|
rotatePiece();
|
|
});
|
|
|
|
// 状态按钮
|
|
document.getElementById('status-button').addEventListener('click', () => {
|
|
log('状态按钮点击');
|
|
if (game.isGameOver) {
|
|
initGame();
|
|
} else {
|
|
togglePause();
|
|
}
|
|
});
|
|
|
|
// 键盘控制
|
|
document.addEventListener('keydown', (e) => {
|
|
log(`键盘按键: ${e.key}`);
|
|
|
|
// 如果游戏还没开始,按任意键开始游戏
|
|
if (!game.currentPiece && !game.isGameOver) {
|
|
initGame();
|
|
return;
|
|
}
|
|
|
|
if (game.isPaused || game.isGameOver) return;
|
|
|
|
switch(e.key) {
|
|
case 'ArrowLeft':
|
|
movePiece(-1, 0);
|
|
break;
|
|
case 'ArrowRight':
|
|
movePiece(1, 0);
|
|
break;
|
|
case 'ArrowDown':
|
|
movePiece(0, 1);
|
|
break;
|
|
case 'ArrowUp':
|
|
case ' ':
|
|
rotatePiece();
|
|
break;
|
|
case 'p':
|
|
case 'P':
|
|
togglePause();
|
|
break;
|
|
}
|
|
});
|
|
|
|
log('控件事件设置完成');
|
|
}
|
|
|
|
// 初始化
|
|
window.addEventListener('load', () => {
|
|
log('页面加载完成');
|
|
log(`浏览器: ${navigator.userAgent}`);
|
|
|
|
setupControls();
|
|
updateUI();
|
|
showStatus('俄罗斯方块', '点击开始按钮或按任意键开始游戏', '开始游戏');
|
|
|
|
// 初始渲染
|
|
render();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |