🎮ArcadeLab

霓虹贪吃蛇

by SonicGlider99
526 lines14.5 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>霓虹贪吃蛇</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(circle at 50% 30%, #111133 0%, #05050f 75%);
  font-family: 'Segoe UI', 'Microsoft YaHei', system-ui, sans-serif;
  color: #cfefff;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
  overflow-x: hidden;
}
.wrap {
  text-align: center;
  padding: 20px 0 28px;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}
h1 {
  font-size: 34px;
  letter-spacing: 8px;
  color: #00f5ff;
  text-shadow: 0 0 10px #00f5ff, 0 0 26px rgba(0,245,255,.7), 0 0 60px rgba(0,245,255,.35);
  margin-bottom: 14px;
  font-weight: 700;
}
.hud {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: min(600px, calc(100vw - 24px));
  margin: 0 auto 12px;
  font-size: 17px;
  font-weight: 600;
  letter-spacing: 1px;
  gap: 8px;
}
.hud .score { color: #00f5ff; text-shadow: 0 0 10px rgba(0,245,255,.9); }
.hud .high { color: #ffd75e; text-shadow: 0 0 10px rgba(255,215,94,.9); }
.hud .speed { color: #b18cff; text-shadow: 0 0 10px rgba(177,140,255,.8); }
.game-container {
  position: relative;
  margin: 0 auto;
}
canvas {
  display: block;
  background: #0a0a1a;
  border: 2px solid rgba(0,245,255,.4);
  border-radius: 12px;
  box-shadow: 0 0 20px rgba(0,245,255,.3), 0 0 60px rgba(0,245,255,.12), inset 0 0 40px rgba(0,0,0,.5);
  touch-action: none;
}
.overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(4, 4, 18, .72);
  border-radius: 12px;
  backdrop-filter: blur(3px);
  opacity: 1;
  transition: opacity .25s ease;
}
.overlay.hidden { opacity: 0; pointer-events: none; }
.panel {
  background: rgba(10, 10, 32, .96);
  border: 1px solid rgba(255, 0, 128, .55);
  border-radius: 14px;
  padding: 30px 44px;
  box-shadow: 0 0 30px rgba(255,0,128,.35), 0 0 70px rgba(255,0,128,.15);
  text-align: center;
  max-width: 86vw;
}
.panel h2 {
  font-size: 28px;
  letter-spacing: 6px;
  color: #ff0080;
  text-shadow: 0 0 12px #ff0080, 0 0 30px rgba(255,0,128,.85);
  margin-bottom: 16px;
}
.panel p {
  font-size: 17px;
  margin: 10px 0;
  color: #cfefff;
  letter-spacing: 1px;
}
.panel p span {
  color: #00f5ff;
  font-weight: 700;
  text-shadow: 0 0 10px rgba(0,245,255,.85);
}
#restartBtn {
  margin-top: 20px;
  padding: 13px 38px;
  font-size: 17px;
  letter-spacing: 4px;
  color: #06121a;
  background: linear-gradient(135deg, #00f5ff, #00c8ff);
  border: none;
  border-radius: 9px;
  cursor: pointer;
  font-weight: 700;
  font-family: inherit;
  box-shadow: 0 0 16px rgba(0,245,255,.65);
  transition: transform .15s ease, box-shadow .15s ease;
  touch-action: manipulation;
}
#restartBtn:hover { transform: translateY(-2px); box-shadow: 0 0 28px rgba(0,245,255,.95); }
#restartBtn:active { transform: translateY(0); }
.dpad {
  display: none;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  margin: 16px auto 0;
}
.dpad-row { display: flex; gap: 8px; }
.dpad-btn {
  width: 62px;
  height: 62px;
  border-radius: 14px;
  background: rgba(0, 245, 255, .08);
  border: 1.5px solid rgba(0, 245, 255, .45);
  color: #00f5ff;
  font-size: 22px;
  font-family: inherit;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 0 12px rgba(0,245,255,.25), inset 0 0 10px rgba(0,245,255,.1);
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
.dpad-btn:active {
  background: rgba(0, 245, 255, .3);
  box-shadow: 0 0 22px rgba(0,245,255,.6);
}
.tip {
  margin-top: 14px;
  color: #7f9bbf;
  font-size: 14px;
  letter-spacing: 2px;
}
.tip-mobile { display: none; }

@media (hover: none), (max-width: 700px) {
  .dpad { display: flex; }
  .tip-desktop { display: none; }
  .tip-mobile { display: inline; }
  h1 { font-size: 24px; letter-spacing: 4px; margin-bottom: 10px; }
  .hud { font-size: 14px; }
  .dpad-btn { width: 60px; height: 60px; font-size: 20px; }
  .tip { font-size: 12px; }
}
</style>
</head>
<body>
<div class="wrap">
  <h1>霓虹贪吃蛇</h1>
  <div class="hud">
    <div class="score">分数 <span id="score">0</span></div>
    <div class="speed">速度 <span id="speedVal">150ms</span></div>
    <div class="high">最高分 <span id="highScore">0</span></div>
  </div>
  <div class="game-container" id="gameContainer">
    <canvas id="game" width="600" height="600"></canvas>
    <div id="overlay" class="overlay hidden">
      <div class="panel">
        <h2>游戏结束</h2>
        <p>本次得分:<span id="finalScore">0</span></p>
        <p>最高分:<span id="finalHigh">0</span></p>
        <button id="restartBtn">重新开始</button>
      </div>
    </div>
  </div>
  <div class="dpad" id="dpad">
    <div class="dpad-row">
      <button class="dpad-btn" data-dir="up">▲</button>
    </div>
    <div class="dpad-row">
      <button class="dpad-btn" data-dir="left">◀</button>
      <button class="dpad-btn" data-dir="down">▼</button>
      <button class="dpad-btn" data-dir="right">▶</button>
    </div>
  </div>
  <p class="tip"><span class="tip-desktop">使用方向键 ↑ ↓ ← → 控制蛇的移动</span><span class="tip-mobile">滑动屏幕或点击下方按钮控制蛇的移动</span></p>
</div>

<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const container = document.getElementById('gameContainer');

const GRID = 20; // 20 x 20 网格
const START_SPEED = 150;
const MIN_SPEED = 80;
const SPEED_STEP = 10;
const ACCEL_EVERY = 5;
const HIGH_KEY = 'neonSnakeHighScore';

const DIRS = {
  up: { x: 0, y: -1 },
  down: { x: 0, y: 1 },
  left: { x: -1, y: 0 },
  right: { x: 1, y: 0 },
};

let SIZE = 600;
let CELL = 30;
let snake = [];
let dir = { x: 1, y: 0 };
let nextDir = null;
let food = null;
let score = 0;
let speed = START_SPEED;
let gameState = 'idle'; // idle | playing | over
let timer = null;

let highScore = parseInt(localStorage.getItem(HIGH_KEY) || '0', 10) || 0;

const $ = (id) => document.getElementById(id);

// ---------- 响应式画布(含手机高分屏适配) ----------
function resizeCanvas() {
  const avail = Math.floor(Math.min(window.innerWidth - 24, 600));
  const dpr = Math.min(window.devicePixelRatio || 1, 3);
  SIZE = avail;
  CELL = SIZE / GRID;
  canvas.style.width = SIZE + 'px';
  canvas.style.height = SIZE + 'px';
  canvas.width = Math.round(SIZE * dpr);
  canvas.height = Math.round(SIZE * dpr);
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  container.style.width = SIZE + 'px';
  container.style.height = SIZE + 'px';
  ctx.fillStyle = '#0a0a1a';
  ctx.fillRect(0, 0, SIZE, SIZE);
}

// ---------- 游戏逻辑 ----------
function getSpeed() {
  return Math.max(MIN_SPEED, START_SPEED - Math.floor(score / ACCEL_EVERY) * SPEED_STEP);
}

function resetGame() {
  clearTimeout(timer);
  const cx = Math.floor(GRID / 2);
  const cy = Math.floor(GRID / 2);
  snake = [
    { x: cx, y: cy },
    { x: cx - 1, y: cy },
    { x: cx - 2, y: cy },
  ];
  dir = { x: 1, y: 0 };
  nextDir = null;
  score = 0;
  speed = getSpeed();
  placeFood();
  gameState = 'idle';
  updateHUD();
  ctx.clearRect(0, 0, SIZE, SIZE);
}

function placeFood() {
  const free = [];
  for (let y = 0; y < GRID; y++) {
    for (let x = 0; x < GRID; x++) {
      if (!snake.some(s => s.x === x && s.y === y)) free.push({ x, y });
    }
  }
  food = free[Math.floor(Math.random() * free.length)] || { x: 0, y: 0 };
}

function updateHUD() {
  $('score').textContent = score;
  $('highScore').textContent = highScore;
  $('speedVal').textContent = speed + 'ms';
}

function scheduleTick() {
  timer = setTimeout(tick, speed);
}

function tick() {
  if (gameState !== 'playing') return;
  if (nextDir) {
    // 禁止 180° 掉头
    if (!(nextDir.x === -dir.x && nextDir.y === -dir.y)) {
      dir = nextDir;
    }
    nextDir = null;
  }
  const head = { x: snake[0].x + dir.x, y: snake[0].y + dir.y };
  // 撞墙
  if (head.x < 0 || head.x >= GRID || head.y < 0 || head.y >= GRID) return gameOver();
  // 撞自身(蛇尾即将移走,因此不计入碰撞)
  if (snake.slice(0, -1).some(s => s.x === head.x && s.y === head.y)) return gameOver();

  snake.unshift(head);
  if (head.x === food.x && head.y === food.y) {
    score++;
    speed = getSpeed();
    placeFood();
    updateHUD();
  } else {
    snake.pop();
  }
  scheduleTick();
}

function gameOver() {
  gameState = 'over';
  clearTimeout(timer);
  if (score > highScore) {
    highScore = score;
    try { localStorage.setItem(HIGH_KEY, String(highScore)); } catch (e) {}
  }
  $('finalScore').textContent = score;
  $('finalHigh').textContent = highScore;
  $('overlay').classList.remove('hidden');
  updateHUD();
}

$('restartBtn').addEventListener('click', () => {
  $('overlay').classList.add('hidden');
  resetGame();
  gameState = 'playing';
  updateHUD();
  scheduleTick();
});

// ---------- 方向控制(键盘 / 按钮 / 滑动统一入口) ----------
function setDirection(d) {
  if (!d) return;
  if (gameState === 'idle') {
    dir = d;
    gameState = 'playing';
    updateHUD();
    scheduleTick();
  } else if (gameState === 'playing') {
    nextDir = d;
  }
}

function keyToDir(k) {
  switch (k) {
    case 'ArrowUp': return DIRS.up;
    case 'ArrowDown': return DIRS.down;
    case 'ArrowLeft': return DIRS.left;
    case 'ArrowRight': return DIRS.right;
    default: return null;
  }
}

document.addEventListener('keydown', (e) => {
  if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) e.preventDefault();
  setDirection(keyToDir(e.key));
});

// 屏幕方向按钮
document.querySelectorAll('.dpad-btn').forEach(btn => {
  btn.addEventListener('touchstart', (e) => {
    e.preventDefault();
    setDirection(DIRS[btn.dataset.dir]);
  }, { passive: false });
  btn.addEventListener('mousedown', (e) => {
    e.preventDefault();
    setDirection(DIRS[btn.dataset.dir]);
  });
});

// 在画布上滑动控制方向
let touchStart = null;
canvas.addEventListener('touchstart', (e) => {
  const t = e.touches[0];
  touchStart = { x: t.clientX, y: t.clientY };
}, { passive: true });
canvas.addEventListener('touchend', (e) => {
  if (!touchStart) return;
  const t = e.changedTouches[0];
  const dx = t.clientX - touchStart.x;
  const dy = t.clientY - touchStart.y;
  touchStart = null;
  const ax = Math.abs(dx), ay = Math.abs(dy);
  if (Math.max(ax, ay) < 24) return; // 忽略误触
  if (ax > ay) setDirection(dx > 0 ? DIRS.right : DIRS.left);
  else setDirection(dy > 0 ? DIRS.down : DIRS.up);
}, { passive: true });

window.addEventListener('resize', resizeCanvas);

// ---------- 渲染 ----------
function lerpColor(a, b, t) {
  return [
    Math.round(a[0] + (b[0] - a[0]) * t),
    Math.round(a[1] + (b[1] - a[1]) * t),
    Math.round(a[2] + (b[2] - a[2]) * t),
  ];
}

function roundRect(x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + h, r);
  ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r);
  ctx.arcTo(x, y, x + w, y, r);
  ctx.closePath();
}

function drawGrid() {
  ctx.strokeStyle = 'rgba(0, 245, 255, 0.05)';
  ctx.lineWidth = 1;
  ctx.beginPath();
  for (let i = 0; i <= GRID; i++) {
    const p = Math.floor(i * CELL) + 0.5;
    ctx.moveTo(p, 0);
    ctx.lineTo(p, SIZE);
    ctx.moveTo(0, p);
    ctx.lineTo(SIZE, p);
  }
  ctx.stroke();
}

function drawFood(ts) {
  const pulse = (Math.sin(ts / 180) + 1) / 2;
  const cx = food.x * CELL + CELL / 2;
  const cy = food.y * CELL + CELL / 2;
  const r = CELL * 0.32 * (0.72 + 0.28 * pulse);
  ctx.save();
  ctx.shadowColor = '#ff0080';
  ctx.shadowBlur = 16 + 14 * pulse;
  ctx.fillStyle = 'rgba(255, 0, 128, ' + (0.7 + 0.3 * pulse).toFixed(2) + ')';
  ctx.beginPath();
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
  ctx.fill();
  ctx.shadowBlur = 0;
  ctx.fillStyle = 'rgba(255, 255, 255, ' + (0.35 + 0.4 * pulse).toFixed(2) + ')';
  ctx.beginPath();
  ctx.arc(cx, cy, r * 0.45, 0, Math.PI * 2);
  ctx.fill();
  ctx.restore();
}

function drawSnake(ts) {
  const n = snake.length;
  for (let i = n - 1; i >= 0; i--) {
    const seg = snake[i];
    const t = n > 1 ? i / (n - 1) : 0;
    // 头部亮青色 -> 尾部深蓝的渐变
    const c = lerpColor([0, 245, 255], [0, 50, 140], t);
    ctx.save();
    ctx.shadowColor = 'rgba(0, 245, 255, ' + (0.85 - 0.5 * t).toFixed(2) + ')';
    ctx.shadowBlur = 14 - 9 * t;
    ctx.fillStyle = 'rgb(' + c[0] + ',' + c[1] + ',' + c[2] + ')';
    roundRect(seg.x * CELL + 2.5, seg.y * CELL + 2.5, CELL - 5, CELL - 5, 7);
    ctx.fill();
    ctx.restore();
  }
  drawEyes();
}

function drawEyes() {
  const head = snake[0];
  if (!head) return;
  const cx = head.x * CELL + CELL / 2;
  const cy = head.y * CELL + CELL / 2;
  const px = -dir.y, py = dir.x;
  const fwd = CELL * 0.17, side = CELL * 0.17;
  ctx.save();
  ctx.fillStyle = '#ffffff';
  ctx.shadowColor = '#00f5ff';
  ctx.shadowBlur = 6;
  for (const s of [-1, 1]) {
    ctx.beginPath();
    ctx.arc(cx + dir.x * fwd + px * side * s, cy + dir.y * fwd + py * side * s, CELL * 0.09, 0, Math.PI * 2);
    ctx.fill();
  }
  ctx.restore();
}

function drawHint(ts) {
  const a = 0.55 + 0.45 * Math.sin(ts / 300);
  ctx.save();
  ctx.font = 'bold ' + Math.max(18, CELL * 0.9) + 'px "Segoe UI", "Microsoft YaHei", sans-serif';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.shadowColor = '#00f5ff';
  ctx.shadowBlur = 18;
  ctx.fillStyle = 'rgba(0, 245, 255, ' + a.toFixed(2) + ')';
  ctx.fillText('滑动或按方向键开始', SIZE / 2, SIZE / 2);
  ctx.restore();
}

function render(ts) {
  // 半透明覆盖实现拖尾残影渐变效果
  ctx.fillStyle = 'rgba(10, 10, 26, 0.18)';
  ctx.fillRect(0, 0, SIZE, SIZE);
  drawGrid();
  if (food) drawFood(ts);
  if (snake.length) drawSnake(ts);
  if (gameState === 'idle') drawHint(ts);
  requestAnimationFrame(render);
}

resizeCanvas();
resetGame();
requestAnimationFrame(render);
</script>
</body>
</html>

Game Source: 霓虹贪吃蛇

Creator: SonicGlider99

Libraries: none

Complexity: complex (526 lines, 14.5 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-sonicglider99" to link back to the original. Then publish at arcadelab.ai/publish.