187 lines
7.8 KiB
JavaScript
Executable File
187 lines
7.8 KiB
JavaScript
Executable File
class SnakeGame {
|
|
constructor() {
|
|
this.canvas = document.getElementById('gameCanvas');
|
|
this.ctx = this.canvas.getContext('2d');
|
|
this.gridSize = 20;
|
|
this.tileCount = this.canvas.width / this.gridSize;
|
|
this.tileCountY = this.canvas.height / this.gridSize;
|
|
|
|
this.snake = [{x:10,y:10},{x:9,y:10},{x:8,y:10}];
|
|
this.food = {x:15,y:15};
|
|
this.dx = 1;
|
|
this.dy = 0;
|
|
this.score = 0;
|
|
this.level = 1;
|
|
this.gameSpeed = 6.5;
|
|
this.gameOver = false;
|
|
this.startTime = Date.now();
|
|
this.foodEaten = 0;
|
|
this.specialFood = null;
|
|
this.specialFoodTimer = 0;
|
|
this.specialFoodDuration = 5000;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.generateFood();
|
|
this.gameLoop();
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (this.gameOver) return;
|
|
switch(e.key) {
|
|
case 'ArrowUp': case 'w': case 'W':
|
|
if (this.dy === 0) { this.dx = 0; this.dy = -1; } break;
|
|
case 'ArrowDown': case 's': case 'S':
|
|
if (this.dy === 0) { this.dx = 0; this.dy = 1; } break;
|
|
case 'ArrowLeft': case 'a': case 'A':
|
|
if (this.dx === 0) { this.dx = -1; this.dy = 0; } break;
|
|
case 'ArrowRight': case 'd': case 'D':
|
|
if (this.dx === 0) { this.dx = 1; this.dy = 0; } break;
|
|
case ' ': this.togglePause(); break;
|
|
}
|
|
});
|
|
}
|
|
|
|
generateFood() {
|
|
let newFood;
|
|
do { newFood = { x: Math.floor(Math.random() * this.tileCount), y: Math.floor(Math.random() * this.tileCountY) }; }
|
|
while (this.isPositionOccupied(newFood));
|
|
this.food = newFood;
|
|
|
|
if (Math.random() < 0.1 && !this.specialFood) this.generateSpecialFood();
|
|
}
|
|
|
|
generateSpecialFood() {
|
|
let f;
|
|
do { f = { x: Math.floor(Math.random() * this.tileCount), y: Math.floor(Math.random() * this.tileCountY), type:'special', value:5 }; }
|
|
while (this.isPositionOccupied(f));
|
|
this.specialFood = f;
|
|
this.specialFoodTimer = Date.now();
|
|
}
|
|
|
|
isPositionOccupied(pos) {
|
|
for (let s of this.snake) if (s.x === pos.x && s.y === pos.y) return true;
|
|
if (this.food && this.food.x === pos.x && this.food.y === pos.y) return true;
|
|
if (this.specialFood && this.specialFood.x === pos.x && this.specialFood.y === pos.y) return true;
|
|
return false;
|
|
}
|
|
|
|
update() {
|
|
if (this.isPaused || this.gameOver) return;
|
|
const head = {x: this.snake[0].x + this.dx, y: this.snake[0].y + this.dy};
|
|
|
|
if (this.checkCollision(head)) { this.gameOver = true; this.showGameOver(); return; }
|
|
|
|
this.snake.unshift(head);
|
|
|
|
if (head.x === this.food.x && head.y === this.food.y) {
|
|
this.score += 1; this.foodEaten++; this.generateFood(); this.updateLevel();
|
|
} else if (this.specialFood && head.x === this.specialFood.x && head.y === this.specialFood.y) {
|
|
this.score += this.specialFood.value; this.foodEaten++; this.specialFood = null; this.generateFood(); this.updateLevel();
|
|
} else {
|
|
this.snake.pop();
|
|
}
|
|
|
|
if (this.specialFood && Date.now() - this.specialFoodTimer > this.specialFoodDuration) this.specialFood = null;
|
|
this.updateUI();
|
|
}
|
|
|
|
checkCollision(head) {
|
|
if (head.x < 0 || head.x >= this.tileCount || head.y < 0 || head.y >= this.tileCountY) return true;
|
|
for (let i = 4; i < this.snake.length; i++) if (this.snake[i].x === head.x && this.snake[i].y === head.y) return true;
|
|
return false;
|
|
}
|
|
|
|
updateLevel() {
|
|
const newLevel = Math.floor(this.foodEaten / 5) + 1;
|
|
if (newLevel > this.level) { this.level = newLevel; this.gameSpeed = Math.min(13, 6.5 + this.level * 0.65); }
|
|
}
|
|
|
|
updateUI() {
|
|
document.getElementById('score').textContent = this.score;
|
|
document.getElementById('length').textContent = this.snake.length;
|
|
document.getElementById('level').textContent = this.level;
|
|
}
|
|
|
|
draw() {
|
|
this.ctx.fillStyle = '#222';
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
this.ctx.strokeStyle = '#333'; this.ctx.lineWidth = 0.5;
|
|
for (let x = 0; x < this.tileCount; x++)
|
|
for (let y = 0; y < this.tileCountY; y++)
|
|
this.ctx.strokeRect(x * this.gridSize, y * this.gridSize, this.gridSize, this.gridSize);
|
|
|
|
this.snake.forEach((seg, i) => {
|
|
this.ctx.fillStyle = i === 0 ? '#4CAF50' : `hsl(120,80%,${60 - (i/this.snake.length)*100}%)`;
|
|
this.ctx.fillRect(seg.x * this.gridSize, seg.y * this.gridSize, this.gridSize, this.gridSize);
|
|
this.ctx.strokeStyle = '#2E7D32';
|
|
this.ctx.strokeRect(seg.x * this.gridSize, seg.y * this.gridSize, this.gridSize, this.gridSize);
|
|
});
|
|
|
|
this.ctx.fillStyle = '#FF5252';
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(this.food.x * this.gridSize + this.gridSize/2, this.food.y * this.gridSize + this.gridSize/2, this.gridSize/2 - 2, 0, Math.PI*2);
|
|
this.ctx.fill();
|
|
|
|
if (this.specialFood) {
|
|
this.ctx.fillStyle = '#FFD700';
|
|
this.ctx.beginPath();
|
|
this.ctx.arc(this.specialFood.x * this.gridSize + this.gridSize/2, this.specialFood.y * this.gridSize + this.gridSize/2, this.gridSize/2 - 1, 0, Math.PI*2);
|
|
this.ctx.fill();
|
|
const alpha = 0.5 + 0.5 * Math.sin((Date.now() - this.specialFoodTimer) / 200);
|
|
this.ctx.globalAlpha = alpha; this.ctx.fillStyle = '#FF6B00'; this.ctx.fill(); this.ctx.globalAlpha = 1;
|
|
}
|
|
|
|
if (this.isPaused) {
|
|
this.ctx.fillStyle = 'rgba(0,0,0,0.7)';
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
this.ctx.fillStyle = 'white'; this.ctx.font = '24px Arial'; this.ctx.textAlign = 'center';
|
|
this.ctx.fillText('游戏暂停', this.canvas.width/2, this.canvas.height/2);
|
|
}
|
|
}
|
|
|
|
gameLoop() {
|
|
this.update(); this.draw();
|
|
if (!this.gameOver) setTimeout(() => this.gameLoop(), 1000 / this.gameSpeed);
|
|
}
|
|
|
|
changeDirection(dx, dy) {
|
|
if (this.gameOver) return;
|
|
if ((this.dx !== 0 && dx !== 0) || (this.dy !== 0 && dy !== 0)) return;
|
|
this.dx = dx; this.dy = dy;
|
|
}
|
|
|
|
showGameOver() {
|
|
const gameTime = Math.floor((Date.now() - this.startTime) / 1000);
|
|
const overlay = document.getElementById('gameOverOverlay');
|
|
const summary = document.getElementById('endSummary');
|
|
|
|
if (summary) {
|
|
summary.innerHTML =
|
|
`<p>得分 <strong>${this.score}</strong> · 长度 <strong>${this.snake.length}</strong> · 等级 <strong>${this.level}</strong></p>` +
|
|
`<p>用时 <strong>${gameTime}</strong> 秒 · 吃掉 <strong>${this.foodEaten}</strong> 个食物</p>`;
|
|
}
|
|
if (overlay) overlay.style.display = 'flex';
|
|
|
|
const btn = document.getElementById('gameOverRestartBtn');
|
|
if (btn) btn.onclick = () => { overlay.style.display = 'none'; this.restart(); };
|
|
}
|
|
|
|
restart() {
|
|
this.snake = [{x:10,y:10},{x:9,y:10},{x:8,y:10}];
|
|
this.dx = 1; this.dy = 0; this.score = 0; this.level = 1;
|
|
this.gameSpeed = 6.5; this.gameOver = false; this.startTime = Date.now();
|
|
this.foodEaten = 0; this.specialFood = null;
|
|
|
|
const overlay = document.getElementById('gameOverOverlay');
|
|
if (overlay) overlay.style.display = 'none';
|
|
|
|
this.generateFood(); this.updateUI(); this.gameLoop();
|
|
}
|
|
}
|
|
|
|
let game;
|
|
document.addEventListener('DOMContentLoaded', () => { game = new SnakeGame(); });
|