update: 2026-03-28 20:59
This commit is contained in:
@@ -2,520 +2,12 @@ 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; // 初始速度 (10 * 0.65)
|
||||
this.gameOver = false;
|
||||
this.startTime = Date.now();
|
||||
this.foodEaten = 0;
|
||||
|
||||
// 特殊食物
|
||||
this.specialFood = null;
|
||||
this.specialFoodTimer = 0;
|
||||
this.specialFoodDuration = 5000; // 5秒
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
// 根据分数计算权重(权重越高,越容易触发且数量偏大)
|
||||
calculateWeightByScore(score) {
|
||||
const w = score / 100; // 1000分趋近高权重
|
||||
return Math.max(0.1, Math.min(0.95, w));
|
||||
}
|
||||
|
||||
// 权重偏向的随机整数,weight越大越偏向更大值
|
||||
biasedRandomInt(maxInclusive, weight) {
|
||||
const r = Math.random();
|
||||
const biased = Math.pow(r, 1 - weight);
|
||||
const val = Math.floor(biased * (maxInclusive + 1));
|
||||
return Math.max(0, Math.min(maxInclusive, val));
|
||||
}
|
||||
|
||||
// 在排行榜弹层追加结束信息
|
||||
appendEndInfo(text, type = 'info') {
|
||||
const summary = document.getElementById('leaderboardSummary');
|
||||
if (!summary) return;
|
||||
const info = document.createElement('div');
|
||||
info.style.marginTop = '8px';
|
||||
info.style.fontSize = '14px';
|
||||
info.style.color = type === 'error' ? '#d9534f' : (type === 'success' ? '#28a745' : '#333');
|
||||
info.textContent = text;
|
||||
summary.appendChild(info);
|
||||
}
|
||||
|
||||
// 游戏结束后尝试加“萌芽币”
|
||||
async tryAwardCoinsOnGameOver() {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
this.appendEndInfo('未登录,无法获得萌芽币');
|
||||
return;
|
||||
}
|
||||
|
||||
let email = null;
|
||||
try {
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (userStr) {
|
||||
const userObj = JSON.parse(userStr);
|
||||
email = userObj && (userObj.email || userObj['邮箱']);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (!email) {
|
||||
this.appendEndInfo('未找到账户信息(email),无法加币', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const weight = this.calculateWeightByScore(this.score);
|
||||
let coins = 0;
|
||||
let guaranteed = false;
|
||||
|
||||
// 得分大于400必定触发获得1-5个萌芽币
|
||||
if (this.score > 5) {
|
||||
guaranteed = true;
|
||||
coins = Math.floor(Math.random() * 5) + 1; // 1~5
|
||||
} else {
|
||||
// 使用权重作为概率
|
||||
const roll = Math.random();
|
||||
if (roll > weight) {
|
||||
this.appendEndInfo('本局未获得萌芽币');
|
||||
return;
|
||||
}
|
||||
// 生成0~10随机数量(权重越高越偏向更大)
|
||||
coins = this.biasedRandomInt(10, weight);
|
||||
coins = Math.max(0, Math.min(10, coins));
|
||||
if (coins <= 0) {
|
||||
this.appendEndInfo('本局未获得萌芽币');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 后端 API base(优先父窗口ENV_CONFIG)
|
||||
const apiBase = (window.parent && window.parent.ENV_CONFIG && window.parent.ENV_CONFIG.API_URL)
|
||||
? window.parent.ENV_CONFIG.API_URL
|
||||
: ((window.ENV_CONFIG && window.ENV_CONFIG.API_URL) ? window.ENV_CONFIG.API_URL : 'http://127.0.0.1:5002');
|
||||
|
||||
const resp = await fetch(`${apiBase}/api/user/add-coins`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ email, amount: coins })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
const msg = err && (err.message || err.error) ? (err.message || err.error) : `请求失败(${resp.status})`;
|
||||
this.appendEndInfo(`加币失败:${msg}`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await resp.json();
|
||||
if (data && data.success) {
|
||||
const newCoins = data.data && data.data.new_coins;
|
||||
this.appendEndInfo(`恭喜获得 ${coins} 个萌芽币!当前余额:${newCoins}`, 'success');
|
||||
} else {
|
||||
const msg = (data && (data.message || data.error)) || '未知错误';
|
||||
this.appendEndInfo(`加币失败:${msg}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加币流程发生错误:', e);
|
||||
this.appendEndInfo('加币失败:网络或系统错误', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 有10%几率生成特殊食物
|
||||
if (Math.random() < 0.1 && !this.specialFood) {
|
||||
this.generateSpecialFood();
|
||||
}
|
||||
}
|
||||
|
||||
generateSpecialFood() {
|
||||
let newFood;
|
||||
do {
|
||||
newFood = {
|
||||
x: Math.floor(Math.random() * this.tileCount),
|
||||
y: Math.floor(Math.random() * this.tileCountY),
|
||||
type: 'special',
|
||||
value: 5 // 特殊食物价值5分
|
||||
};
|
||||
} while (this.isPositionOccupied(newFood));
|
||||
|
||||
this.specialFood = newFood;
|
||||
this.specialFoodTimer = Date.now();
|
||||
}
|
||||
|
||||
isPositionOccupied(position) {
|
||||
// 检查是否与蛇身重叠
|
||||
for (let segment of this.snake) {
|
||||
if (segment.x === position.x && segment.y === position.y) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否与普通食物重叠
|
||||
if (this.food && this.food.x === position.x && this.food.y === position.y) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否与特殊食物重叠
|
||||
if (this.specialFood && this.specialFood.x === position.x && this.specialFood.y === position.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;
|
||||
}
|
||||
|
||||
// 更新UI
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
checkCollision(head) {
|
||||
// 检查撞墙
|
||||
if (head.x < 0 || head.x >= this.tileCount || head.y < 0 || head.y >= this.tileCountY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查撞到自己(从第4节开始检查,避免误判)
|
||||
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() {
|
||||
// 每吃5个食物升一级
|
||||
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); // 速度上限13 (20 * 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((segment, index) => {
|
||||
if (index === 0) {
|
||||
// 蛇头
|
||||
this.ctx.fillStyle = '#4CAF50';
|
||||
} else {
|
||||
// 蛇身,渐变颜色
|
||||
const gradient = (index / this.snake.length) * 100;
|
||||
this.ctx.fillStyle = `hsl(120, 80%, ${60 - gradient}%)`;
|
||||
}
|
||||
this.ctx.fillRect(segment.x * this.gridSize, segment.y * this.gridSize, this.gridSize, this.gridSize);
|
||||
|
||||
// 边框
|
||||
this.ctx.strokeStyle = '#2E7D32';
|
||||
this.ctx.strokeRect(segment.x * this.gridSize, segment.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 time = Date.now() - this.specialFoodTimer;
|
||||
const alpha = 0.5 + 0.5 * Math.sin(time / 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;
|
||||
|
||||
// 防止180度转弯
|
||||
if ((this.dx !== 0 && dx !== 0) || (this.dy !== 0 && dy !== 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
}
|
||||
|
||||
// 工具:格式化日期为 YYYY-MM-DD
|
||||
formatDate(date = new Date()) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
showGameOver() {
|
||||
// 构建并展示排行榜弹层
|
||||
const gameTime = Math.floor((Date.now() - this.startTime) / 1000);
|
||||
const overlay = document.getElementById('leaderboardOverlay');
|
||||
const listEl = document.getElementById('leaderboardList');
|
||||
const lbScore = document.getElementById('lbScore');
|
||||
const lbLength = document.getElementById('lbLength');
|
||||
const lbLevel = document.getElementById('lbLevel');
|
||||
const lbGameTime = document.getElementById('lbGameTime');
|
||||
const lbRank = document.getElementById('lbRank');
|
||||
|
||||
if (!overlay || !listEl) {
|
||||
console.warn('排行榜容器不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 汇总当前玩家数据
|
||||
lbScore.textContent = this.score;
|
||||
lbLength.textContent = this.snake.length;
|
||||
lbLevel.textContent = this.level;
|
||||
lbGameTime.textContent = `${gameTime}秒`;
|
||||
|
||||
const currentEntry = {
|
||||
"名称": localStorage.getItem('snakePlayerName') || '我',
|
||||
"账号": localStorage.getItem('snakePlayerAccount') || 'guest@local',
|
||||
"分数": this.score,
|
||||
"时间": this.formatDate(new Date()),
|
||||
__isCurrent: true,
|
||||
__duration: gameTime
|
||||
};
|
||||
|
||||
// 合并并排序数据(使用 gamedata.js 中的 playerdata)
|
||||
const baseData = (typeof playerdata !== 'undefined' && Array.isArray(playerdata)) ? playerdata : [];
|
||||
const merged = [...baseData, currentEntry];
|
||||
merged.sort((a, b) => (b["分数"] || 0) - (a["分数"] || 0));
|
||||
const playerIndex = merged.findIndex(e => e.__isCurrent);
|
||||
lbRank.textContent = playerIndex >= 0 ? `#${playerIndex + 1}` : '—';
|
||||
|
||||
// 生成排行榜(TOP 10)
|
||||
const topList = merged.slice(0, 10).map((entry, idx) => {
|
||||
const isCurrent = !!entry.__isCurrent;
|
||||
const name = entry["名称"] ?? '未知玩家';
|
||||
const score = entry["分数"] ?? 0;
|
||||
const dateStr = entry["时间"] ?? '';
|
||||
const timeStr = isCurrent ? `时长:${entry.__duration}秒` : `时间:${dateStr}`;
|
||||
return `
|
||||
<div class="leaderboard-item ${isCurrent ? 'current-player' : ''}">
|
||||
<span class="rank">#${idx + 1}</span>
|
||||
<span class="player-name">${name}</span>
|
||||
<span class="player-score">${score}分</span>
|
||||
<span class="player-time">${timeStr}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
listEl.innerHTML = topList;
|
||||
|
||||
overlay.style.display = 'flex';
|
||||
// 结束时尝试加币(异步,不阻塞UI)
|
||||
this.tryAwardCoinsOnGameOver();
|
||||
|
||||
// 触发游戏结束事件(供统计模块使用)
|
||||
const gameOverEvent = new CustomEvent('gameOver', {
|
||||
detail: {
|
||||
score: this.score,
|
||||
length: this.snake.length,
|
||||
level: this.level,
|
||||
gameTime: gameTime,
|
||||
foodEaten: this.foodEaten
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(gameOverEvent);
|
||||
|
||||
// 绑定重新开始按钮
|
||||
const restartBtn = document.getElementById('leaderboardRestartBtn');
|
||||
if (restartBtn) {
|
||||
restartBtn.onclick = () => {
|
||||
overlay.style.display = 'none';
|
||||
this.restart();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
restart() {
|
||||
this.snake = [
|
||||
{x: 10, y: 10},
|
||||
{x: 9, y: 10},
|
||||
{x: 8, y: 10}
|
||||
];
|
||||
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;
|
||||
@@ -525,21 +17,170 @@ class SnakeGame {
|
||||
this.startTime = Date.now();
|
||||
this.foodEaten = 0;
|
||||
this.specialFood = null;
|
||||
|
||||
// 隐藏排行榜弹层(若可见)
|
||||
const overlay = document.getElementById('leaderboardOverlay');
|
||||
if (overlay) overlay.style.display = 'none';
|
||||
|
||||
this.specialFoodTimer = 0;
|
||||
this.specialFoodDuration = 5000;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.generateFood();
|
||||
this.updateUI();
|
||||
|
||||
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();
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => { game = new SnakeGame(); });
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
class GameStatistics {
|
||||
constructor() {
|
||||
this.highScores = JSON.parse(localStorage.getItem('snakeHighScores')) || [];
|
||||
this.sessionStats = {
|
||||
gamesPlayed: 0,
|
||||
totalScore: 0,
|
||||
maxLength: 0,
|
||||
maxLevel: 0,
|
||||
totalTime: 0
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 恢复会话统计(如果存在)
|
||||
const savedSession = localStorage.getItem('snakeSessionStats');
|
||||
if (savedSession) {
|
||||
this.sessionStats = JSON.parse(savedSession);
|
||||
}
|
||||
|
||||
// 监听游戏事件
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// 监听自定义游戏事件
|
||||
document.addEventListener('gameOver', (e) => {
|
||||
this.handleGameOver(e.detail);
|
||||
});
|
||||
|
||||
document.addEventListener('foodEaten', (e) => {
|
||||
this.handleFoodEaten(e.detail);
|
||||
});
|
||||
|
||||
document.addEventListener('levelUp', (e) => {
|
||||
this.handleLevelUp(e.detail);
|
||||
});
|
||||
}
|
||||
|
||||
handleGameOver(gameData) {
|
||||
this.sessionStats.gamesPlayed++;
|
||||
this.sessionStats.totalScore += gameData.score;
|
||||
this.sessionStats.maxLength = Math.max(this.sessionStats.maxLength, gameData.length);
|
||||
this.sessionStats.maxLevel = Math.max(this.sessionStats.maxLevel, gameData.level);
|
||||
this.sessionStats.totalTime += gameData.gameTime;
|
||||
|
||||
// 保存会话统计
|
||||
localStorage.setItem('snakeSessionStats', JSON.stringify(this.sessionStats));
|
||||
|
||||
// 检查是否进入高分榜
|
||||
this.checkHighScore(gameData);
|
||||
|
||||
// 显示统计信息
|
||||
this.displaySessionStats();
|
||||
}
|
||||
|
||||
handleFoodEaten(foodData) {
|
||||
// 可以记录特殊食物统计等
|
||||
console.log('食物被吃掉:', foodData);
|
||||
}
|
||||
|
||||
handleLevelUp(levelData) {
|
||||
// 等级提升统计
|
||||
console.log('等级提升到:', levelData.level);
|
||||
}
|
||||
|
||||
checkHighScore(gameData) {
|
||||
const highScoreEntry = {
|
||||
score: gameData.score,
|
||||
length: gameData.length,
|
||||
level: gameData.level,
|
||||
time: gameData.gameTime,
|
||||
date: new Date().toLocaleDateString(),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 添加到高分榜
|
||||
this.highScores.push(highScoreEntry);
|
||||
|
||||
// 按分数排序(降序)
|
||||
this.highScores.sort((a, b) => b.score - a.score);
|
||||
|
||||
// 只保留前10名
|
||||
this.highScores = this.highScores.slice(0, 10);
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('snakeHighScores', JSON.stringify(this.highScores));
|
||||
}
|
||||
|
||||
displaySessionStats() {
|
||||
const statsElement = document.createElement('div');
|
||||
statsElement.className = 'session-stats';
|
||||
statsElement.innerHTML = `
|
||||
<h3>本次会话统计</h3>
|
||||
<p>游戏次数: ${this.sessionStats.gamesPlayed}</p>
|
||||
<p>总得分: ${this.sessionStats.totalScore}</p>
|
||||
<p>最高长度: ${this.sessionStats.maxLength}</p>
|
||||
<p>最高等级: ${this.sessionStats.maxLevel}</p>
|
||||
<p>总游戏时间: ${Math.floor(this.sessionStats.totalTime / 60)}分钟</p>
|
||||
<p>平均得分: ${Math.round(this.sessionStats.totalScore / this.sessionStats.gamesPlayed)}</p>
|
||||
`;
|
||||
|
||||
// 添加到游戏结束模态框
|
||||
const statsContainer = document.querySelector('.stats');
|
||||
if (statsContainer && !document.querySelector('.session-stats')) {
|
||||
statsContainer.appendChild(statsElement);
|
||||
}
|
||||
}
|
||||
|
||||
displayHighScores() {
|
||||
const highScoresElement = document.createElement('div');
|
||||
highScoresElement.className = 'high-scores';
|
||||
|
||||
if (this.highScores.length > 0) {
|
||||
highScoresElement.innerHTML = `
|
||||
<h3>🏆 高分榜</h3>
|
||||
${this.highScores.map((score, index) => `
|
||||
<div class="score-item ${index === 0 ? 'first-place' : ''}">
|
||||
<span class="rank">${index + 1}.</span>
|
||||
<span class="score">${score.score}分</span>
|
||||
<span class="length">长度:${score.length}</span>
|
||||
<span class="date">${score.date}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
} else {
|
||||
highScoresElement.innerHTML = '<p>暂无高分记录</p>';
|
||||
}
|
||||
|
||||
// 添加到游戏结束模态框
|
||||
const modalContent = document.querySelector('.modal-content');
|
||||
if (modalContent && !document.querySelector('.high-scores')) {
|
||||
modalContent.appendChild(highScoresElement);
|
||||
}
|
||||
}
|
||||
|
||||
getAchievements(gameData) {
|
||||
const achievements = [];
|
||||
|
||||
if (gameData.score >= 100) achievements.push('百分达人');
|
||||
if (gameData.length >= 20) achievements.push('长蛇之王');
|
||||
if (gameData.level >= 5) achievements.push('等级大师');
|
||||
if (gameData.gameTime >= 300) achievements.push('持久战将');
|
||||
if (gameData.score >= 50 && gameData.gameTime <= 60) achievements.push('速通高手');
|
||||
|
||||
return achievements;
|
||||
}
|
||||
|
||||
// 工具方法:格式化时间
|
||||
formatTime(seconds) {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}分${secs}秒`;
|
||||
}
|
||||
|
||||
// 清除统计
|
||||
clearStatistics() {
|
||||
this.highScores = [];
|
||||
this.sessionStats = {
|
||||
gamesPlayed: 0,
|
||||
totalScore: 0,
|
||||
maxLength: 0,
|
||||
maxLevel: 0,
|
||||
totalTime: 0
|
||||
};
|
||||
|
||||
localStorage.removeItem('snakeHighScores');
|
||||
localStorage.removeItem('snakeSessionStats');
|
||||
|
||||
console.log('统计信息已清除');
|
||||
}
|
||||
}
|
||||
|
||||
// 原游戏结束界面已移除,保留统计模块以便响应 'gameOver' 事件
|
||||
|
||||
// 初始化统计模块
|
||||
let gameStats;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
gameStats = new GameStatistics();
|
||||
});
|
||||
|
||||
// 添加CSS样式
|
||||
const statsStyles = `
|
||||
.session-stats {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #fdfcfb 0%, #e2d1c3 100%);
|
||||
border-radius: 10px;
|
||||
border: 2px solid #d4a76a;
|
||||
}
|
||||
|
||||
.session-stats h3 {
|
||||
color: #8b4513;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.session-stats p {
|
||||
margin: 5px 0;
|
||||
color: #654321;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.high-scores {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #fff1eb 0%, #ace0f9 100%);
|
||||
border-radius: 10px;
|
||||
border: 2px solid #4682b4;
|
||||
}
|
||||
|
||||
.high-scores h3 {
|
||||
color: #2c5282;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.score-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #cbd5e0;
|
||||
}
|
||||
|
||||
.score-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.score-item.first-place {
|
||||
background: linear-gradient(135deg, #fceabb 0%, #f8b500 100%);
|
||||
border-radius: 5px;
|
||||
padding: 8px;
|
||||
margin: -8px -8px 8px -8px;
|
||||
}
|
||||
|
||||
.rank {
|
||||
font-weight: bold;
|
||||
color: #2d3748;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
.score {
|
||||
font-weight: bold;
|
||||
color: #e53e3e;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.length {
|
||||
color: #4a5568;
|
||||
font-size: 0.8rem;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: #718096;
|
||||
font-size: 0.7rem;
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
|
||||
// 注入样式
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = statsStyles;
|
||||
document.head.appendChild(styleSheet);
|
||||
@@ -1,50 +0,0 @@
|
||||
const playerdata = [
|
||||
{
|
||||
"名称":"树萌芽",
|
||||
"账号":"3205788256@qq.com",
|
||||
"分数":1568,
|
||||
"时间":"2025-09-08"
|
||||
},
|
||||
{
|
||||
"名称":"风行者",
|
||||
"账号":"4456723190@qq.com",
|
||||
"分数":1987,
|
||||
"时间":"2025-09-30"
|
||||
},
|
||||
{
|
||||
"名称":"月光骑士",
|
||||
"账号":"5832197462@qq.com",
|
||||
"分数":876,
|
||||
"时间":"2025-10-02"
|
||||
},
|
||||
{
|
||||
"名称":"星河",
|
||||
"账号":"6724981532@qq.com",
|
||||
"分数":1345,
|
||||
"时间":"2025-10-05"
|
||||
},
|
||||
{
|
||||
"名称":"雷霆",
|
||||
"账号":"7891234567@qq.com",
|
||||
"分数":2105,
|
||||
"时间":"2025-10-08"
|
||||
},
|
||||
{
|
||||
"名称":"火焰猫",
|
||||
"账号":"8912345678@qq.com",
|
||||
"分数":654,
|
||||
"时间":"2025-10-10"
|
||||
},
|
||||
{
|
||||
"名称":"冰雪女王",
|
||||
"账号":"9123456789@qq.com",
|
||||
"分数":1789,
|
||||
"时间":"2025-10-12"
|
||||
},
|
||||
{
|
||||
"名称":"😊",
|
||||
"账号":"1125234890@qq.com",
|
||||
"分数":1432,
|
||||
"时间":"2025-10-15"
|
||||
}
|
||||
]
|
||||
@@ -26,37 +26,17 @@
|
||||
<button id="restartBtn" class="control-btn">重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 游戏结束排行榜弹层(替换旧的游戏结束界面) -->
|
||||
<div id="leaderboardOverlay" class="modal">
|
||||
<div id="gameOverOverlay" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>游戏结束排行榜</h2>
|
||||
<div class="leaderboard-summary" id="leaderboardSummary">
|
||||
<p>
|
||||
分数: <span id="lbScore">0</span>
|
||||
|长度: <span id="lbLength">3</span>
|
||||
|等级: <span id="lbLevel">1</span>
|
||||
</p>
|
||||
<p>
|
||||
游戏时长: <span id="lbGameTime">0秒</span>
|
||||
|你的排名: <span id="lbRank">—</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="leaderboard">
|
||||
<h3>TOP 10</h3>
|
||||
<div id="leaderboardList" class="leaderboard-list"></div>
|
||||
</div>
|
||||
<button id="leaderboardRestartBtn" class="restart-btn">重新开始</button>
|
||||
<h2>游戏结束</h2>
|
||||
<div class="end-summary" id="endSummary"></div>
|
||||
<button id="gameOverRestartBtn" class="restart-btn">重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="gamedata.js"></script>
|
||||
<script src="game-core.js"></script>
|
||||
<script src="game-controls.js"></script>
|
||||
<script src="game-stats.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -175,85 +175,13 @@ body {
|
||||
box-shadow: 0 8px 16px rgba(46, 125, 50, 0.3);
|
||||
}
|
||||
|
||||
/* 排行榜样式 */
|
||||
.leaderboard {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 15px;
|
||||
border: 1px solid rgba(46, 125, 50, 0.2);
|
||||
}
|
||||
|
||||
.leaderboard-summary {
|
||||
margin: 10px 0 15px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 12px;
|
||||
color: #1b5e20;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(46, 125, 50, 0.2);
|
||||
}
|
||||
|
||||
.leaderboard-summary p {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.leaderboard h3 {
|
||||
color: #1b5e20;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.3rem;
|
||||
.end-summary {
|
||||
margin: 16px 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.leaderboard-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.leaderboard-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 10px;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.leaderboard-item.current-player {
|
||||
background: linear-gradient(135deg, #ffeb3b 0%, #fff176 100%);
|
||||
font-weight: bold;
|
||||
border: 2px solid #f57f17;
|
||||
}
|
||||
|
||||
.leaderboard-item .rank {
|
||||
font-weight: bold;
|
||||
min-width: 30px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.leaderboard-item .player-name {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
margin-left: 10px;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.leaderboard-item .player-score {
|
||||
font-weight: bold;
|
||||
color: #1b5e20;
|
||||
min-width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.leaderboard-item .player-time {
|
||||
color: #4a5568;
|
||||
font-size: 0.8rem;
|
||||
min-width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
.end-summary strong { color: #2e7d32; }
|
||||
|
||||
/* 手机端优化 */
|
||||
@media (max-width: 768px) {
|
||||
|
||||
Reference in New Issue
Block a user