feat: 添加贪吃蛇和记忆配对纸牌游戏
- 贪吃蛇游戏: Canvas渲染、方向控制、碰撞检测、计分系统 - 记忆配对纸牌: 3D翻转动画、配对消除、计时器、胜利判定
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
class SnakeGame {
|
||||
constructor(canvasId, scoreId) {
|
||||
this.canvas = document.getElementById(canvasId);
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.scoreElement = document.getElementById(scoreId);
|
||||
|
||||
this.gridSize = 20;
|
||||
this.tileCount = this.canvas.width / this.gridSize;
|
||||
|
||||
this.reset();
|
||||
|
||||
window.addEventListener('keydown', this.handleKeyDown.bind(this));
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.snake = [
|
||||
{ x: 10, y: 10 },
|
||||
{ x: 10, y: 11 },
|
||||
{ x: 10, y: 12 }
|
||||
];
|
||||
this.food = this.getRandomFood();
|
||||
this.dx = 0;
|
||||
this.dy = -1;
|
||||
this.score = 0;
|
||||
this.updateScore();
|
||||
this.gameOver = false;
|
||||
}
|
||||
|
||||
getRandomFood() {
|
||||
let newFood;
|
||||
while (true) {
|
||||
newFood = {
|
||||
x: Math.floor(Math.random() * this.tileCount),
|
||||
y: Math.floor(Math.random() * this.tileCount)
|
||||
};
|
||||
// 确保食物不生成在蛇身上
|
||||
let collision = this.snake.some(segment => segment.x === newFood.x && segment.y === newFood.y);
|
||||
if (!collision) break;
|
||||
}
|
||||
return newFood;
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
if (this.dy === 0) { this.dx = 0; this.dy = -1; }
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (this.dy === 0) { this.dx = 0; this.dy = 1; }
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
if (this.dx === 0) { this.dx = -1; this.dy = 0; }
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
if (this.dx === 0) { this.dx = 1; this.dy = 0; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.gameOver) return;
|
||||
|
||||
const head = { x: this.snake[0].x + this.dx, y: this.snake[0].y + this.dy };
|
||||
|
||||
// 碰撞墙壁检测
|
||||
if (head.x < 0 || head.x >= this.tileCount || head.y < 0 || head.y >= this.tileCount) {
|
||||
this.endGame();
|
||||
return;
|
||||
}
|
||||
|
||||
// 碰撞自身检测
|
||||
if (this.snake.some(segment => segment.x === head.x && segment.y === head.y)) {
|
||||
this.endGame();
|
||||
return;
|
||||
}
|
||||
|
||||
this.snake.unshift(head);
|
||||
|
||||
// 进食检测
|
||||
if (head.x === this.food.x && head.y === this.food.y) {
|
||||
this.score += 10;
|
||||
this.updateScore();
|
||||
this.food = this.getRandomFood();
|
||||
} else {
|
||||
this.snake.pop();
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
// 清屏
|
||||
this.ctx.fillStyle = '#1a1a1a';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// 画网格背景 (可选)
|
||||
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.03)';
|
||||
this.ctx.lineWidth = 1;
|
||||
for (let i = 0; i <= this.tileCount; i++) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(i * this.gridSize, 0);
|
||||
this.ctx.lineTo(i * this.gridSize, this.canvas.height);
|
||||
this.ctx.stroke();
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(0, i * this.gridSize);
|
||||
this.ctx.lineTo(this.canvas.width, i * this.gridSize);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
// 画食物
|
||||
const foodX = this.food.x * this.gridSize + this.gridSize / 2;
|
||||
const foodY = this.food.y * this.gridSize + this.gridSize / 2;
|
||||
const foodRadius = this.gridSize / 2 - 2;
|
||||
|
||||
const gradient = this.ctx.createRadialGradient(foodX, foodY, 2, foodX, foodY, foodRadius);
|
||||
gradient.addColorStop(0, '#ff7675');
|
||||
gradient.addColorStop(1, '#d63031');
|
||||
|
||||
this.ctx.fillStyle = gradient;
|
||||
this.ctx.shadowBlur = 15;
|
||||
this.ctx.shadowColor = '#d63031';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(foodX, foodY, foodRadius, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
this.ctx.shadowBlur = 0;
|
||||
|
||||
// 画蛇
|
||||
this.snake.forEach((segment, index) => {
|
||||
const isHead = index === 0;
|
||||
const x = segment.x * this.gridSize;
|
||||
const y = segment.y * this.gridSize;
|
||||
const size = this.gridSize - 2;
|
||||
const radius = isHead ? 8 : 4;
|
||||
|
||||
this.ctx.fillStyle = isHead ? '#55efc4' : '#00b894';
|
||||
|
||||
// 蛇头阴影
|
||||
if (isHead) {
|
||||
this.ctx.shadowBlur = 10;
|
||||
this.ctx.shadowColor = '#55efc4';
|
||||
}
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.roundRect(x + 1, y + 1, size, size, radius);
|
||||
this.ctx.fill();
|
||||
this.ctx.shadowBlur = 0;
|
||||
|
||||
// 眼睛
|
||||
if (isHead) {
|
||||
this.ctx.fillStyle = '#2d3436';
|
||||
const eyeSize = 3;
|
||||
const offset = 5;
|
||||
|
||||
// 根据方向画眼睛
|
||||
if (this.dy === -1) { // Up
|
||||
this.ctx.fillRect(x + offset, y + offset, eyeSize, eyeSize);
|
||||
this.ctx.fillRect(x + size - offset - eyeSize, y + offset, eyeSize, eyeSize);
|
||||
} else if (this.dy === 1) { // Down
|
||||
this.ctx.fillRect(x + offset, y + size - offset - eyeSize, eyeSize, eyeSize);
|
||||
this.ctx.fillRect(x + size - offset - eyeSize, y + size - offset - eyeSize, eyeSize, eyeSize);
|
||||
} else if (this.dx === -1) { // Left
|
||||
this.ctx.fillRect(x + offset, y + offset, eyeSize, eyeSize);
|
||||
this.ctx.fillRect(x + offset, y + size - offset - eyeSize, eyeSize, eyeSize);
|
||||
} else { // Right
|
||||
this.ctx.fillRect(x + size - offset - eyeSize, y + offset, eyeSize, eyeSize);
|
||||
this.ctx.fillRect(x + size - offset - eyeSize, y + size - offset - eyeSize, eyeSize, eyeSize);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.gameOver) {
|
||||
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.75)';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
this.ctx.shadowBlur = 20;
|
||||
this.ctx.shadowColor = 'rgba(255, 255, 255, 0.5)';
|
||||
this.ctx.fillStyle = 'white';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.font = 'bold 40px Segoe UI';
|
||||
this.ctx.fillText('GAME OVER', this.canvas.width / 2, this.canvas.height / 2);
|
||||
|
||||
this.ctx.font = '20px Segoe UI';
|
||||
this.ctx.fillStyle = '#bdc3c7';
|
||||
this.ctx.fillText('按空格重新开始', this.canvas.width / 2, this.canvas.height / 2 + 50);
|
||||
this.ctx.shadowBlur = 0;
|
||||
this.ctx.textAlign = 'start';
|
||||
}
|
||||
}
|
||||
|
||||
updateScore() {
|
||||
this.scoreElement.innerText = this.score;
|
||||
}
|
||||
|
||||
endGame() {
|
||||
this.gameOver = true;
|
||||
}
|
||||
|
||||
run() {
|
||||
this.update();
|
||||
this.draw();
|
||||
}
|
||||
}
|
||||
|
||||
const game = new SnakeGame('snakeGame', 'score');
|
||||
setInterval(() => game.run(), 100);
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.code === 'Space' && game.gameOver) {
|
||||
game.reset();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user