This repository has been archived on 2026-03-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
zhangqing c4c48d17b4 feat: 添加贪吃蛇和记忆配对纸牌游戏
- 贪吃蛇游戏: Canvas渲染、方向控制、碰撞检测、计分系统
- 记忆配对纸牌: 3D翻转动画、配对消除、计时器、胜利判定
2026-01-16 23:34:10 +08:00

209 lines
6.9 KiB
JavaScript

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();
}
});