部分修复
This commit is contained in:
279
frontend/smallgame/俄罗斯方块/game-controls.js
Normal file
279
frontend/smallgame/俄罗斯方块/game-controls.js
Normal file
@@ -0,0 +1,279 @@
|
||||
// 游戏控制模块
|
||||
class GameControls {
|
||||
constructor(game) {
|
||||
this.game = game;
|
||||
this.keys = {};
|
||||
this.keyRepeatDelay = 150;
|
||||
this.keyRepeatInterval = 50;
|
||||
this.keyTimers = {};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupKeyboardControls();
|
||||
this.setupTouchControls();
|
||||
this.setupButtonControls();
|
||||
}
|
||||
|
||||
setupKeyboardControls() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
this.handleKeyDown(e);
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
this.handleKeyUp(e);
|
||||
});
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
const key = e.key;
|
||||
|
||||
// 防止重复触发
|
||||
if (this.keys[key]) return;
|
||||
this.keys[key] = true;
|
||||
|
||||
switch(key) {
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
this.game.moveLeft();
|
||||
this.startKeyRepeat('ArrowLeft', () => this.game.moveLeft());
|
||||
break;
|
||||
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
this.game.moveRight();
|
||||
this.startKeyRepeat('ArrowRight', () => this.game.moveRight());
|
||||
break;
|
||||
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
this.game.moveDown();
|
||||
this.startKeyRepeat('ArrowDown', () => this.game.moveDown());
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
this.game.rotatePiece();
|
||||
break;
|
||||
|
||||
case ' ':
|
||||
e.preventDefault();
|
||||
if (this.game.gameRunning) {
|
||||
this.game.pause();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (!this.game.gameRunning) {
|
||||
this.game.start();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'r':
|
||||
case 'R':
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
this.game.restart();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
if (this.game.gameRunning) {
|
||||
this.game.pause();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp(e) {
|
||||
const key = e.key;
|
||||
this.keys[key] = false;
|
||||
this.stopKeyRepeat(key);
|
||||
}
|
||||
|
||||
startKeyRepeat(key, action) {
|
||||
this.stopKeyRepeat(key);
|
||||
|
||||
this.keyTimers[key] = setTimeout(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
if (this.keys[key]) {
|
||||
action();
|
||||
} else {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}, this.keyRepeatInterval);
|
||||
|
||||
this.keyTimers[key] = intervalId;
|
||||
}, this.keyRepeatDelay);
|
||||
}
|
||||
|
||||
stopKeyRepeat(key) {
|
||||
if (this.keyTimers[key]) {
|
||||
clearTimeout(this.keyTimers[key]);
|
||||
clearInterval(this.keyTimers[key]);
|
||||
delete this.keyTimers[key];
|
||||
}
|
||||
}
|
||||
|
||||
setupTouchControls() {
|
||||
// 移动端触摸控制
|
||||
const leftBtn = document.getElementById('leftBtn');
|
||||
const rightBtn = document.getElementById('rightBtn');
|
||||
const downBtn = document.getElementById('downBtn');
|
||||
const rotateBtn = document.getElementById('rotateBtn');
|
||||
const dropBtn = document.getElementById('dropBtn');
|
||||
const pauseBtn = document.getElementById('pauseBtn');
|
||||
|
||||
// 左移
|
||||
this.addTouchEvents(leftBtn, () => this.game.moveLeft());
|
||||
|
||||
// 右移
|
||||
this.addTouchEvents(rightBtn, () => this.game.moveRight());
|
||||
|
||||
// 下移
|
||||
this.addTouchEvents(downBtn, () => this.game.moveDown());
|
||||
|
||||
// 旋转
|
||||
rotateBtn.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
this.game.rotatePiece();
|
||||
});
|
||||
|
||||
rotateBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.game.rotatePiece();
|
||||
});
|
||||
|
||||
// 硬降
|
||||
dropBtn.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
this.game.hardDrop();
|
||||
});
|
||||
|
||||
dropBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.game.hardDrop();
|
||||
});
|
||||
|
||||
// 暂停
|
||||
pauseBtn.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
this.game.pause();
|
||||
});
|
||||
|
||||
pauseBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.game.pause();
|
||||
});
|
||||
}
|
||||
|
||||
addTouchEvents(element, action) {
|
||||
let touchInterval;
|
||||
let touchTimeout;
|
||||
|
||||
const startAction = (e) => {
|
||||
e.preventDefault();
|
||||
action();
|
||||
|
||||
touchTimeout = setTimeout(() => {
|
||||
touchInterval = setInterval(action, this.keyRepeatInterval);
|
||||
}, this.keyRepeatDelay);
|
||||
};
|
||||
|
||||
const stopAction = (e) => {
|
||||
e.preventDefault();
|
||||
if (touchTimeout) {
|
||||
clearTimeout(touchTimeout);
|
||||
touchTimeout = null;
|
||||
}
|
||||
if (touchInterval) {
|
||||
clearInterval(touchInterval);
|
||||
touchInterval = null;
|
||||
}
|
||||
};
|
||||
|
||||
element.addEventListener('touchstart', startAction);
|
||||
element.addEventListener('touchend', stopAction);
|
||||
element.addEventListener('touchcancel', stopAction);
|
||||
element.addEventListener('mousedown', startAction);
|
||||
element.addEventListener('mouseup', stopAction);
|
||||
element.addEventListener('mouseleave', stopAction);
|
||||
}
|
||||
|
||||
setupButtonControls() {
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const restartBtn = document.getElementById('restartBtn');
|
||||
|
||||
startBtn.addEventListener('click', () => {
|
||||
this.game.start();
|
||||
});
|
||||
|
||||
restartBtn.addEventListener('click', () => {
|
||||
this.game.restart();
|
||||
});
|
||||
}
|
||||
|
||||
// 游戏手势支持
|
||||
setupSwipeControls() {
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let threshold = 50;
|
||||
|
||||
this.game.canvas.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
startX = touch.clientX;
|
||||
startY = touch.clientY;
|
||||
});
|
||||
|
||||
this.game.canvas.addEventListener('touchmove', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
this.game.canvas.addEventListener('touchend', (e) => {
|
||||
e.preventDefault();
|
||||
const touch = e.changedTouches[0];
|
||||
const deltaX = touch.clientX - startX;
|
||||
const deltaY = touch.clientY - startY;
|
||||
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||
// 水平滑动
|
||||
if (Math.abs(deltaX) > threshold) {
|
||||
if (deltaX > 0) {
|
||||
this.game.moveRight();
|
||||
} else {
|
||||
this.game.moveLeft();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 垂直滑动
|
||||
if (Math.abs(deltaY) > threshold) {
|
||||
if (deltaY > 0) {
|
||||
this.game.moveDown();
|
||||
} else {
|
||||
this.game.rotatePiece();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 双击旋转
|
||||
let lastTap = 0;
|
||||
this.game.canvas.addEventListener('touchend', (e) => {
|
||||
const currentTime = new Date().getTime();
|
||||
const tapLength = currentTime - lastTap;
|
||||
|
||||
if (tapLength < 500 && tapLength > 0) {
|
||||
e.preventDefault();
|
||||
this.game.rotatePiece();
|
||||
}
|
||||
lastTap = currentTime;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化控制系统
|
||||
const gameControls = new GameControls(game);
|
||||
338
frontend/smallgame/俄罗斯方块/game-stats.js
Normal file
338
frontend/smallgame/俄罗斯方块/game-stats.js
Normal file
@@ -0,0 +1,338 @@
|
||||
// 游戏统计和成就系统
|
||||
class GameStats {
|
||||
constructor() {
|
||||
this.achievements = [
|
||||
{
|
||||
id: 'first_game',
|
||||
name: '初次体验',
|
||||
description: '完成第一次游戏',
|
||||
condition: (stats) => true
|
||||
},
|
||||
{
|
||||
id: 'score_1000',
|
||||
name: '小试牛刀',
|
||||
description: '单局得分达到1000分',
|
||||
condition: (stats) => stats.score >= 1000
|
||||
},
|
||||
{
|
||||
id: 'score_5000',
|
||||
name: '游戏达人',
|
||||
description: '单局得分达到5000分',
|
||||
condition: (stats) => stats.score >= 5000
|
||||
},
|
||||
{
|
||||
id: 'score_10000',
|
||||
name: '方块大师',
|
||||
description: '单局得分达到10000分',
|
||||
condition: (stats) => stats.score >= 10000
|
||||
},
|
||||
{
|
||||
id: 'level_5',
|
||||
name: '步步高升',
|
||||
description: '达到第5级',
|
||||
condition: (stats) => stats.level >= 5
|
||||
},
|
||||
{
|
||||
id: 'level_10',
|
||||
name: '速度之王',
|
||||
description: '达到第10级',
|
||||
condition: (stats) => stats.level >= 10
|
||||
},
|
||||
{
|
||||
id: 'lines_50',
|
||||
name: '消除专家',
|
||||
description: '累计消除50行',
|
||||
condition: (stats) => stats.lines >= 50
|
||||
},
|
||||
{
|
||||
id: 'lines_100',
|
||||
name: '清理大师',
|
||||
description: '累计消除100行',
|
||||
condition: (stats) => stats.lines >= 100
|
||||
},
|
||||
{
|
||||
id: 'tetris',
|
||||
name: 'Tetris!',
|
||||
description: '一次消除4行',
|
||||
condition: (stats) => stats.maxCombo >= 4
|
||||
},
|
||||
{
|
||||
id: 'time_10min',
|
||||
name: '持久战士',
|
||||
description: '单局游戏时间超过10分钟',
|
||||
condition: (stats) => stats.playTime >= 600000
|
||||
},
|
||||
{
|
||||
id: 'efficiency',
|
||||
name: '效率专家',
|
||||
description: '平均每分钟得分超过500',
|
||||
condition: (stats) => stats.avgScore >= 500
|
||||
}
|
||||
];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const playAgainBtn = document.getElementById('playAgainBtn');
|
||||
playAgainBtn.addEventListener('click', () => {
|
||||
this.hideStats();
|
||||
game.restart();
|
||||
});
|
||||
}
|
||||
|
||||
showStats(gameData) {
|
||||
const playTimeMinutes = gameData.playTime / 60000;
|
||||
const avgScore = playTimeMinutes > 0 ? Math.round(gameData.score / playTimeMinutes) : 0;
|
||||
|
||||
const stats = {
|
||||
...gameData,
|
||||
avgScore: avgScore
|
||||
};
|
||||
|
||||
// 更新统计显示
|
||||
document.getElementById('finalScore').textContent = stats.score.toLocaleString();
|
||||
document.getElementById('finalLevel').textContent = stats.level;
|
||||
document.getElementById('finalLines').textContent = stats.lines;
|
||||
document.getElementById('playTime').textContent = this.formatTime(stats.playTime);
|
||||
document.getElementById('maxCombo').textContent = stats.maxCombo;
|
||||
document.getElementById('avgScore').textContent = stats.avgScore;
|
||||
|
||||
// 检查成就
|
||||
const achievement = this.checkAchievements(stats);
|
||||
this.displayAchievement(achievement);
|
||||
|
||||
// 显示统计界面
|
||||
document.getElementById('gameStats').style.display = 'flex';
|
||||
document.getElementById('gameStats').classList.add('fade-in');
|
||||
}
|
||||
|
||||
hideStats() {
|
||||
document.getElementById('gameStats').style.display = 'none';
|
||||
document.getElementById('gameStats').classList.remove('fade-in');
|
||||
}
|
||||
|
||||
formatTime(milliseconds) {
|
||||
const seconds = Math.floor(milliseconds / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
|
||||
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
checkAchievements(stats) {
|
||||
// 获取已获得的成就
|
||||
const earnedAchievements = this.getEarnedAchievements();
|
||||
|
||||
// 检查新成就
|
||||
for (let achievement of this.achievements) {
|
||||
if (!earnedAchievements.includes(achievement.id) &&
|
||||
achievement.condition(stats)) {
|
||||
|
||||
// 保存新成就
|
||||
this.saveAchievement(achievement.id);
|
||||
return achievement;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
displayAchievement(achievement) {
|
||||
const achievementEl = document.getElementById('achievement');
|
||||
|
||||
if (achievement) {
|
||||
achievementEl.innerHTML = `
|
||||
🏆 <strong>成就解锁!</strong><br>
|
||||
<strong>${achievement.name}</strong><br>
|
||||
${achievement.description}
|
||||
`;
|
||||
achievementEl.classList.add('pulse');
|
||||
} else {
|
||||
// 显示随机鼓励话语
|
||||
const encouragements = [
|
||||
'继续努力,你会变得更强!',
|
||||
'每一次游戏都是进步的机会!',
|
||||
'方块世界需要你的智慧!',
|
||||
'熟能生巧,加油!',
|
||||
'下一局一定会更好!',
|
||||
'坚持就是胜利!',
|
||||
'你的反应速度在提升!',
|
||||
'策略思维正在增强!'
|
||||
];
|
||||
|
||||
const randomEncouragement = encouragements[Math.floor(Math.random() * encouragements.length)];
|
||||
achievementEl.innerHTML = `💪 ${randomEncouragement}`;
|
||||
achievementEl.classList.remove('pulse');
|
||||
}
|
||||
}
|
||||
|
||||
getEarnedAchievements() {
|
||||
const saved = localStorage.getItem('tetris_achievements');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
}
|
||||
|
||||
saveAchievement(achievementId) {
|
||||
const earned = this.getEarnedAchievements();
|
||||
if (!earned.includes(achievementId)) {
|
||||
earned.push(achievementId);
|
||||
localStorage.setItem('tetris_achievements', JSON.stringify(earned));
|
||||
}
|
||||
}
|
||||
|
||||
// 获取历史最佳记录
|
||||
getBestStats() {
|
||||
const saved = localStorage.getItem('tetris_best_stats');
|
||||
return saved ? JSON.parse(saved) : {
|
||||
score: 0,
|
||||
level: 0,
|
||||
lines: 0,
|
||||
maxCombo: 0
|
||||
};
|
||||
}
|
||||
|
||||
// 保存最佳记录
|
||||
saveBestStats(stats) {
|
||||
const best = this.getBestStats();
|
||||
let updated = false;
|
||||
|
||||
if (stats.score > best.score) {
|
||||
best.score = stats.score;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (stats.level > best.level) {
|
||||
best.level = stats.level;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (stats.lines > best.lines) {
|
||||
best.lines = stats.lines;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (stats.maxCombo > best.maxCombo) {
|
||||
best.maxCombo = stats.maxCombo;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
localStorage.setItem('tetris_best_stats', JSON.stringify(best));
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
// 显示排行榜
|
||||
showLeaderboard() {
|
||||
const best = this.getBestStats();
|
||||
const earned = this.getEarnedAchievements();
|
||||
|
||||
console.log('最佳记录:', best);
|
||||
console.log('已获得成就:', earned.length + '/' + this.achievements.length);
|
||||
}
|
||||
}
|
||||
|
||||
// 高级特效系统
|
||||
class GameEffects {
|
||||
constructor(game) {
|
||||
this.game = game;
|
||||
this.particles = [];
|
||||
this.effects = [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 创建特效canvas
|
||||
this.effectsCanvas = document.createElement('canvas');
|
||||
this.effectsCanvas.width = this.game.canvas.width;
|
||||
this.effectsCanvas.height = this.game.canvas.height;
|
||||
this.effectsCanvas.style.position = 'absolute';
|
||||
this.effectsCanvas.style.top = '0';
|
||||
this.effectsCanvas.style.left = '0';
|
||||
this.effectsCanvas.style.pointerEvents = 'none';
|
||||
this.effectsCanvas.style.zIndex = '10';
|
||||
|
||||
this.effectsCtx = this.effectsCanvas.getContext('2d');
|
||||
|
||||
// 将特效canvas添加到游戏板容器中
|
||||
this.game.canvas.parentElement.style.position = 'relative';
|
||||
this.game.canvas.parentElement.appendChild(this.effectsCanvas);
|
||||
}
|
||||
|
||||
// 行消除特效
|
||||
lineCleared(row) {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
this.particles.push({
|
||||
x: Math.random() * this.game.canvas.width,
|
||||
y: row * this.game.CELL_SIZE + this.game.CELL_SIZE / 2,
|
||||
vx: (Math.random() - 0.5) * 10,
|
||||
vy: (Math.random() - 0.5) * 10,
|
||||
life: 1,
|
||||
decay: 0.02,
|
||||
color: `hsl(${Math.random() * 360}, 100%, 50%)`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 方块锁定特效
|
||||
pieceLocked(piece) {
|
||||
const centerX = (piece.x + piece.matrix[0].length / 2) * this.game.CELL_SIZE;
|
||||
const centerY = (piece.y + piece.matrix.length / 2) * this.game.CELL_SIZE;
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
this.particles.push({
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
vx: (Math.random() - 0.5) * 8,
|
||||
vy: (Math.random() - 0.5) * 8,
|
||||
life: 0.8,
|
||||
decay: 0.03,
|
||||
color: piece.color
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新特效
|
||||
update() {
|
||||
// 更新粒子
|
||||
for (let i = this.particles.length - 1; i >= 0; i--) {
|
||||
const particle = this.particles[i];
|
||||
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
particle.life -= particle.decay;
|
||||
|
||||
if (particle.life <= 0) {
|
||||
this.particles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制特效
|
||||
draw() {
|
||||
this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height);
|
||||
|
||||
// 绘制粒子
|
||||
for (let particle of this.particles) {
|
||||
this.effectsCtx.save();
|
||||
this.effectsCtx.globalAlpha = particle.life;
|
||||
this.effectsCtx.fillStyle = particle.color;
|
||||
this.effectsCtx.beginPath();
|
||||
this.effectsCtx.arc(particle.x, particle.y, 3, 0, Math.PI * 2);
|
||||
this.effectsCtx.fill();
|
||||
this.effectsCtx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建统计系统实例
|
||||
const gameStats = new GameStats();
|
||||
|
||||
// 在适当的地方创建特效系统
|
||||
// const gameEffects = new GameEffects(game);
|
||||
124
frontend/smallgame/俄罗斯方块/index.html
Normal file
124
frontend/smallgame/俄罗斯方块/index.html
Normal file
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>俄罗斯方块</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<div class="game-header">
|
||||
<h1>俄罗斯方块</h1>
|
||||
<div class="score-board">
|
||||
<div class="score-item">
|
||||
<span class="label">分数:</span>
|
||||
<span id="score">0</span>
|
||||
</div>
|
||||
<div class="score-item">
|
||||
<span class="label">等级:</span>
|
||||
<span id="level">1</span>
|
||||
</div>
|
||||
<div class="score-item">
|
||||
<span class="label">消除行数:</span>
|
||||
<span id="lines">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-main">
|
||||
<div class="game-board">
|
||||
<canvas id="gameCanvas" width="300" height="600"></canvas>
|
||||
<div class="game-overlay" id="gameOverlay">
|
||||
<div class="overlay-content">
|
||||
<h2 id="overlayTitle">游戏暂停</h2>
|
||||
<p id="overlayMessage">按空格键继续游戏</p>
|
||||
<button id="startBtn" class="game-btn">开始游戏</button>
|
||||
<button id="restartBtn" class="game-btn">重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-sidebar">
|
||||
<div class="next-piece">
|
||||
<h3>下一个</h3>
|
||||
<canvas id="nextCanvas" width="120" height="120"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="controls-info">
|
||||
<h3>操作说明</h3>
|
||||
<div class="control-item">
|
||||
<span class="key">←→</span>
|
||||
<span class="desc">移动</span>
|
||||
</div>
|
||||
<div class="control-item">
|
||||
<span class="key">↓</span>
|
||||
<span class="desc">快速下降</span>
|
||||
</div>
|
||||
<div class="control-item">
|
||||
<span class="key">↑</span>
|
||||
<span class="desc">旋转</span>
|
||||
</div>
|
||||
<div class="control-item">
|
||||
<span class="key">空格</span>
|
||||
<span class="desc">暂停/继续</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手机端触摸控制 -->
|
||||
<div class="mobile-controls">
|
||||
<div class="mobile-controls-left">
|
||||
<button class="control-btn" id="rotateBtn">↻</button>
|
||||
<button class="control-btn" id="leftBtn">←</button>
|
||||
<button class="control-btn" id="downBtn">↓</button>
|
||||
</div>
|
||||
<div class="mobile-controls-right">
|
||||
<button class="control-btn" id="dropBtn">⇩</button>
|
||||
<button class="control-btn" id="rightBtn">→</button>
|
||||
<button class="control-btn" id="pauseBtn">⏸</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 游戏结束统计界面 -->
|
||||
<div class="game-stats" id="gameStats">
|
||||
<div class="stats-content">
|
||||
<h2>游戏结束</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">最终分数</span>
|
||||
<span class="stat-value" id="finalScore">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">达到等级</span>
|
||||
<span class="stat-value" id="finalLevel">1</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">消除行数</span>
|
||||
<span class="stat-value" id="finalLines">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">游戏时长</span>
|
||||
<span class="stat-value" id="playTime">00:00</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">单次消除最大行数</span>
|
||||
<span class="stat-value" id="maxCombo">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">平均每分钟分数</span>
|
||||
<span class="stat-value" id="avgScore">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="achievement" id="achievement"></div>
|
||||
<button class="game-btn" id="playAgainBtn">再玩一次</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="tetris.js"></script>
|
||||
<script src="game-controls.js"></script>
|
||||
<script src="game-stats.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
459
frontend/smallgame/俄罗斯方块/styles.css
Normal file
459
frontend/smallgame/俄罗斯方块/styles.css
Normal file
@@ -0,0 +1,459 @@
|
||||
/* 基础样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
background: linear-gradient(135deg, #1e3c72, #2a5298);
|
||||
color: white;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* 游戏容器 */
|
||||
.game-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* 游戏头部 */
|
||||
.game-header {
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.game-header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.score-board {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.score-item {
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 8px 15px;
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.score-item .label {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.score-item span:last-child {
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
/* 游戏主体 */
|
||||
.game-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.game-board {
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
display: block;
|
||||
background: #1a1a1a;
|
||||
border: 2px solid #333;
|
||||
}
|
||||
|
||||
/* 游戏覆盖层 */
|
||||
.game-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.overlay-content {
|
||||
text-align: center;
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.overlay-content h2 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.overlay-content p {
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 游戏按钮 */
|
||||
.game-btn {
|
||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
margin: 5px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.game-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.game-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
.game-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.next-piece {
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.next-piece h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
#nextCanvas {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.controls-info {
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.controls-info h3 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.control-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.control-item .key {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.control-item .desc {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 手机端控制 */
|
||||
.mobile-controls {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.mobile-controls-left {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.mobile-controls-right {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
font-size: 1.4rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||
user-select: none;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 2px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.control-btn:active {
|
||||
transform: scale(0.9);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
/* 游戏统计界面 */
|
||||
.game-stats {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.9);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
padding: 30px;
|
||||
border-radius: 20px;
|
||||
text-align: center;
|
||||
max-width: 90%;
|
||||
width: 400px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.stats-content h2 {
|
||||
margin-bottom: 25px;
|
||||
font-size: 2rem;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.achievement {
|
||||
background: linear-gradient(45deg, #f093fb, #f5576c);
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 25px;
|
||||
font-weight: bold;
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.game-container {
|
||||
padding: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.game-header h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.score-board {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.score-item {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.game-main {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.game-board {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.game-sidebar {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
min-width: unset;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.next-piece,
|
||||
.controls-info {
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.mobile-controls {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
width: 250px;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
padding: 20px;
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.game-header h1 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.score-board {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.score-item {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
width: 200px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.mobile-controls-left,
|
||||
.mobile-controls-right {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mobile-controls-left {
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.mobile-controls-right {
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.game-sidebar {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.next-piece,
|
||||
.controls-info {
|
||||
max-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
/* 隐藏类 */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
521
frontend/smallgame/俄罗斯方块/tetris.js
Normal file
521
frontend/smallgame/俄罗斯方块/tetris.js
Normal file
@@ -0,0 +1,521 @@
|
||||
// 俄罗斯方块主游戏逻辑
|
||||
class TetrisGame {
|
||||
constructor() {
|
||||
this.canvas = document.getElementById('gameCanvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.nextCanvas = document.getElementById('nextCanvas');
|
||||
this.nextCtx = this.nextCanvas.getContext('2d');
|
||||
|
||||
// 游戏配置
|
||||
this.BOARD_WIDTH = 10;
|
||||
this.BOARD_HEIGHT = 20;
|
||||
this.CELL_SIZE = 30;
|
||||
|
||||
// 游戏状态
|
||||
this.board = [];
|
||||
this.currentPiece = null;
|
||||
this.nextPiece = null;
|
||||
this.score = 0;
|
||||
this.level = 1;
|
||||
this.lines = 0;
|
||||
this.dropTime = 0;
|
||||
this.dropInterval = 1000; // 初始下降间隔(毫秒)
|
||||
this.gameRunning = false;
|
||||
this.gamePaused = false;
|
||||
this.lastTime = 0;
|
||||
|
||||
// 统计数据
|
||||
this.gameStartTime = 0;
|
||||
this.maxCombo = 0;
|
||||
this.currentCombo = 0;
|
||||
|
||||
// 方块类型定义
|
||||
this.pieces = {
|
||||
I: {
|
||||
color: '#00f5ff',
|
||||
matrix: [
|
||||
[0, 0, 0, 0],
|
||||
[1, 1, 1, 1],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0]
|
||||
]
|
||||
},
|
||||
O: {
|
||||
color: '#ffff00',
|
||||
matrix: [
|
||||
[1, 1],
|
||||
[1, 1]
|
||||
]
|
||||
},
|
||||
T: {
|
||||
color: '#800080',
|
||||
matrix: [
|
||||
[0, 1, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0]
|
||||
]
|
||||
},
|
||||
S: {
|
||||
color: '#00ff00',
|
||||
matrix: [
|
||||
[0, 1, 1],
|
||||
[1, 1, 0],
|
||||
[0, 0, 0]
|
||||
]
|
||||
},
|
||||
Z: {
|
||||
color: '#ff0000',
|
||||
matrix: [
|
||||
[1, 1, 0],
|
||||
[0, 1, 1],
|
||||
[0, 0, 0]
|
||||
]
|
||||
},
|
||||
J: {
|
||||
color: '#0000ff',
|
||||
matrix: [
|
||||
[1, 0, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0]
|
||||
]
|
||||
},
|
||||
L: {
|
||||
color: '#ffa500',
|
||||
matrix: [
|
||||
[0, 0, 1],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0]
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
this.pieceTypes = Object.keys(this.pieces);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initBoard();
|
||||
this.nextPiece = this.createPiece();
|
||||
this.spawnPiece();
|
||||
this.updateDisplay();
|
||||
this.showOverlay('游戏准备', '点击开始游戏按钮开始');
|
||||
}
|
||||
|
||||
initBoard() {
|
||||
this.board = [];
|
||||
for (let row = 0; row < this.BOARD_HEIGHT; row++) {
|
||||
this.board[row] = [];
|
||||
for (let col = 0; col < this.BOARD_WIDTH; col++) {
|
||||
this.board[row][col] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createPiece() {
|
||||
const type = this.pieceTypes[Math.floor(Math.random() * this.pieceTypes.length)];
|
||||
const piece = this.pieces[type];
|
||||
return {
|
||||
type: type,
|
||||
color: piece.color,
|
||||
matrix: this.copyMatrix(piece.matrix),
|
||||
x: Math.floor(this.BOARD_WIDTH / 2) - Math.floor(piece.matrix[0].length / 2),
|
||||
y: 0
|
||||
};
|
||||
}
|
||||
|
||||
copyMatrix(matrix) {
|
||||
return matrix.map(row => [...row]);
|
||||
}
|
||||
|
||||
spawnPiece() {
|
||||
this.currentPiece = this.nextPiece;
|
||||
this.nextPiece = this.createPiece();
|
||||
this.currentPiece.x = Math.floor(this.BOARD_WIDTH / 2) - Math.floor(this.currentPiece.matrix[0].length / 2);
|
||||
this.currentPiece.y = 0;
|
||||
|
||||
// 检查游戏结束
|
||||
if (this.collision(this.currentPiece)) {
|
||||
this.gameOver();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.drawNextPiece();
|
||||
return true;
|
||||
}
|
||||
|
||||
collision(piece, dx = 0, dy = 0) {
|
||||
const matrix = piece.matrix;
|
||||
const x = piece.x + dx;
|
||||
const y = piece.y + dy;
|
||||
|
||||
for (let row = 0; row < matrix.length; row++) {
|
||||
for (let col = 0; col < matrix[row].length; col++) {
|
||||
if (matrix[row][col] !== 0) {
|
||||
const newX = x + col;
|
||||
const newY = y + row;
|
||||
|
||||
if (newX < 0 || newX >= this.BOARD_WIDTH ||
|
||||
newY >= this.BOARD_HEIGHT ||
|
||||
(newY >= 0 && this.board[newY][newX] !== 0)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
rotate(piece) {
|
||||
const matrix = piece.matrix;
|
||||
const N = matrix.length;
|
||||
const rotated = [];
|
||||
|
||||
// 创建旋转后的矩阵
|
||||
for (let i = 0; i < N; i++) {
|
||||
rotated[i] = [];
|
||||
for (let j = 0; j < N; j++) {
|
||||
rotated[i][j] = matrix[N - 1 - j][i];
|
||||
}
|
||||
}
|
||||
|
||||
return rotated;
|
||||
}
|
||||
|
||||
hardDrop() {
|
||||
while (!this.collision(this.currentPiece, 0, 1)) {
|
||||
this.currentPiece.y++;
|
||||
this.score += 2; // 硬降给额外分数
|
||||
}
|
||||
this.lockPiece();
|
||||
}
|
||||
|
||||
lockPiece() {
|
||||
const matrix = this.currentPiece.matrix;
|
||||
const x = this.currentPiece.x;
|
||||
const y = this.currentPiece.y;
|
||||
|
||||
// 将方块锁定到游戏板上
|
||||
for (let row = 0; row < matrix.length; row++) {
|
||||
for (let col = 0; col < matrix[row].length; col++) {
|
||||
if (matrix[row][col] !== 0) {
|
||||
if (y + row >= 0) {
|
||||
this.board[y + row][x + col] = this.currentPiece.color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并清除完整的行
|
||||
const linesCleared = this.clearLines();
|
||||
if (linesCleared > 0) {
|
||||
this.lines += linesCleared;
|
||||
this.currentCombo = linesCleared;
|
||||
this.maxCombo = Math.max(this.maxCombo, this.currentCombo);
|
||||
|
||||
// 计算分数 (基于消除行数和等级)
|
||||
const lineScores = [0, 40, 100, 300, 1200];
|
||||
this.score += lineScores[linesCleared] * this.level;
|
||||
|
||||
// 升级逻辑
|
||||
this.level = Math.floor(this.lines / 10) + 1;
|
||||
this.dropInterval = Math.max(50, 1000 - (this.level - 1) * 50);
|
||||
} else {
|
||||
this.currentCombo = 0;
|
||||
}
|
||||
|
||||
// 生成下一个方块
|
||||
this.spawnPiece();
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
clearLines() {
|
||||
let linesCleared = 0;
|
||||
|
||||
for (let row = this.BOARD_HEIGHT - 1; row >= 0; row--) {
|
||||
if (this.board[row].every(cell => cell !== 0)) {
|
||||
// 移除完整的行
|
||||
this.board.splice(row, 1);
|
||||
// 在顶部添加新的空行
|
||||
this.board.unshift(new Array(this.BOARD_WIDTH).fill(0));
|
||||
linesCleared++;
|
||||
row++; // 重新检查当前行
|
||||
}
|
||||
}
|
||||
|
||||
return linesCleared;
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
if (!this.gameRunning || this.gamePaused) return;
|
||||
|
||||
this.dropTime += deltaTime;
|
||||
|
||||
if (this.dropTime >= this.dropInterval) {
|
||||
if (!this.collision(this.currentPiece, 0, 1)) {
|
||||
this.currentPiece.y++;
|
||||
// 自然下降不加分
|
||||
} else {
|
||||
this.lockPiece();
|
||||
}
|
||||
this.dropTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// 绘制游戏板
|
||||
this.drawBoard();
|
||||
|
||||
// 绘制当前方块
|
||||
if (this.currentPiece) {
|
||||
this.drawPiece(this.currentPiece, this.ctx);
|
||||
}
|
||||
|
||||
// 绘制网格
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
drawBoard() {
|
||||
for (let row = 0; row < this.BOARD_HEIGHT; row++) {
|
||||
for (let col = 0; col < this.BOARD_WIDTH; col++) {
|
||||
if (this.board[row][col] !== 0) {
|
||||
this.ctx.fillStyle = this.board[row][col];
|
||||
this.ctx.fillRect(
|
||||
col * this.CELL_SIZE,
|
||||
row * this.CELL_SIZE,
|
||||
this.CELL_SIZE,
|
||||
this.CELL_SIZE
|
||||
);
|
||||
this.ctx.strokeStyle = '#333';
|
||||
this.ctx.lineWidth = 1;
|
||||
this.ctx.strokeRect(
|
||||
col * this.CELL_SIZE,
|
||||
row * this.CELL_SIZE,
|
||||
this.CELL_SIZE,
|
||||
this.CELL_SIZE
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawPiece(piece, context) {
|
||||
context.fillStyle = piece.color;
|
||||
const matrix = piece.matrix;
|
||||
|
||||
for (let row = 0; row < matrix.length; row++) {
|
||||
for (let col = 0; col < matrix[row].length; col++) {
|
||||
if (matrix[row][col] !== 0) {
|
||||
context.fillRect(
|
||||
(piece.x + col) * this.CELL_SIZE,
|
||||
(piece.y + row) * this.CELL_SIZE,
|
||||
this.CELL_SIZE,
|
||||
this.CELL_SIZE
|
||||
);
|
||||
context.strokeStyle = '#333';
|
||||
context.lineWidth = 1;
|
||||
context.strokeRect(
|
||||
(piece.x + col) * this.CELL_SIZE,
|
||||
(piece.y + row) * this.CELL_SIZE,
|
||||
this.CELL_SIZE,
|
||||
this.CELL_SIZE
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawGrid() {
|
||||
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
|
||||
this.ctx.lineWidth = 1;
|
||||
|
||||
// 垂直线
|
||||
for (let col = 0; col <= this.BOARD_WIDTH; col++) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(col * this.CELL_SIZE, 0);
|
||||
this.ctx.lineTo(col * this.CELL_SIZE, this.canvas.height);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
// 水平线
|
||||
for (let row = 0; row <= this.BOARD_HEIGHT; row++) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(0, row * this.CELL_SIZE);
|
||||
this.ctx.lineTo(this.canvas.width, row * this.CELL_SIZE);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
drawNextPiece() {
|
||||
this.nextCtx.clearRect(0, 0, this.nextCanvas.width, this.nextCanvas.height);
|
||||
|
||||
if (this.nextPiece) {
|
||||
const size = 20;
|
||||
const matrix = this.nextPiece.matrix;
|
||||
const offsetX = (this.nextCanvas.width - matrix[0].length * size) / 2;
|
||||
const offsetY = (this.nextCanvas.height - matrix.length * size) / 2;
|
||||
|
||||
this.nextCtx.fillStyle = this.nextPiece.color;
|
||||
|
||||
for (let row = 0; row < matrix.length; row++) {
|
||||
for (let col = 0; col < matrix[row].length; col++) {
|
||||
if (matrix[row][col] !== 0) {
|
||||
this.nextCtx.fillRect(
|
||||
offsetX + col * size,
|
||||
offsetY + row * size,
|
||||
size,
|
||||
size
|
||||
);
|
||||
this.nextCtx.strokeStyle = '#333';
|
||||
this.nextCtx.lineWidth = 1;
|
||||
this.nextCtx.strokeRect(
|
||||
offsetX + col * size,
|
||||
offsetY + row * size,
|
||||
size,
|
||||
size
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateDisplay() {
|
||||
document.getElementById('score').textContent = this.score;
|
||||
document.getElementById('level').textContent = this.level;
|
||||
document.getElementById('lines').textContent = this.lines;
|
||||
}
|
||||
|
||||
showOverlay(title, message) {
|
||||
document.getElementById('overlayTitle').textContent = title;
|
||||
document.getElementById('overlayMessage').textContent = message;
|
||||
document.getElementById('gameOverlay').style.display = 'flex';
|
||||
}
|
||||
|
||||
hideOverlay() {
|
||||
document.getElementById('gameOverlay').style.display = 'none';
|
||||
}
|
||||
|
||||
start() {
|
||||
this.gameRunning = true;
|
||||
this.gamePaused = false;
|
||||
this.gameStartTime = Date.now();
|
||||
this.hideOverlay();
|
||||
this.gameLoop();
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (!this.gameRunning) return;
|
||||
|
||||
this.gamePaused = !this.gamePaused;
|
||||
|
||||
if (this.gamePaused) {
|
||||
this.showOverlay('游戏暂停', '按空格键继续游戏');
|
||||
} else {
|
||||
this.hideOverlay();
|
||||
this.gameLoop();
|
||||
}
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.gameRunning = false;
|
||||
this.gamePaused = false;
|
||||
this.score = 0;
|
||||
this.level = 1;
|
||||
this.lines = 0;
|
||||
this.dropTime = 0;
|
||||
this.dropInterval = 1000;
|
||||
this.maxCombo = 0;
|
||||
this.currentCombo = 0;
|
||||
|
||||
this.init();
|
||||
this.start();
|
||||
}
|
||||
|
||||
gameOver() {
|
||||
this.gameRunning = false;
|
||||
this.gamePaused = false;
|
||||
|
||||
// 显示游戏统计
|
||||
gameStats.showStats({
|
||||
score: this.score,
|
||||
level: this.level,
|
||||
lines: this.lines,
|
||||
playTime: Date.now() - this.gameStartTime,
|
||||
maxCombo: this.maxCombo
|
||||
});
|
||||
}
|
||||
|
||||
gameLoop(currentTime = 0) {
|
||||
if (!this.gameRunning || this.gamePaused) return;
|
||||
|
||||
const deltaTime = currentTime - this.lastTime;
|
||||
this.lastTime = currentTime;
|
||||
|
||||
this.update(deltaTime);
|
||||
this.draw();
|
||||
|
||||
requestAnimationFrame((time) => this.gameLoop(time));
|
||||
}
|
||||
|
||||
// 移动方法
|
||||
moveLeft() {
|
||||
if (this.gameRunning && !this.gamePaused && this.currentPiece) {
|
||||
if (!this.collision(this.currentPiece, -1, 0)) {
|
||||
this.currentPiece.x--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveRight() {
|
||||
if (this.gameRunning && !this.gamePaused && this.currentPiece) {
|
||||
if (!this.collision(this.currentPiece, 1, 0)) {
|
||||
this.currentPiece.x++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveDown() {
|
||||
if (this.gameRunning && !this.gamePaused && this.currentPiece) {
|
||||
if (!this.collision(this.currentPiece, 0, 1)) {
|
||||
this.currentPiece.y++;
|
||||
this.score += 1; // 只有主动按下键才给软降分数
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rotatePiece() {
|
||||
if (this.gameRunning && !this.gamePaused && this.currentPiece) {
|
||||
const rotated = this.rotate(this.currentPiece);
|
||||
const originalMatrix = this.currentPiece.matrix;
|
||||
this.currentPiece.matrix = rotated;
|
||||
|
||||
// 检查旋转后是否会碰撞
|
||||
if (this.collision(this.currentPiece)) {
|
||||
// 尝试wall kick
|
||||
const kicks = [
|
||||
[-1, 0], [1, 0], [0, -1], [-2, 0], [2, 0]
|
||||
];
|
||||
|
||||
let canRotate = false;
|
||||
for (let kick of kicks) {
|
||||
if (!this.collision(this.currentPiece, kick[0], kick[1])) {
|
||||
this.currentPiece.x += kick[0];
|
||||
this.currentPiece.y += kick[1];
|
||||
canRotate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!canRotate) {
|
||||
this.currentPiece.matrix = originalMatrix;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建游戏实例
|
||||
const game = new TetrisGame();
|
||||
Reference in New Issue
Block a user