Spaces:
Running
Running
<html lang="zh"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>炫酷道具贪吃蛇</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); | |
body { | |
font-family: 'Press Start 2P', cursive; | |
background: linear-gradient(135deg, #1a1a2e, #16213e); | |
color: #fff; | |
overflow: hidden; | |
} | |
.game-container { | |
position: relative; | |
box-shadow: 0 0 50px rgba(0, 255, 255, 0.3); | |
border: 4px solid rgba(0, 255, 255, 0.5); | |
border-radius: 8px; | |
overflow: hidden; | |
} | |
#game-canvas { | |
background-color: #0f0f23; | |
display: block; | |
} | |
.snake-cell { | |
box-shadow: 0 0 10px currentColor; | |
} | |
.food-cell { | |
animation: pulse 1.5s infinite; | |
} | |
.powerup-cell { | |
animation: glow 2s infinite alternate, spin 4s infinite linear; | |
} | |
.portal-cell { | |
animation: portalPulse 2s infinite; | |
} | |
.bomb-cell { | |
animation: bombPulse 1s infinite; | |
} | |
@keyframes pulse { | |
0% { transform: scale(1); } | |
50% { transform: scale(1.2); } | |
100% { transform: scale(1); } | |
} | |
@keyframes glow { | |
0% { filter: drop-shadow(0 0 5px currentColor); } | |
100% { filter: drop-shadow(0 0 15px currentColor); } | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
@keyframes portalPulse { | |
0% { box-shadow: 0 0 10px #9c27b0, inset 0 0 10px #9c27b0; } | |
50% { box-shadow: 0 0 20px #9c27b0, inset 0 0 20px #9c27b0; } | |
100% { box-shadow: 0 0 10px #9c27b0, inset 0 0 10px #9c27b0; } | |
} | |
@keyframes bombPulse { | |
0% { opacity: 0.7; transform: scale(0.9); } | |
50% { opacity: 1; transform: scale(1.1); } | |
100% { opacity: 0.7; transform: scale(0.9); } | |
} | |
.score-display { | |
text-shadow: 0 0 10px #00ffaa; | |
font-size: 1.5rem; | |
letter-spacing: 2px; | |
} | |
.particle { | |
position: absolute; | |
width: 8px; | |
height: 8px; | |
border-radius: 50%; | |
pointer-events: none; | |
} | |
.game-over { | |
backdrop-filter: blur(5px); | |
background-color: rgba(0, 0, 0, 0.7); | |
} | |
.powerup-indicator { | |
animation: indicatorPulse 1s infinite alternate; | |
box-shadow: 0 0 10px currentColor; | |
} | |
@keyframes indicatorPulse { | |
0% { opacity: 0.7; transform: scale(0.9); } | |
100% { opacity: 1; transform: scale(1.1); } | |
} | |
</style> | |
</head> | |
<body class="min-h-screen flex flex-col items-center justify-center p-4"> | |
<div class="text-center mb-6"> | |
<h1 class="text-4xl md:text-5xl font-bold mb-2 text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-purple-500"> | |
炫酷道具贪吃蛇 | |
</h1> | |
<p class="text-amber-400 text-sm md:text-base">AI自动模式已启用 - 按空格键切换手动/自动</p> | |
</div> | |
<div class="flex flex-col md:flex-row gap-6 items-center justify-center"> | |
<div class="game-container relative"> | |
<canvas id="game-canvas" width="600" height="600"></canvas> | |
<div id="game-over" class="game-over absolute inset-0 flex flex-col items-center justify-center hidden"> | |
<h2 class="text-4xl text-red-500 mb-6">游戏结束!</h2> | |
<p class="text-xl text-white mb-8">最终分数: <span id="final-score" class="text-green-400">0</span></p> | |
<button id="restart-btn" class="px-6 py-3 bg-gradient-to-r from-cyan-500 to-blue-600 rounded-full font-bold uppercase tracking-wider hover:scale-105 transition-transform"> | |
重新开始 | |
</button> | |
</div> | |
</div> | |
<div class="w-full md:w-64 flex flex-col gap-6"> | |
<div class="bg-gray-900 bg-opacity-70 p-4 rounded-lg border border-cyan-500 border-opacity-30"> | |
<h3 class="text-xl text-cyan-400 mb-3 flex items-center gap-2"> | |
<i class="fas fa-tachometer-alt"></i> 游戏状态 | |
</h3> | |
<div class="space-y-4"> | |
<div> | |
<p class="text-gray-400 text-sm mb-1">得分</p> | |
<p id="score" class="score-display text-2xl text-green-400">0</p> | |
</div> | |
<div> | |
<p class="text-gray-400 text-sm mb-1">长度</p> | |
<p id="length" class="text-xl text-white">1</p> | |
</div> | |
<div> | |
<p class="text-gray-400 text-sm mb-1">速度</p> | |
<p id="speed" class="text-xl text-white">1x</p> | |
</div> | |
<div> | |
<p class="text-gray-400 text-sm mb-1">模式</p> | |
<p id="mode" class="text-xl text-amber-400">AI自动</p> | |
</div> | |
</div> | |
</div> | |
<div class="bg-gray-900 bg-opacity-70 p-4 rounded-lg border border-purple-500 border-opacity-30"> | |
<h3 class="text-xl text-purple-400 mb-3 flex items-center gap-2"> | |
<i class="fas fa-magic"></i> 道具效果 | |
</h3> | |
<div class="space-y-3"> | |
<div class="flex items-center gap-3"> | |
<div class="w-6 h-6 rounded-full bg-yellow-400 powerup-indicator" id="speed-indicator"></div> | |
<p class="text-sm">速度提升 <span id="speed-timer" class="text-yellow-400">0s</span></p> | |
</div> | |
<div class="flex items-center gap-3"> | |
<div class="w-6 h-6 rounded-full bg-green-500 powerup-indicator" id="growth-indicator"></div> | |
<p class="text-sm">快速生长 <span id="growth-timer" class="text-green-500">0s</span></p> | |
</div> | |
<div class="flex items-center gap-3"> | |
<div class="w-6 h-6 rounded-full bg-red-500 powerup-indicator" id="invincible-indicator"></div> | |
<p class="text-sm">无敌状态 <span id="invincible-timer" class="text-red-500">0s</span></p> | |
</div> | |
</div> | |
</div> | |
<div class="bg-gray-900 bg-opacity-70 p-4 rounded-lg border border-amber-500 border-opacity-30"> | |
<h3 class="text-xl text-amber-400 mb-3 flex items-center gap-2"> | |
<i class="fas fa-info-circle"></i> 道具说明 | |
</h3> | |
<div class="space-y-3 text-xs"> | |
<div class="flex items-start gap-2"> | |
<div class="w-4 h-4 rounded-full bg-red-500 mt-1 flex-shrink-0"></div> | |
<p>普通食物 - +1分, 长度+1</p> | |
</div> | |
<div class="flex items-start gap-2"> | |
<div class="w-4 h-4 rounded-full bg-yellow-400 mt-1 flex-shrink-0"></div> | |
<p>速度道具 - 移动速度加快</p> | |
</div> | |
<div class="flex items-start gap-2"> | |
<div class="w-4 h-4 rounded-full bg-green-500 mt-1 flex-shrink-0"></div> | |
<p>生长道具 - 每次移动长度增加</p> | |
</div> | |
<div class="flex items-start gap-2"> | |
<div class="w-4 h-4 rounded-full bg-blue-400 mt-1 flex-shrink-0"></div> | |
<p>无敌道具 - 可以穿过墙壁和身体</p> | |
</div> | |
<div class="flex items-start gap-2"> | |
<div class="w-4 h-4 rounded-full bg-purple-500 mt-1 flex-shrink-0"></div> | |
<p>传送门 - 传送到随机位置</p> | |
</div> | |
<div class="flex items-start gap-2"> | |
<div class="w-4 h-4 rounded-full bg-orange-500 mt-1 flex-shrink-0"></div> | |
<p>炸弹 - 长度减半</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="mt-8 text-gray-400 text-xs"> | |
<p>使用方向键或WASD控制 | 空格键切换手动/AI模式 | 躲避炸弹</p> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
// 游戏常量 | |
const CELL_SIZE = 20; | |
const CELLS_X = 30; | |
const CELLS_Y = 30; | |
const FPS = 60; | |
const BASE_SPEED = 5; // 每x帧移动一次 | |
const SPEED_INCREASE = 0.95; // 每次吃到食物速度增加系数 | |
// 道具类型 | |
const ITEM_TYPES = { | |
FOOD: { color: '#f44336', score: 1, grow: 1, lifetime: 0 }, | |
SPEED: { color: '#ffeb3b', score: 2, grow: 0, lifetime: 10, effect: 'speed' }, | |
GROWTH: { color: '#4caf50', score: 2, grow: 0, lifetime: 10, effect: 'growth' }, | |
INVINCIBLE: { color: '#2196f3', score: 3, grow: 0, lifetime: 8, effect: 'invincible' }, | |
PORTAL: { color: '#9c27b0', score: 0, grow: -1, lifetime: 0 }, | |
BOMB: { color: '#ff9800', score: 0, grow: -0.5, lifetime: 0 } | |
}; | |
// 游戏变量 | |
let canvas = document.getElementById('game-canvas'); | |
let ctx = canvas.getContext('2d'); | |
let snake = []; | |
let direction = 'right'; | |
let nextDirection = 'right'; | |
let gameLoopInterval; | |
let gameSpeed = BASE_SPEED; | |
let speedCounter = 0; | |
let score = 0; | |
let items = []; | |
let particles = []; | |
let activeEffects = {}; | |
let isGameOver = false; | |
let isAutoMode = true; | |
let lastPortalTime = 0; | |
let portalCooldown = 3; // seconds | |
// DOM 元素 | |
const scoreElement = document.getElementById('score'); | |
const lengthElement = document.getElementById('length'); | |
const speedElement = document.getElementById('speed'); | |
const modeElement = document.getElementById('mode'); | |
const gameOverElement = document.getElementById('game-over'); | |
const finalScoreElement = document.getElementById('final-score'); | |
const restartButton = document.getElementById('restart-btn'); | |
const speedIndicator = document.getElementById('speed-indicator'); | |
const growthIndicator = document.getElementById('growth-indicator'); | |
const invincibleIndicator = document.getElementById('invincible-indicator'); | |
const speedTimer = document.getElementById('speed-timer'); | |
const growthTimer = document.getElementById('growth-timer'); | |
const invincibleTimer = document.getElementById('invincible-timer'); | |
// 初始化游戏 | |
function initGame() { | |
// 重置游戏状态 | |
snake = [ | |
{ x: 5, y: 15 }, | |
{ x: 4, y: 15 }, | |
{ x: 3, y: 15 } | |
]; | |
direction = 'right'; | |
nextDirection = 'right'; | |
gameSpeed = BASE_SPEED; | |
speedCounter = 0; | |
score = 0; | |
items = []; | |
particles = []; | |
activeEffects = {}; | |
isGameOver = false; | |
// 更新UI | |
updateScore(0); | |
lengthElement.textContent = snake.length; | |
speedElement.textContent = '1x'; | |
modeElement.textContent = isAutoMode ? 'AI自动' : '手动控制'; | |
modeElement.className = isAutoMode ? 'text-xl text-amber-400' : 'text-xl text-white'; | |
// 隐藏游戏结束画面 | |
gameOverElement.classList.add('hidden'); | |
// 创建初始食物 | |
createRandomItem(ITEM_TYPES.FOOD); | |
// 开始游戏循环 | |
if (gameLoopInterval) clearInterval(gameLoopInterval); | |
gameLoopInterval = setInterval(gameLoop, 1000 / FPS); | |
} | |
// 游戏主循环 | |
function gameLoop() { | |
// 清除画布 | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
// 更新计时器 | |
speedCounter++; | |
updateEffects(); | |
// AI控制 | |
if (isAutoMode && speedCounter % 2 === 0) { | |
aiControl(); | |
} | |
// 每gameSpeed帧移动一次 | |
if (speedCounter >= gameSpeed) { | |
moveSnake(); | |
speedCounter = 0; | |
} | |
// 绘制网格线 | |
drawGrid(); | |
// 绘制道具 | |
drawItems(); | |
// 绘制蛇 | |
drawSnake(); | |
// 绘制粒子效果 | |
drawParticles(); | |
// 更新粒子 | |
updateParticles(); | |
// 随机生成道具 | |
if (Math.random() < 0.002) { // 0.2% 每帧的几率 | |
const availableTypes = Object.values(ITEM_TYPES).filter(type => | |
type !== ITEM_TYPES.FOOD && | |
(!activeEffects[type.effect] || type.effect === undefined) | |
); | |
if (availableTypes.length > 0) { | |
const randomType = availableTypes[Math.floor(Math.random() * availableTypes.length)]; | |
createRandomItem(randomType); | |
} | |
} | |
} | |
// 绘制网格线 | |
function drawGrid() { | |
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)'; | |
ctx.lineWidth = 1; | |
// 垂直线 | |
for (let x = 0; x <= CELLS_X; x++) { | |
ctx.beginPath(); | |
ctx.moveTo(x * CELL_SIZE, 0); | |
ctx.lineTo(x * CELL_SIZE, CELLS_Y * CELL_SIZE); | |
ctx.stroke(); | |
} | |
// 水平线 | |
for (let y = 0; y <= CELLS_Y; y++) { | |
ctx.beginPath(); | |
ctx.moveTo(0, y * CELL_SIZE); | |
ctx.lineTo(CELLS_X * CELL_SIZE, y * CELL_SIZE); | |
ctx.stroke(); | |
} | |
} | |
// 移动蛇 | |
function moveSnake() { | |
direction = nextDirection; | |
// 创建新头部 | |
const head = { ...snake[0] }; | |
switch (direction) { | |
case 'up': head.y--; break; | |
case 'down': head.y++; break; | |
case 'left': head.x--; break; | |
case 'right': head.x++; break; | |
} | |
// 检查是否撞墙(无敌状态下可以穿过墙壁) | |
if (!activeEffects.invincible) { | |
if (head.x < 0 || head.x >= CELLS_X || head.y < 0 || head.y >= CELLS_Y) { | |
gameOver(); | |
return; | |
} | |
} else { | |
// 穿墙处理 | |
if (head.x < 0) head.x = CELLS_X - 1; | |
if (head.x >= CELLS_X) head.x = 0; | |
if (head.y < 0) head.y = CELLS_Y - 1; | |
if (head.y >= CELLS_Y) head.y = 0; | |
} | |
// 检查是否撞到自己(无敌状态下可以穿过自己) | |
if (!activeEffects.invincible) { | |
for (let i = 0; i < snake.length - 1; i++) { | |
if (head.x === snake[i].x && head.y === snake[i].y) { | |
gameOver(); | |
return; | |
} | |
} | |
} | |
// 添加到头部 | |
snake.unshift(head); | |
// 检查是否吃到道具 | |
let itemConsumed = false; | |
let itemIndex = -1; | |
for (let i = 0; i < items.length; i++) { | |
const item = items[i]; | |
if (head.x === item.x && head.y === item.y) { | |
itemIndex = i; | |
// 应用道具效果 | |
if (item.type.effect) { | |
activateEffect(item.type.effect, item.type.lifetime); | |
} | |
// 处理特殊道具 | |
if (item.type === ITEM_TYPES.PORTAL) { | |
if (Date.now() - lastPortalTime > portalCooldown * 1000) { | |
teleportSnake(); | |
lastPortalTime = Date.now(); | |
} else { | |
continue; // 跳过传送门,因为冷却中 | |
} | |
} | |
if (item.type === ITEM_TYPES.BOMB) { | |
createExplosion(head.x, head.y); | |
} | |
// 更新分数和长度 | |
updateScore(score + item.type.score); | |
// 生长处理 (正数=增加长度,负数=减少长度) | |
if (item.type.grow > 0) { | |
// 如果吃的生长道具,额外添加几节 | |
if (activeEffects.growth) { | |
for (let j = 0; j < 2; j++) { | |
snake.push({ ...snake[snake.length - 1] }); | |
} | |
} | |
} else if (item.type.grow < 0) { | |
// 炸弹效果 | |
const newLength = Math.ceil(snake.length / (Math.abs(item.type.grow) + 1)); | |
snake = snake.slice(0, newLength); | |
if (snake.length < 1) { | |
gameOver(); | |
return; | |
} | |
} | |
// 粒子效果 | |
createParticles(head.x, head.y, item.type.color); | |
itemConsumed = true; | |
break; | |
} | |
} | |
// 移除吃到的道具 | |
if (itemConsumed && itemIndex !== -1) { | |
items.splice(itemIndex, 1); | |
// 如果吃的是普通食物,创建新的食物 | |
if (items.filter(item => item.type === ITEM_TYPES.FOOD).length < 1) { | |
createRandomItem(ITEM_TYPES.FOOD); | |
} | |
} else { | |
// 如果没有吃到道具,移除尾部 | |
snake.pop(); | |
} | |
// 快速生长效果 | |
if (activeEffects.growth) { | |
snake.push({ ...snake[snake.length - 1] }); | |
} | |
// 更新长度显示 | |
lengthElement.textContent = snake.length; | |
} | |
// 绘制蛇 | |
function drawSnake() { | |
const head = snake[0]; | |
// 绘制身体 | |
for (let i = 0; i < snake.length; i++) { | |
const segment = snake[i]; | |
const isHead = i === 0; | |
let color; | |
if (isHead) { | |
if (activeEffects.invincible) { | |
// 头部无敌效果 | |
color = `hsl(${(Date.now() / 20) % 360}, 100%, 60%)`; | |
} else { | |
color = '#00ffaa'; | |
} | |
} else { | |
// 彩虹身体效果 | |
const hue = (240 + i * 1) % 360; | |
color = `hsl(${hue}, 100%, 60%)`; | |
} | |
// 绘制蛇节 | |
ctx.fillStyle = color; | |
ctx.beginPath(); | |
ctx.roundRect( | |
segment.x * CELL_SIZE + 1, | |
segment.y * CELL_SIZE + 1, | |
CELL_SIZE - 2, | |
CELL_SIZE - 2, | |
4 | |
); | |
ctx.fill(); | |
// 添加视觉层次效果 | |
if (i > 0) { | |
ctx.fillStyle = `rgba(255, 255, 255, 0.1)`; | |
ctx.beginPath(); | |
ctx.roundRect( | |
segment.x * CELL_SIZE + 4, | |
segment.y * CELL_SIZE + 4, | |
CELL_SIZE - 8, | |
CELL_SIZE - 8, | |
2 | |
); | |
ctx.fill(); | |
} | |
// 如果是头部,添加眼睛 | |
if (isHead) { | |
const eyeSize = CELL_SIZE * 0.15; | |
const eyeOffset = CELL_SIZE * 0.2; | |
ctx.fillStyle = 'white'; | |
// 根据方向绘制眼睛 | |
if (direction === 'right' || direction === 'left') { | |
// 水平方向 - 上下两只眼睛 | |
ctx.beginPath(); | |
ctx.arc( | |
segment.x * CELL_SIZE + (direction === 'right' ? CELL_SIZE - eyeOffset : eyeOffset), | |
segment.y * CELL_SIZE + eyeOffset, | |
eyeSize, 0, Math.PI * 2 | |
); | |
ctx.fill(); | |
ctx.beginPath(); | |
ctx.arc( | |
segment.x * CELL_SIZE + (direction === 'right' ? CELL_SIZE - eyeOffset : eyeOffset), | |
segment.y * CELL_SIZE + CELL_SIZE - eyeOffset, | |
eyeSize, 0, Math.PI * 2 | |
); | |
ctx.fill(); | |
} else { | |
// 垂直方向 - 左右两只眼睛 | |
ctx.beginPath(); | |
ctx.arc( | |
segment.x * CELL_SIZE + eyeOffset, | |
segment.y * CELL_SIZE + (direction === 'down' ? CELL_SIZE - eyeOffset : eyeOffset), | |
eyeSize, 0, Math.PI * 2 | |
); | |
ctx.fill(); | |
ctx.beginPath(); | |
ctx.arc( | |
segment.x * CELL_SIZE + CELL_SIZE - eyeOffset, | |
segment.y * CELL_SIZE + (direction === 'down' ? CELL_SIZE - eyeOffset : eyeOffset), | |
eyeSize, 0, Math.PI * 2 | |
); | |
ctx.fill(); | |
} | |
// 瞳孔 | |
const pupilSize = eyeSize * 0.6; | |
ctx.fillStyle = '#333'; | |
if (direction === 'right' || direction === 'left') { | |
// 水平方向 | |
ctx.beginPath(); | |
ctx.arc( | |
segment.x * CELL_SIZE + (direction === 'right' ? CELL_SIZE - eyeOffset : eyeOffset), | |
segment.y * CELL_SIZE + eyeOffset, | |
pupilSize, 0, Math.PI * 2 | |
); | |
ctx.fill(); | |
ctx.beginPath(); | |
ctx.arc( | |
segment.x * CELL_SIZE + (direction === 'right' ? CELL_SIZE - eyeOffset : eyeOffset), | |
segment.y * CELL_SIZE + CELL_SIZE - eyeOffset, | |
pupilSize, 0, Math.PI * 2 | |
); | |
ctx.fill(); | |
} else { | |
// 垂直方向 | |
ctx.beginPath(); | |
ctx.arc( | |
segment.x * CELL_SIZE + eyeOffset, | |
segment.y * CELL_SIZE + (direction === 'down' ? CELL_SIZE - eyeOffset : eyeOffset), | |
pupilSize, 0, Math.PI * 2 | |
); | |
ctx.fill(); | |
ctx.beginPath(); | |
ctx.arc( | |
segment.x * CELL_SIZE + CELL_SIZE - eyeOffset, | |
segment.y * CELL_SIZE + (direction === 'down' ? CELL_SIZE - eyeOffset : eyeOffset), | |
pupilSize, 0, Math.PI * 2 | |
); | |
ctx.fill(); | |
} | |
} | |
} | |
} | |
// 绘制道具 | |
function drawItems() { | |
for (const item of items) { | |
const itemX = item.x * CELL_SIZE + CELL_SIZE / 2; | |
const itemY = item.y * CELL_SIZE + CELL_SIZE / 2; | |
const itemSize = CELL_SIZE * 0.6; | |
ctx.save(); | |
ctx.translate(itemX, itemY); | |
// 不同类型的绘制效果 | |
switch (item.type) { | |
case ITEM_TYPES.FOOD: | |
// 普通食物 - 圆形 | |
ctx.fillStyle = item.type.color; | |
ctx.beginPath(); | |
ctx.arc(0, 0, itemSize / 2, 0, Math.PI * 2); | |
ctx.fill(); | |
// 高光 | |
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; | |
ctx.beginPath(); | |
ctx.arc(-itemSize / 4, -itemSize / 4, itemSize / 5, 0, Math.PI * 2); | |
ctx.fill(); | |
break; | |
case ITEM_TYPES.SPEED: | |
// 速度道具 - 闪电形状 | |
ctx.fillStyle = item.type.color; | |
ctx.beginPath(); | |
ctx.moveTo(-itemSize / 2, -itemSize / 4); | |
ctx.lineTo(itemSize / 4, 0); | |
ctx.lineTo(-itemSize / 4, 0); | |
ctx.lineTo(itemSize / 2, itemSize / 4); | |
ctx.lineTo(-itemSize / 4, itemSize / 4); | |
ctx.lineTo(itemSize / 4, -itemSize / 4); | |
ctx.closePath(); | |
ctx.fill(); | |
break; | |
case ITEM_TYPES.GROWTH: | |
// 生长道具 - 加号形状 | |
ctx.fillStyle = item.type.color; | |
ctx.fillRect(-itemSize / 8, -itemSize / 2, itemSize / 4, itemSize); | |
ctx.fillRect(-itemSize / 2, -itemSize / 8, itemSize, itemSize / 4); | |
break; | |
case ITEM_TYPES.INVINCIBLE: | |
// 无敌道具 - 星星形状 | |
ctx.fillStyle = item.type.color; | |
ctx.beginPath(); | |
for (let i = 0; i < 5; i++) { | |
ctx.lineTo( | |
0, | |
-itemSize / 2 | |
); | |
ctx.rotate(Math.PI / 5); | |
ctx.lineTo( | |
0, | |
-itemSize / 5 | |
); | |
ctx.rotate(Math.PI / 5); | |
} | |
ctx.closePath(); | |
ctx.fill(); | |
break; | |
case ITEM_TYPES.PORTAL: | |
// 传送门 - 环形 | |
ctx.fillStyle = item.type.color; | |
ctx.beginPath(); | |
ctx.arc(0, 0, itemSize / 2, 0, Math.PI * 2); | |
ctx.fill(); | |
ctx.fillStyle = '#0f0f23'; | |
ctx.beginPath(); | |
ctx.arc(0, 0, itemSize / 3, 0, Math.PI * 2); | |
ctx.fill(); | |
// 动态环 | |
ctx.strokeStyle = item.type.color; | |
ctx.lineWidth = 2; | |
ctx.beginPath(); | |
ctx.arc(0, 0, itemSize / 2 - 2, 0, Math.PI * (1 + performance.now() / 1000 % 1)); | |
ctx.stroke(); | |
break; | |
case ITEM_TYPES.BOMB: | |
// 炸弹 - 圆形带引线 | |
ctx.fillStyle = item.type.color; | |
ctx.beginPath(); | |
ctx.arc(0, 0, itemSize / 2, 0, Math.PI * 2); | |
ctx.fill(); | |
// 引线 | |
ctx.strokeStyle = '#333'; | |
ctx.lineWidth = 2; | |
ctx.beginPath(); | |
ctx.moveTo(itemSize / 2, -itemSize / 4); | |
ctx.lineTo(itemSize / 2 + 5, -itemSize / 2); | |
ctx.stroke(); | |
// 火花 | |
ctx.fillStyle = `hsl(${(Date.now() / 30) % 360}, 100%, 60%)`; | |
ctx.beginPath(); | |
ctx.arc(itemSize / 2 + 5, -itemSize / 2, 2, 0, Math.PI * 2); | |
ctx.fill(); | |
break; | |
} | |
ctx.restore(); | |
} | |
} | |
// 创建随机道具 | |
function createRandomItem(type) { | |
const item = { | |
x: Math.floor(Math.random() * CELLS_X), | |
y: Math.floor(Math.random() * CELLS_Y), | |
type: type, | |
createdAt: Date.now() | |
}; | |
// 确保道具不会生成在蛇身上 | |
let isValidPosition = true; | |
for (const segment of snake) { | |
if (segment.x === item.x && segment.y === item.y) { | |
isValidPosition = false; | |
break; | |
} | |
} | |
// 确保道具不会重叠 | |
for (const existingItem of items) { | |
if (existingItem.x === item.x && existingItem.y === item.y) { | |
isValidPosition = false; | |
break; | |
} | |
} | |
if (isValidPosition) { | |
items.push(item); | |
return true; | |
} else { | |
// 如果位置无效,尝试递归创建(最多3次) | |
return createRandomItem(type); | |
} | |
} | |
// 更新分数 | |
function updateScore(newScore) { | |
score = newScore; | |
scoreElement.textContent = score; | |
// 随着分数增加,速度会提升 | |
const speedMultiplier = 1 + score * 0.005; | |
gameSpeed = Math.max(BASE_SPEED * SPEED_INCREASE ** (score / 5), 3); | |
speedElement.textContent = speedMultiplier.toFixed(1) + 'x'; | |
} | |
// 游戏结束 | |
function gameOver() { | |
isGameOver = true; | |
clearInterval(gameLoopInterval); | |
// 显示游戏结束画面 | |
finalScoreElement.textContent = score; | |
gameOverElement.classList.remove('hidden'); | |
// 创建爆炸效果 | |
createExplosion(snake[0].x, snake[0].y); | |
} | |
// 传送蛇 | |
function teleportSnake() { | |
// 找到有效位置 | |
let newX, newY; | |
let attempts = 0; | |
const maxAttempts = 100; | |
do { | |
newX = Math.floor(Math.random() * (CELLS_X - 10)) + 5; | |
newY = Math.floor(Math.random() * (CELLS_Y - 10)) + 5; | |
attempts++; | |
} while ( | |
(attempts < maxAttempts) && | |
items.some(item => item.x === newX && item.y === newY) | |
); | |
if (attempts < maxAttempts) { | |
// 计算偏移量 | |
const offsetX = newX - snake[0].x; | |
const offsetY = newY - snake[0].y; | |
// 重新定位整条蛇 | |
for (let i = 0; i < snake.length; i++) { | |
snake[i].x += offsetX; | |
snake[i].y += offsetY; | |
// 确保蛇不会超出边界 | |
if (snake[i].x < 0) snake[i].x = 0; | |
if (snake[i].x >= CELLS_X) snake[i].x = CELLS_X - 1; | |
if (snake[i].y < 0) snake[i].y = 0; | |
if (snake[i].y >= CELLS_Y) snake[i].y = CELLS_Y - 1; | |
} | |
// 传送门粒子效果 | |
createPortalEffect(snake[0].x, snake[0].y); | |
} | |
} | |
// 激活效果 | |
function activateEffect(effect, duration) { | |
// 重置或延长已有效果 | |
if (activeEffects[effect]) { | |
clearTimeout(activeEffects[effect].timeout); | |
} | |
activeEffects[effect] = { | |
startTime: Date.now(), | |
duration: duration * 1000, // 转换为毫秒 | |
timeout: setTimeout(() => { | |
delete activeEffects[effect]; | |
updateEffectIndicators(); | |
}, duration * 1000) | |
}; | |
// 应用效果 | |
switch (effect) { | |
case 'speed': | |
break; | |
case 'growth': | |
break; | |
case 'invincible': | |
break; | |
} | |
updateEffectIndicators(); | |
} | |
// 更新效果计时器显示 | |
function updateEffects() { | |
const now = Date.now(); | |
for (const effect in activeEffects) { | |
const remaining = Math.max(0, (activeEffects[effect].startTime + activeEffects[effect].duration - now) / 1000); | |
switch (effect) { | |
case 'speed': | |
speedTimer.textContent = remaining.toFixed(1) + 's'; | |
break; | |
case 'growth': | |
growthTimer.textContent = remaining.toFixed(1) + 's'; | |
break; | |
case 'invincible': | |
invincibleTimer.textContent = remaining.toFixed(1) + 's'; | |
break; | |
} | |
} | |
} | |
// 更新效果指示器 | |
function updateEffectIndicators() { | |
speedIndicator.style.display = activeEffects.speed ? 'block' : 'none'; | |
growthIndicator.style.display = activeEffects.growth ? 'block' : 'none'; | |
invincibleIndicator.style.display = activeEffects.invincible ? 'block' : 'none'; | |
speedTimer.textContent = '0s'; | |
growthTimer.textContent = '0s'; | |
invincibleTimer.textContent = '0s'; | |
} | |
// 创建粒子效果 | |
function createParticles(x, y, color) { | |
const particleCount = 20; | |
const centerX = x * CELL_SIZE + CELL_SIZE / 2; | |
const centerY = y * CELL_SIZE + CELL_SIZE / 2; | |
for (let i = 0; i < particleCount; i++) { | |
const angle = Math.random() * Math.PI * 2; | |
const speed = Math.random() * 3 + 1; | |
const size = Math.random() * 4 + 2; | |
const lifetime = Math.random() * 1000 + 500; | |
particles.push({ | |
x: centerX, | |
y: centerY, | |
vx: Math.cos(angle) * speed, | |
vy: Math.sin(angle) * speed, | |
size: size, | |
color: color, | |
life: lifetime, | |
maxLife: lifetime, | |
alpha: 1 | |
}); | |
} | |
} | |
// 创建爆炸效果 | |
function createExplosion(x, y) { | |
const particleCount = 50; | |
const centerX = x * CELL_SIZE + CELL_SIZE / 2; | |
const centerY = y * CELL_SIZE + CELL_SIZE / 2; | |
for (let i = 0; i < particleCount; i++) { | |
const angle = Math.random() * Math.PI * 2; | |
const speed = Math.random() * 5 + 2; | |
const size = Math.random() * 6 + 3; | |
const lifetime = Math.random() * 1500 + 500; | |
const hue = Math.random() * 60; // 橙红色调 | |
particles.push({ | |
x: centerX, | |
y: centerY, | |
vx: Math.cos(angle) * speed, | |
vy: Math.sin(angle) * speed, | |
size: size, | |
color: `hsl(${hue}, 100%, 50%)`, | |
life: lifetime, | |
maxLife: lifetime, | |
alpha: 1 | |
}); | |
} | |
} | |
// 创建传送门效果 | |
function createPortalEffect(x, y) { | |
const particleCount = 40; | |
const centerX = x * CELL_SIZE + CELL_SIZE / 2; | |
const centerY = y * CELL_SIZE + CELL_SIZE / 2; | |
for (let i = 0; i < particleCount; i++) { | |
const angle = Math.random() * Math.PI * 2; | |
const radius = Math.random() * 20 + 10; | |
const startRadius = 0; | |
const endRadius = radius * 2; | |
const speed = Math.random() * 0.5 + 0.5; | |
const size = Math.random() * 3 + 2; | |
const lifetime = Math.random() * 2000 + 1000; | |
const hue = Math.random() * 40 + 280; // 紫色调 | |
particles.push({ | |
x: centerX + Math.cos(angle) * startRadius, | |
y: centerY + Math.sin(angle) * startRadius, | |
vx: Math.cos(angle) * speed, | |
vy: Math.sin(angle) * speed, | |
targetX: centerX + Math.cos(angle) * endRadius, | |
targetY: centerY + Math.sin(angle) * endRadius, | |
size: size, | |
color: `hsl(${hue}, 100%, 60%)`, | |
life: lifetime, | |
maxLife: lifetime, | |
alpha: 1, | |
isPortal: true | |
}); | |
} | |
} | |
// 更新粒子 | |
function updateParticles() { | |
for (let i = particles.length - 1; i >= 0; i--) { | |
const p = particles[i]; | |
// 更新位置 | |
p.x += p.vx; | |
p.y += p.vy; | |
// 如果是传送门粒子,移动到目标位置 | |
if (p.isPortal && p.targetX && p.targetY) { | |
const dx = p.targetX - p.x; | |
const dy = p.targetY - p.y; | |
const dist = Math.sqrt(dx * dx + dy * dy); | |
p.vx = dx / dist * p.vx * 2; | |
p.vy = dy / dist * p.vy * 2; | |
if (dist < 5) { | |
p.life = 0; // 到达目标后立即消失 | |
} | |
} else { | |
// 重力效果 | |
p.vy += 0.05; | |
// 摩擦力 | |
p.vx *= 0.98; | |
p.vy *= 0.98; | |
} | |
// 更新生命周期 | |
p.life -= 1000 / FPS; | |
p.alpha = p.life / p.maxLife; | |
// 移除死亡的粒子 | |
if (p.life <= 0) { | |
particles.splice(i, 1); | |
} | |
} | |
} | |
// 绘制粒子 | |
function drawParticles() { | |
for (const p of particles) { | |
ctx.globalAlpha = p.alpha; | |
ctx.fillStyle = p.color; | |
ctx.beginPath(); | |
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); | |
ctx.fill(); | |
} | |
ctx.globalAlpha = 1; | |
} | |
// AI控制算法 | |
function aiControl() { | |
const head = snake[0]; | |
const food = items.filter(item => item.type === ITEM_TYPES.FOOD)[0]; | |
const dangerousItems = items.filter(item => item.type === ITEM_TYPES.BOMB); | |
// 如果没有食物,保持移动 | |
if (!food) { | |
return; | |
} | |
// 计算到食物的方向 | |
const dx = food.x - head.x; | |
const dy = food.y - head.y; | |
const adx = Math.abs(dx); | |
const ady = Math.abs(dy); | |
// 计算安全移动方向(避免撞墙和撞自己) | |
const safeDirections = []; | |
// 检查上方是否安全 | |
if (!willCollide(head.x, head.y - 1) && direction !== 'down') { | |
safeDirections.push('up'); | |
} | |
// 检查下方是否安全 | |
if (!willCollide(head.x, head.y + 1) && direction !== 'up') { | |
safeDirections.push('down'); | |
} | |
// 检查左方是否安全 | |
if (!willCollide(head.x - 1, head.y) && direction !== 'right') { | |
safeDirections.push('left'); | |
} | |
// 检查右方是否安全 | |
if (!willCollide(head.x + 1, head.y) && direction !== 'left') { | |
safeDirections.push('right'); | |
} | |
// 如果没有安全方向,保持当前方向(可能会撞上,但AI会尽量避障) | |
if (safeDirections.length === 0) { | |
nextDirection = direction; | |
return; | |
} | |
// 优先考虑靠近食物的方向 | |
let bestDirection; | |
if (adx > ady) { | |
// 水平优先 | |
if (dx > 0 && safeDirections.includes('right')) { | |
bestDirection = 'right'; | |
} else if (dx < 0 && safeDirections.includes('left')) { | |
bestDirection = 'left'; | |
} else { | |
// 水平不可行,尝试垂直 | |
if (dy > 0 && safeDirections.includes('down')) { | |
bestDirection = 'down'; | |
} else if (dy < 0 && safeDirections.includes('up')) { | |
bestDirection = 'up'; | |
} else { | |
// 随机选择安全方向 | |
bestDirection = safeDirections[Math.floor(Math.random() * safeDirections.length)]; | |
} | |
} | |
} else { | |
// 垂直优先 | |
if (dy > 0 && safeDirections.includes('down')) { | |
bestDirection = 'down'; | |
} else if (dy < 0 && safeDirections.includes('up')) { | |
bestDirection = 'up'; | |
} else { | |
// 垂直不可行,尝试水平 | |
if (dx > 0 && safeDirections.includes('right')) { | |
bestDirection = 'right'; | |
} else if (dx < 0 && safeDirections.includes('left')) { | |
bestDirection = 'left'; | |
} else { | |
// 随机选择安全方向 | |
bestDirection = safeDirections[Math.floor(Math.random() * safeDirections.length)]; | |
} | |
} | |
} | |
// 检查危险的炸弹并避开 | |
for (const bomb of dangerousItems) { | |
const distToBomb = Math.abs(bomb.x - head.x) + Math.abs(bomb.y - head.y); | |
if (distToBomb < 5) { // 如果在炸弹附近 | |
// 计算远离炸弹的方向 | |
const dxBomb = bomb.x - head.x; | |
const dyBomb = bomb.y - head.y; | |
// 尝试远离炸弹 | |
const moveAwayDirections = []; | |
if (dxBomb > 0 && safeDirections.includes('left')) { | |
moveAwayDirections.push('left'); | |
} | |
if (dxBomb < 0 && safeDirections.includes('right')) { | |
moveAwayDirections.push('right'); | |
} | |
if (dyBomb > 0 && safeDirections.includes('up')) { | |
moveAwayDirections.push('up'); | |
} | |
if (dyBomb < 0 && safeDirections.includes('down')) { | |
moveAwayDirections.push('down'); | |
} | |
if (moveAwayDirections.length > 0) { | |
// 选择最远离炸弹的方向(如果可能) | |
let maxDist = 0; | |
let bestAvoidDirection = moveAwayDirections[0]; | |
for (const dir of moveAwayDirections) { | |
let newX = head.x; | |
let newY = head.y; | |
switch (dir) { | |
case 'up': newY--; break; | |
case 'down': newY++; break; | |
case 'left': newX--; break; | |
case 'right': newX++; break; | |
} | |
const newDist = Math.abs(bomb.x - newX) + Math.abs(bomb.y - newY); | |
if (newDist > maxDist) { | |
maxDist = newDist; | |
bestAvoidDirection = dir; | |
} | |
} | |
bestDirection = bestAvoidDirection; | |
} | |
} | |
} | |
// 检查传送门,如果有无敌效果可以使用 | |
const portal = items.find(item => item.type === ITEM_TYPES.PORTAL); | |
if (activeEffects.invincible && portal && Date.now() - lastPortalTime > portalCooldown * 1000) { | |
const dxPortal = portal.x - head.x; | |
const dyPortal = portal.y - head.y; | |
if (Math.abs(dxPortal) + Math.abs(dyPortal) < 3) { // 接近传送门 | |
if (dxPortal > 0 && safeDirections.includes('right')) { | |
bestDirection = 'right'; | |
} else if (dxPortal < 0 && safeDirections.includes('left')) { | |
bestDirection = 'left'; | |
} else if (dyPortal > 0 && safeDirections.includes('down')) { | |
bestDirection = 'down'; | |
} else if (dyPortal < 0 && safeDirections.includes('up')) { | |
bestDirection = 'up'; | |
} | |
} | |
} | |
// 检查速度道具,如果有无敌效果可以使用 | |
const speedItem = items.find(item => item.type === ITEM_TYPES.SPEED); | |
if (speedItem && !activeEffects.speed) { | |
const dxSpeed = speedItem.x - head.x; | |
const dySpeed = speedItem.y - head.y; | |
if (Math.abs(dxSpeed) + Math.abs(dySpeed) < 8) { // 在速度道具附近 | |
if (dxSpeed > 0 && safeDirections.includes('right')) { | |
bestDirection = 'right'; | |
} else if (dxSpeed < 0 && safeDirections.includes('left')) { | |
bestDirection = 'left'; | |
} else if (dySpeed > 0 && safeDirections.includes('down')) { | |
bestDirection = 'down'; | |
} else if (dySpeed < 0 && safeDirections.includes('up')) { | |
bestDirection = 'up'; | |
} | |
} | |
} | |
nextDirection = bestDirection; | |
} | |
// 检查移动是否会碰撞 | |
function willCollide(x, y) { | |
// 检查墙壁(无敌状态下可以穿过) | |
if (!activeEffects.invincible) { | |
if (x < 0 || x >= CELLS_X || y < 0 || y >= CELLS_Y) { | |
return true; | |
} | |
} | |
// 检查自己(无敌状态下可以穿过) | |
if (!activeEffects.invincible) { | |
for (let i = 0; i < snake.length; i++) { | |
if (x === snake[i].x && y === snake[i].y) { | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
// 键盘控制 | |
document.addEventListener('keydown', (e) => { | |
if (isGameOver && e.key === 'r') { | |
initGame(); | |
return; | |
} | |
// 空格键切换自动/手动模式 | |
if (e.key === ' ') { | |
isAutoMode = !isAutoMode; | |
modeElement.textContent = isAutoMode ? 'AI自动' : '手动控制'; | |
modeElement.className = isAutoMode ? 'text-xl text-amber-400' : 'text-xl text-white'; | |
return; | |
} | |
// 手动控制 | |
if (!isAutoMode) { | |
switch (e.key) { | |
case 'ArrowUp': | |
case 'w': | |
case 'W': | |
if (direction !== 'down') nextDirection = 'up'; | |
break; | |
case 'ArrowDown': | |
case 's': | |
case 'S': | |
if (direction !== 'up') nextDirection = 'down'; | |
break; | |
case 'ArrowLeft': | |
case 'a': | |
case 'A': | |
if (direction !== 'right') nextDirection = 'left'; | |
break; | |
case 'ArrowRight': | |
case 'd': | |
case 'D': | |
if (direction !== 'left') nextDirection = 'right'; | |
break; | |
} | |
} | |
}); | |
// 重新开始按钮 | |
restartButton.addEventListener('click', initGame); | |
// 开始游戏 | |
initGame(); | |
}); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=yuliqing16/spider" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p><p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Yoleo/snake-literario" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |