c4c48d17b4
- 贪吃蛇游戏: Canvas渲染、方向控制、碰撞检测、计分系统 - 记忆配对纸牌: 3D翻转动画、配对消除、计时器、胜利判定
209 lines
6.9 KiB
JavaScript
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();
|
|
}
|
|
}); |