借助AI制作的贪吃蛇游戏(单HTML文件)

在本地部署了Qwen3.6-27B-MTP模型,然后结合WorkBuddy,制作了一个贪吃蛇游戏试下效果。看起来还不错。纯html文件,没有使用任何图片元素。浏览器打开直接可以玩。游戏截图如下:

让AI写了个游戏说明.md

贪吃蛇游戏 🐍

一款赛博朋克风格的贪吃蛇游戏,使用纯 HTML、CSS 和 JavaScript 打造。无需安装任何依赖,在浏览器中打开即可畅玩!

游戏特色

  • 赛博朋克霓虹风格 --- 深色背景搭配发光青色与紫色,科技感拉满
  • 响应式游戏画面 --- 游戏区域自动适应窗口大小
  • 丰富的设置项 --- 速度、网格大小、音效、网格线等自由切换
  • 隐藏无敌模式 --- 解锁穿墙不死的秘密玩法
  • 粒子特效 --- 吃食物时炸裂的霓虹粒子效果
  • 移动端支持 --- 触摸滑动 + 虚拟方向键,随时随地开玩
  • 最高分记录 --- 自动保存你的最好成绩

如何开始

双击项目文件夹中的 index.html 文件,用浏览器打开即可开始游戏。

操作指南

键盘控制

|---------|---------------|
| 操作 | 按键 |
| 上移 | 方向键 或 W |
| 下移 | 方向键 或 S |
| 左移 | 方向键 或 A |
| 右移 | 方向键 或 D |
| 暂停 / 继续 | 空格键Esc |

手机操作

  • 滑动屏幕 --- 在画面上滑动即可改变蛇的方向
  • 虚拟方向键 --- 屏幕底部有方向按钮,点击控制

游戏设置

游戏过程中点击右上角的 菜单 按钮,可打开设置面板:

  • 游戏速度 --- 慢 / 正常 / 快 / 极快,四种速度任你选
  • 网格大小 --- 15×15(大格子)/ 20×20(适中)/ 30×30(小格子)
  • 音效开关 --- 开启或关闭游戏音效
  • 网格线 --- 显示或隐藏背景网格
  • 食物样式 --- 圆形 / 方形 / 菱形,切换食物外观

无敌模式 🔓

想要不死蛇?来试试隐藏的无敌模式!

解锁方法

  1. 打开 设置面板
  1. 在设置面板底部找到 版本号v1.0.0
  1. 连续点击版本号 7 次
  1. 看到版本号旁边出现 盾牌图标 🛡️,说明无敌模式已激活!
  1. 进入游戏后,右上角的 菜单按钮会变成金色,表示无敌模式生效中

无敌效果

开启后,蛇可以:

  • 穿墙 --- 撞到墙壁会从另一侧出来,不会死亡
  • 穿身 --- 碰到自己的身体也不会死亡

最高分

  • 最高分会自动保存在浏览器中
  • 当你的分数超过历史最高时,会自动更新
  • 只要不清除浏览器缓存,成绩会一直保留

技术支持

浏览器兼容性

推荐使用以下浏览器:

  • ✅ Chrome / Edge(推荐)
  • ✅ Firefox
  • ✅ Safari
  • ✅ Opera

技术实现

  • 单文件应用 --- 所有代码在 index.html 中,无需额外文件
  • Canvas 2D --- 霓虹发光效果,丝滑渲染
  • Web Audio API --- 音效实时合成,无需音频文件
  • Local Storage --- 自动保存最高分和设置

常见问题

Q:游戏画面闪烁怎么办? A:请确保使用较新版本的浏览器。如果仍然闪烁,尝试关闭硬件加速。

Q:为什么最高分重置了? A:清除浏览器缓存或 Local Storage 会导致最高分丢失。建议不要清除浏览器数据。

Q:手机上怎么操作? A:在画面上滑动即可改变方向,或使用屏幕底部的虚拟方向键。

Q:无敌模式怎么关闭? A:无敌模式在当前浏览器会话中保持激活。刷新页面或关闭浏览器后会自动关闭。


祝游戏愉快!🎮


原始游戏代码就是一个HTML,也提供出来

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
  <title>贪吃蛇</title>
  <style>
    /* ========== Reset & Base ========== */
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

    :root {
      /* 赛博朋克 (默认) */
      --bg: #0a0a1a;
      --primary: #00ffff;
      --secondary: #ff00ff;
      --grid-line: #1a1a3a;
      --text: #e0e0e0;
      --snake-head: #00ffff;
      --snake-body: #00ccff;
      --food-color: #ff00ff;
      --panel-bg: rgba(10, 10, 26, 0.95);
      --border-glow: 0 0 10px rgba(0, 255, 255, 0.5), 0 0 20px rgba(0, 255, 255, 0.2);
      --text-glow: 0 0 10px rgba(0, 255, 255, 0.8);
    }

    body {
      background: var(--bg);
      color: var(--text);
      font-family: monospace;
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      overflow: hidden;
      -webkit-user-select: none;
      user-select: none;
    }

    /* ========== 顶部装饰条 ========== */
    body::before {
      content: '';
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 3px;
      background: linear-gradient(90deg, var(--primary), var(--secondary), var(--primary));
      z-index: 999;
      pointer-events: none;
    }

    /* ========== 通用面板 ========== */
    .screen {
      display: none;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      width: 100%;
      height: 100vh;
      padding: 20px;
    }
    .screen.active { display: flex; }

    .panel {
      background: var(--panel-bg);
      border: 1px solid var(--primary);
      box-shadow: var(--border-glow);
      border-radius: 8px;
      padding: 40px 30px;
      text-align: center;
      max-width: 420px;
      width: 90%;
    }

    .panel h1 {
      font-size: 2.2em;
      color: var(--primary);
      text-shadow: var(--text-glow);
      margin-bottom: 30px;
      letter-spacing: 4px;
    }

    .panel h2 {
      font-size: 1.6em;
      color: var(--secondary);
      text-shadow: 0 0 10px rgba(255, 0, 255, 0.6);
      margin-bottom: 20px;
    }

    .panel .score-display {
      font-size: 1.4em;
      margin-bottom: 24px;
      color: var(--primary);
    }

    /* ========== 按钮 ========== */
    .btn {
      display: inline-block;
      padding: 14px 36px;
      margin: 10px;
      font-family: monospace;
      font-size: 1.1em;
      font-weight: bold;
      color: var(--bg);
      background: var(--primary);
      border: 2px solid var(--primary);
      border-radius: 6px;
      cursor: pointer;
      text-shadow: none;
      box-shadow: var(--border-glow);
      transition: all 0.2s ease;
      letter-spacing: 2px;
    }
    .btn:hover {
      background: transparent;
      color: var(--primary);
      box-shadow: 0 0 20px rgba(0, 255, 255, 0.7);
    }
    .btn-secondary {
      background: transparent;
      color: var(--primary);
      border-color: var(--secondary);
      box-shadow: 0 0 8px rgba(255, 0, 255, 0.3);
    }
    .btn-secondary:hover {
      background: var(--secondary);
      color: var(--bg);
    }

    /* ========== 游戏界面 ========== */
    #game-screen {
      gap: 8px;
    }

    #game-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      width: min(90vw, calc(90vh - 130px));
      max-width: 600px;
      padding: 6px 0;
      font-size: 1em;
    }
    #game-header .score-info {
      color: var(--primary);
      text-shadow: var(--text-glow);
    }
    #game-header .pause-btn {
      background: transparent;
      color: var(--secondary);
      border: 1px solid var(--secondary);
      padding: 4px 16px;
      font-family: monospace;
      cursor: pointer;
      border-radius: 4px;
      font-size: 0.9em;
      transition: all 0.3s ease;
    }
    #game-header .pause-btn.god-mode {
      color: #ffd700;
      border-color: #ffd700;
      text-shadow: 0 0 8px #ffd700, 0 0 16px #ffd700;
      box-shadow: 0 0 12px rgba(255, 215, 0, 0.5), 0 0 24px rgba(255, 215, 0, 0.3);
    }

    #game-canvas {
      border: 1px solid var(--primary);
      box-shadow: var(--border-glow);
      display: block;
      border-radius: 2px;
    }

    /* ========== 暂停弹出层 ========== */
    #pause-overlay {
      display: none;
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(10, 10, 26, 0.85);
      z-index: 500;
      flex-direction: column;
      align-items: center;
      justify-content: center;
    }
    #pause-overlay.active { display: flex; }

    .pause-panel {
      background: rgba(10, 10, 26, 0.97);
      border: 1px solid var(--secondary);
      box-shadow: 0 0 15px rgba(255, 0, 255, 0.4), 0 0 30px rgba(255, 0, 255, 0.15);
      border-radius: 8px;
      padding: 40px 50px;
      text-align: center;
      animation: pauseSlideIn 0.2s ease-out;
    }
    .pause-panel h2 {
      margin-bottom: 28px;
    }
    .pause-panel .btn {
      display: block;
      margin: 12px auto;
      width: 220px;
    }

    @keyframes pauseSlideIn {
      from { transform: scale(0.85); opacity: 0; }
      to { transform: scale(1); opacity: 1; }
    }

    /* ========== 设置界面 ========== */
    .settings-panel {
      text-align: left;
      max-width: 420px;
      width: 90%;
    }
    .settings-panel h2 {
      text-align: center;
    }
    .setting-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin: 14px 0;
      font-size: 0.95em;
    }
    .setting-row label {
      color: var(--text);
      min-width: 90px;
    }
    .setting-row select,
    .setting-row input[type="checkbox"] {
      font-family: monospace;
      background: var(--bg);
      color: var(--primary);
      border: 1px solid var(--grid-line);
      padding: 6px 10px;
      border-radius: 4px;
      font-size: 0.9em;
    }
    .setting-row select:focus { border-color: var(--primary); outline: none; }

    .version-text {
      text-align: center;
      margin-top: 30px;
      font-size: 0.85em;
      color: #555;
      cursor: pointer;
      user-select: none;
    }
    .version-text:hover { color: #777; }

    .shield-icon {
      display: inline-block;
      margin-left: 6px;
      color: var(--primary);
      text-shadow: 0 0 6px var(--primary), 0 0 12px var(--primary);
    }

    /* ========== 虚拟方向键 ========== */
    #virtual-dpad {
      display: none;
      grid-template-areas:
        ".  up  ."
        "left down right";
      grid-template-columns: repeat(3, 64px);
      grid-template-rows: repeat(2, 64px);
      gap: 4px;
      margin-top: 10px;
    }
    #virtual-dpad .dpad-btn {
      background: rgba(0, 255, 255, 0.1);
      color: var(--primary);
      border: 1px solid var(--primary);
      border-radius: 8px;
      font-size: 1.5em;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      -webkit-tap-highlight-color: transparent;
    }
    #virtual-dpad .dpad-btn:active {
      background: rgba(0, 255, 255, 0.3);
    }
    #virtual-dpad .up    { grid-area: up; }
    #virtual-dpad .down   { grid-area: down; }
    #virtual-dpad .left   { grid-area: left; }
    #virtual-dpad .right  { grid-area: right; }

    @media (max-width: 768px) {
      #virtual-dpad { display: grid; }
    }
  </style>
</head>
<body>

  <!-- ==================== 开始界面 ==================== -->
  <div id="start-screen" class="screen active">
    <div class="panel">
      <h1>&#9889; 贪吃蛇 &#9889;</h1>
      <button class="btn" id="btn-start">&#9654; 开始游戏</button><br>
      <button class="btn btn-secondary" id="btn-settings">&#9881; 游戏设置</button>
    </div>
  </div>

  <!-- ==================== 游戏界面 ==================== -->
  <div id="game-screen" class="screen">
    <div id="game-header">
      <span class="score-info" id="score-display">分数: 0</span>
      <span class="score-info" id="best-display">最高: 0</span>
      <button class="pause-btn" id="btn-pause">&#128994; 菜单</button>
    </div>
    <canvas id="game-canvas"></canvas>
    <div id="virtual-dpad">
      <div class="dpad-btn up"    data-dir="up">&#9650;</div>
      <div class="dpad-btn left"  data-dir="left">&#9664;</div>
      <div class="dpad-btn down"  data-dir="down">&#9660;</div>
      <div class="dpad-btn right" data-dir="right">&#9654;</div>
    </div>
  </div>

  <!-- ==================== 暂停弹出层 ==================== -->
  <div id="pause-overlay">
    <div class="pause-panel">
      <h2>&#9208; 已暂停</h2>
      <button class="btn" id="btn-resume">&#9654; 继续游戏</button>
      <button class="btn btn-secondary" id="btn-pause-menu">&#8617; 返回菜单</button>
    </div>
  </div>

  <!-- ==================== 游戏结束界面 ==================== -->
  <div id="gameover-screen" class="screen">
    <div class="panel">
      <h2>&#10060; 游戏结束</h2>
      <div class="score-display" id="final-score">最终分数: 0</div>
      <button class="btn" id="btn-restart">&#8635; 再来一局</button><br>
      <button class="btn btn-secondary" id="btn-menu">&#8617; 返回菜单</button>
    </div>
  </div>

  <!-- ==================== 设置界面 ==================== -->
  <div id="settings-screen" class="screen">
    <div class="panel settings-panel">
      <h2>&#9881; 游戏设置</h2>

      <div class="setting-row">
        <label>游戏速度:</label>
        <select id="set-speed">
          <option value="200">慢</option>
          <option value="150" selected>正常</option>
          <option value="100">快</option>
          <option value="60">极快</option>
        </select>
      </div>

      <div class="setting-row">
        <label>网格大小:</label>
        <select id="set-grid">
          <option value="15">小 (15&#215;15)</option>
          <option value="20" selected>中 (20&#215;20)</option>
          <option value="30">大 (30&#215;30)</option>
        </select>
      </div>

      <div class="setting-row">
        <label>主题色彩:</label>
        <select id="set-theme">
          <option value="cyberpunk" selected>赛博朋克</option>
          <option value="classic">经典蓝绿</option>
          <option value="neon">霓虹粉紫</option>
        </select>
      </div>

      <div class="setting-row">
        <label>音效开关:</label>
        <input type="checkbox" id="set-sound" checked />
      </div>

      <div class="setting-row">
        <label>网格线:</label>
        <input type="checkbox" id="set-gridline" checked />
      </div>

      <div class="setting-row">
        <label>食物样式:</label>
        <select id="set-food">
          <option value="classic" selected>经典</option>
          <option value="blink">闪烁</option>
          <option value="pulse">脉冲</option>
        </select>
      </div>

      <div style="text-align:center; margin-top: 22px;">
        <button class="btn btn-secondary" id="btn-settings-back">&#8617; 返回</button>
      </div>

      <div class="version-text" id="version-text">v1.0.0</div>
    </div>
  </div>

  <script>
    // ====================================================================
    // 1. 配置常量
    // ====================================================================
    const THEMES = {
      cyberpunk: {
        bg: '#0a0a1a',
        primary: '#00ffff',
        secondary: '#ff00ff',
        gridLine: '#1a1a3a',
        text: '#e0e0e0',
        snakeHead: '#00ffff',
        snakeBody: '#00ccff',
        food: '#ff00ff'
      },
      classic: {
        bg: '#111',
        primary: '#0f0',
        secondary: '#ff0',
        gridLine: '#1a1a1a',
        text: '#e0e0e0',
        snakeHead: '#0f0',
        snakeBody: '#0a0',
        food: '#f00'
      },
      neon: {
        bg: '#1a0a1a',
        primary: '#ff69b4',
        secondary: '#9b59b6',
        gridLine: '#2a1a2a',
        text: '#e0e0e0',
        snakeHead: '#ff69b4',
        snakeBody: '#d44d9a',
        food: '#9b59b6'
      }
    };

    const SPEED_MAP = { '慢': 200, '正常': 150, '快': 100, '极快': 60 };
    const GRID_MAP  = { '小': 15, '中': 20, '大': 30 };

    // ====================================================================
    // 2. 游戏状态
    // ====================================================================
    const CONFIG = {
      speed: 150,
      gridSize: 20,
      theme: 'cyberpunk',
      sound: true,
      gridLine: true,
      foodStyle: 'classic',
      invincible: false
    };

    let state = {
      screen: 'start',       // start | game | gameover | settings
      snake: [],
      direction: { x: 1, y: 0 },
      nextDirection: { x: 1, y: 0 },
      food: { x: 0, y: 0 },
      score: 0,
      bestScore: 0,
      paused: false,
      isPlaying: false,
      cellSize: 20,
      canvasSize: 400,
      particles: [],
      animFrame: 0
    };

    // ====================================================================
    // 3. 工具函数
    // ====================================================================

    // --- 3.1 音效生成 (Web Audio API) ---
    let audioCtx = null;

    function getAudioContext() {
      if (!audioCtx) {
        try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch (_) { audioCtx = null; }
      }
      return audioCtx;
    }

    function playSound(type) {
      if (!CONFIG.sound) return;
      const ctx = getAudioContext();
      if (!ctx) return;
      const osc = ctx.createOscillator();
      const gain = ctx.createGain();
      osc.connect(gain);
      gain.connect(ctx.destination);

      switch (type) {
        case 'eat':
          osc.frequency.setValueAtTime(800, ctx.currentTime);
          gain.gain.setValueAtTime(0.3, ctx.currentTime);
          gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.1);
          osc.start(ctx.currentTime);
          osc.stop(ctx.currentTime + 0.1);
          break;
        case 'gameover':
          osc.type = 'sawtooth';
          osc.frequency.setValueAtTime(400, ctx.currentTime);
          osc.frequency.linearRampToValueAtTime(200, ctx.currentTime + 0.3);
          gain.gain.setValueAtTime(0.2, ctx.currentTime);
          gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
          osc.start(ctx.currentTime);
          osc.stop(ctx.currentTime + 0.3);
          break;
        case 'pause':
          osc.frequency.setValueAtTime(600, ctx.currentTime);
          gain.gain.setValueAtTime(0.2, ctx.currentTime);
          gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.08);
          osc.start(ctx.currentTime);
          osc.stop(ctx.currentTime + 0.08);
          break;
      }
    }

    // --- 3.2 粒子系统 ---
    function spawnParticles(x, y) {
      const colors = [THEMES[CONFIG.theme].primary, THEMES[CONFIG.theme].secondary, '#ffffff'];
      for (let i = 0; i < 12; i++) {
        const angle = (Math.PI * 2 * i) / 12 + (Math.random() - 0.5) * 0.5;
        const speed = 2 + Math.random() * 3;
        state.particles.push({
          x: x,
          y: y,
          vx: Math.cos(angle) * speed,
          vy: Math.sin(angle) * speed,
          size: 2 + Math.random() * 2,
          color: colors[Math.floor(Math.random() * colors.length)],
          life: 30,
          maxLife: 30
        });
      }
    }

    /**
     * 更新粒子状态,使用 filter 替代 splice 进行清理
     * 微优化:减少数组操作的复杂度
     */
    function updateParticles() {
      for (const p of state.particles) {
        p.x += p.vx;
        p.y += p.vy;
        p.life--;
      }
      state.particles = state.particles.filter(p => p.life > 0);
    }

    // --- 3.3 Canvas 渲染辅助 ---
    const canvas = document.getElementById('game-canvas');
    const ctx = canvas.getContext('2d');

    function calcCanvasSize() {
      const availableWidth = window.innerWidth - 40;
      const availableHeight = window.innerHeight - 160;
      state.cellSize = Math.floor(Math.min(availableWidth, availableHeight) / CONFIG.gridSize);
      state.cellSize = Math.max(state.cellSize, 8);
      state.canvasSize = state.cellSize * CONFIG.gridSize;
      canvas.width = state.canvasSize;
      canvas.height = state.canvasSize;
    }

    // ====================================================================
    // 4. 游戏逻辑
    // ====================================================================

    // --- 4.1 初始化 ---
    function initGame() {
      const mid = Math.floor(CONFIG.gridSize / 2);
      state.snake = [
        { x: mid, y: mid },
        { x: mid - 1, y: mid },
        { x: mid - 2, y: mid }
      ];
      state.direction = { x: 1, y: 0 };
      state.nextDirection = { x: 1, y: 0 };
      state.score = 0;
      state.paused = false;
      state.isPlaying = true;
      state.particles = [];
      state.animFrame = 0;

      // 同步最高分:取 localStorage 和当前会话的最大值
      try {
        const stored = localStorage.getItem('snake_best_score');
        if (stored) {
          const parsed = parseInt(stored, 10);
          if (!isNaN(parsed) && parsed > state.bestScore) {
            state.bestScore = parsed;
          }
        }
      } catch (_) { /* ignore */ }

      spawnFood();
      updateScoreDisplay();
      document.getElementById('btn-pause').textContent = '\u23F8 菜单';
      updatePauseButtonGodMode();
    }

    function spawnFood() {
      let pos;
      do {
        pos = {
          x: Math.floor(Math.random() * CONFIG.gridSize),
          y: Math.floor(Math.random() * CONFIG.gridSize)
        };
      } while (state.snake.some(s => s.x === pos.x && s.y === pos.y));
      state.food = pos;
    }

    // --- 4.2 更新 ---
    function update() {
      state.animFrame++;
      state.direction = { ...state.nextDirection };
      const head = state.snake[0];
      const newHead = {
        x: head.x + state.direction.x,
        y: head.y + state.direction.y
      };

      // 穿墙 / 撞墙检测
      if (newHead.x < 0 || newHead.x >= CONFIG.gridSize || newHead.y < 0 || newHead.y >= CONFIG.gridSize) {
        if (CONFIG.invincible) {
          // 穿墙
          if (newHead.x < 0) newHead.x = CONFIG.gridSize - 1;
          if (newHead.x >= CONFIG.gridSize) newHead.x = 0;
          if (newHead.y < 0) newHead.y = CONFIG.gridSize - 1;
          if (newHead.y >= CONFIG.gridSize) newHead.y = 0;
        } else {
          gameOver();
          return;
        }
      }

      // 撞自己检测
      if (!CONFIG.invincible && state.snake.some(s => s.x === newHead.x && s.y === newHead.y)) {
        gameOver();
        return;
      }

      state.snake.unshift(newHead);

      // 吃食物检测
      if (newHead.x === state.food.x && newHead.y === state.food.y) {
        state.score += 10;
        updateScoreDisplay();
        playSound('eat');
        // 粒子特效
        const px = state.food.x * state.cellSize + state.cellSize / 2;
        const py = state.food.y * state.cellSize + state.cellSize / 2;
        spawnParticles(px, py);
        spawnFood();
      } else {
        state.snake.pop();
      }

      updateParticles();
    }

    // --- 4.3 渲染 ---
    function render() {
      const cs = state.cellSize;
      const t = THEMES[CONFIG.theme];

      // 清空
      ctx.fillStyle = t.bg;
      ctx.fillRect(0, 0, state.canvasSize, state.canvasSize);

      // 网格线
      if (CONFIG.gridLine) {
        ctx.strokeStyle = t.gridLine;
        ctx.lineWidth = 0.5;
        for (let i = 0; i <= CONFIG.gridSize; i++) {
          ctx.beginPath();
          ctx.moveTo(i * cs, 0);
          ctx.lineTo(i * cs, state.canvasSize);
          ctx.stroke();
          ctx.beginPath();
          ctx.moveTo(0, i * cs);
          ctx.lineTo(state.canvasSize, i * cs);
          ctx.stroke();
        }
      }

      // 蛇身 (渐变) --- 微优化:用一个 save/restore 块集中设置 shadowBlur
      // 增强发光效果:外层大光晕 + 内层小光晕,让蛇身整体亮起来
      ctx.save();
      const bodyLen = state.snake.length;
      for (let i = bodyLen - 1; i >= 1; i--) {
        const seg = state.snake[i];
        const ratio = 1 - (i / bodyLen) * 0.5;
        // 从 snakeHead -> snakeBody 渐变
        const r = Math.round(hexR(t.snakeHead) * ratio + hexR(t.snakeBody) * (1 - ratio));
        const g = Math.round(hexG(t.snakeHead) * ratio + hexG(t.snakeBody) * (1 - ratio));
        const b = Math.round(hexB(t.snakeHead) * ratio + hexB(t.snakeBody) * (1 - ratio));
        ctx.fillStyle = `rgb(${r},${g},${b})`;
        ctx.shadowColor = t.snakeHead;
        ctx.shadowBlur = 12;
        roundRect(ctx, seg.x * cs + 1, seg.y * cs + 1, cs - 2, cs - 2, 3);
      }
      ctx.restore();

      // 蛇头 (带眼睛)
      const head = state.snake[0];
      ctx.fillStyle = t.snakeHead;
      ctx.shadowColor = t.snakeHead;
      ctx.shadowBlur = 12;
      roundRect(ctx, head.x * cs + 1, head.y * cs + 1, cs - 2, cs - 2, 4);
      ctx.shadowBlur = 0;

      // 眼睛
      ctx.fillStyle = t.bg;
      const eyeSize = Math.max(2, cs * 0.18);
      const eyeOffset = cs * 0.28;
      let eye1x, eye1y, eye2x, eye2y;
      if (state.direction.x === 1) { // 右
        eye1x = head.x * cs + cs - eyeOffset - eyeSize; eye1y = head.y * cs + eyeOffset;
        eye2x = head.x * cs + cs - eyeOffset - eyeSize; eye2y = head.y * cs + cs - eyeOffset - eyeSize * 2;
      } else if (state.direction.x === -1) { // 左
        eye1x = head.x * cs + eyeOffset; eye1y = head.y * cs + eyeOffset;
        eye2x = head.x * cs + eyeOffset; eye2y = head.y * cs + cs - eyeOffset - eyeSize * 2;
      } else if (state.direction.y === -1) { // 上
        eye1x = head.x * cs + eyeOffset; eye1y = head.y * cs + eyeOffset;
        eye2x = head.x * cs + cs - eyeOffset - eyeSize * 2; eye2y = head.y * cs + eyeOffset;
      } else { // 下
        eye1x = head.x * cs + eyeOffset; eye1y = head.y * cs + cs - eyeOffset - eyeSize;
        eye2x = head.x * cs + cs - eyeOffset - eyeSize * 2; eye2y = head.y * cs + cs - eyeOffset - eyeSize;
      }
      ctx.beginPath();
      ctx.arc(eye1x + eyeSize / 2, eye1y + eyeSize / 2, eyeSize, 0, Math.PI * 2);
      ctx.fill();
      ctx.beginPath();
      ctx.arc(eye2x + eyeSize / 2, eye2y + eyeSize / 2, eyeSize, 0, Math.PI * 2);
      ctx.fill();

      // 食物
      drawFood();
    }

    function drawFood() {
      const cs = state.cellSize;
      const fx = state.food.x * cs + cs / 2;
      const fy = state.food.y * cs + cs / 2;
      const r = cs * 0.4;
      const t = THEMES[CONFIG.theme];
      let alpha = 1;
      let radius = r;

      if (CONFIG.foodStyle === 'blink') {
        alpha = (state.animFrame % 10 < 5) ? 1 : 0.3;
      } else if (CONFIG.foodStyle === 'pulse') {
        radius = r + Math.sin(state.animFrame * 0.3) * r * 0.3;
      }

      ctx.save();
      ctx.globalAlpha = alpha;
      ctx.shadowColor = t.food;
      ctx.shadowBlur = 15;
      ctx.fillStyle = t.food;
      ctx.beginPath();
      ctx.arc(fx, fy, radius, 0, Math.PI * 2);
      ctx.fill();
      ctx.shadowBlur = 0;
      ctx.restore();
    }

    function drawParticles() {
      for (const p of state.particles) {
        const alpha = p.life / p.maxLife;
        ctx.save();
        ctx.globalAlpha = alpha;
        ctx.fillStyle = p.color;
        ctx.shadowColor = p.color;
        ctx.shadowBlur = 4;
        ctx.beginPath();
        ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
      }
    }

    // --- 4.4 游戏循环 ---
    let gameTimer = null;

    function gameLoop() {
      if (state.isPlaying && !state.paused) {
        update();
        render();
        drawParticles();
      }
      // 暂停时不调用 render(),保持最后一帧画面
      // 使用纯 setTimeout 调度,避免嵌套 rAF 导致的闪烁
      gameTimer = setTimeout(gameLoop, CONFIG.speed);
    }

    function startGameLoop() {
      if (gameTimer) clearTimeout(gameTimer);
      gameLoop();
    }

    function stopGameLoop() {
      if (gameTimer) { clearTimeout(gameTimer); gameTimer = null; }
    }

    // --- 4.5 游戏结束 ---
    function gameOver() {
      state.isPlaying = false;
      stopGameLoop();
      playSound('gameover');

      // 更新最高分
      if (state.score > state.bestScore) {
        state.bestScore = state.score;
        localStorage.setItem('snake_best_score', String(state.bestScore));
      }

      document.getElementById('final-score').textContent = `最终分数: ${state.score}`;
      switchScreen('gameover');
    }

    // ====================================================================
    // 5. UI 控制
    // ====================================================================

    function switchScreen(name) {
      state.screen = name;
      document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
      const target = document.getElementById(name + '-screen');
      if (target) target.classList.add('active');

      if (name === 'game') {
        calcCanvasSize();
      }
    }

    function updateScoreDisplay() {
      // 确保最高分 >= 当前分数(会话内真实记录)
      if (state.score > state.bestScore) {
        state.bestScore = state.score;
      }
      document.getElementById('score-display').textContent = `分数: ${state.score}`;
      document.getElementById('best-display').textContent = `最高: ${state.bestScore}`;
    }

    function updatePauseButtonGodMode() {
      const btn = document.getElementById('btn-pause');
      if (CONFIG.invincible) {
        btn.classList.add('god-mode');
      } else {
        btn.classList.remove('god-mode');
      }
    }

    function updateVersionShield() {
      const verEl = document.getElementById('version-text');
      if (CONFIG.invincible) {
        verEl.innerHTML = 'v1.0.0 <span class="shield-icon">🛡️</span>';
      } else {
        verEl.textContent = 'v1.0.0';
      }
    }

    // --- 5.1 暂停弹出层控制 ---
    function showPauseOverlay() {
      document.getElementById('pause-overlay').classList.add('active');
    }

    function hidePauseOverlay() {
      document.getElementById('pause-overlay').classList.remove('active');
    }

    // --- 5.2 事件绑定 ---
    document.getElementById('btn-start').addEventListener('click', () => {
      // 恢复 AudioContext (浏览器策略)
      getAudioContext();
      initGame();
      switchScreen('game');
      startGameLoop();
    });

    document.getElementById('btn-settings').addEventListener('click', () => {
      switchScreen('settings');
      loadSettings();
    });

    document.getElementById('btn-restart').addEventListener('click', () => {
      initGame();
      switchScreen('game');
      startGameLoop();
    });

    document.getElementById('btn-menu').addEventListener('click', () => {
      stopGameLoop();
      state.isPlaying = false;
      document.getElementById('btn-pause').textContent = '\u23F8 菜单';
      switchScreen('start');
    });

    // 顶部"菜单"按钮 → 弹出暂停层
    document.getElementById('btn-pause').addEventListener('click', () => {
      if (!state.isPlaying) return;
      if (state.paused) {
        // 已经在暂停状态,不做重复操作
        return;
      }
      doPause();
    });

    // 暂停弹出层 - 继续游戏
    document.getElementById('btn-resume').addEventListener('click', () => {
      doResume();
    });

    // 暂停弹出层 - 返回菜单
    document.getElementById('btn-pause-menu').addEventListener('click', () => {
      hidePauseOverlay();
      stopGameLoop();
      state.isPlaying = false;
      state.paused = false;
      document.getElementById('btn-pause').textContent = '\u23F8 菜单';
      switchScreen('start');
    });

    document.getElementById('btn-settings-back').addEventListener('click', () => {
      saveSettings();
      switchScreen('start');
    });

    /**
     * 执行暂停:设置 paused 状态 + 显示弹出层
     */
    function doPause() {
      state.paused = true;
      playSound('pause');
      showPauseOverlay();
    }

    /**
     * 执行恢复:清除 paused 状态 + 隐藏弹出层
     */
    function doResume() {
      state.paused = false;
      hidePauseOverlay();
    }

    /**
     * 切换暂停(用于 ESC 快捷键)
     */
    function togglePause() {
      if (!state.isPlaying) return;
      if (state.paused) {
        doResume();
      } else {
        doPause();
      }
    }

    // --- 5.3 键盘事件 ---
    document.addEventListener('keydown', (e) => {
      const key = e.key;
      if (state.screen === 'game') {
        // ESC 键触发暂停/恢复
        if (key === 'Escape') {
          e.preventDefault();
          togglePause();
          return;
        }

        // 空格键暂停(保留兼容)
        if (key === ' ') {
          e.preventDefault();
          togglePause();
          return;
        }

        // 暂停时屏蔽方向键输入
        if (state.paused || !state.isPlaying) return;

        // 方向控制
        switch (key) {
          case 'ArrowUp': case 'w': case 'W':
            if (state.direction.y !== 1) state.nextDirection = { x: 0, y: -1 };
            e.preventDefault();
            break;
          case 'ArrowDown': case 's': case 'S':
            if (state.direction.y !== -1) state.nextDirection = { x: 0, y: 1 };
            e.preventDefault();
            break;
          case 'ArrowLeft': case 'a': case 'A':
            if (state.direction.x !== 1) state.nextDirection = { x: -1, y: 0 };
            e.preventDefault();
            break;
          case 'ArrowRight': case 'd': case 'D':
            if (state.direction.x !== -1) state.nextDirection = { x: 1, y: 0 };
            e.preventDefault();
            break;
        }
      }
    });

    // --- 5.4 虚拟方向键 ---
    document.querySelectorAll('#virtual-dpad .dpad-btn').forEach(btn => {
      btn.addEventListener('touchstart', (e) => {
        e.preventDefault();
        const dir = btn.dataset.dir;
        if (state.paused || !state.isPlaying) return;
        switch (dir) {
          case 'up':
            if (state.direction.y !== 1) state.nextDirection = { x: 0, y: -1 };
            break;
          case 'down':
            if (state.direction.y !== -1) state.nextDirection = { x: 0, y: 1 };
            break;
          case 'left':
            if (state.direction.x !== 1) state.nextDirection = { x: -1, y: 0 };
            break;
          case 'right':
            if (state.direction.x !== -1) state.nextDirection = { x: 1, y: 0 };
            break;
        }
      });

      btn.addEventListener('click', () => {
        const dir = btn.dataset.dir;
        if (state.paused || !state.isPlaying) return;
        switch (dir) {
          case 'up':
            if (state.direction.y !== 1) state.nextDirection = { x: 0, y: -1 };
            break;
          case 'down':
            if (state.direction.y !== -1) state.nextDirection = { x: 0, y: 1 };
            break;
          case 'left':
            if (state.direction.x !== 1) state.nextDirection = { x: -1, y: 0 };
            break;
          case 'right':
            if (state.direction.x !== -1) state.nextDirection = { x: 1, y: 0 };
            break;
        }
      });
    });

    // --- 5.5 触摸事件 (滑动 + 双击) ---
    let touchStartX = 0, touchStartY = 0, touchStartTime = 0;

    canvas.addEventListener('touchstart', (e) => {
      const touch = e.touches[0];
      touchStartX = touch.clientX;
      touchStartY = touch.clientY;
      touchStartTime = Date.now();
    }, { passive: true });

    canvas.addEventListener('touchend', (e) => {
      const touch = e.changedTouches[0];
      const dx = touch.clientX - touchStartX;
      const dy = touch.clientY - touchStartY;
      const dt = Date.now() - touchStartTime;

      // 双击检测 (暂停)
      if (dt < 300 && Math.abs(dx) < 10 && Math.abs(dy) < 10) {
        // 判断是否双击
        if (canvas._lastTap && Date.now() - canvas._lastTap < 350) {
          canvas._lastTap = 0;
          if (state.isPlaying) togglePause();
          return;
        }
        canvas._lastTap = Date.now();
      }

      // 滑动检测
      const absDx = Math.abs(dx);
      const absDy = Math.abs(dy);
      if (Math.max(absDx, absDy) < 20) return; // 太短不算滑动

      if (absDx > absDy) {
        // 水平
        if (dx > 0 && state.direction.x !== -1) state.nextDirection = { x: 1, y: 0 };
        else if (dx < 0 && state.direction.x !== 1) state.nextDirection = { x: -1, y: 0 };
      } else {
        // 垂直
        if (dy > 0 && state.direction.y !== -1) state.nextDirection = { x: 0, y: 1 };
        else if (dy < 0 && state.direction.y !== 1) state.nextDirection = { x: 0, y: -1 };
      }
    }, { passive: true });

    // --- 5.6 窗口 resize (P0 性能优化:100ms 防抖) ---
    let resizeTimer = null;

    window.addEventListener('resize', () => {
      if (state.screen !== 'game') return;

      // 清除之前的定时器
      if (resizeTimer) clearTimeout(resizeTimer);

      // 100ms 防抖
      resizeTimer = setTimeout(() => {
        calcCanvasSize();
        if (state.isPlaying) {
          render();
          drawParticles();
        }
        resizeTimer = null;
      }, 100);
    });

    // ====================================================================
    // 6. 设置面板
    // ====================================================================

    function loadSettings() {
      const saved = localStorage.getItem('snake_settings');
      if (saved) {
        try {
          const s = JSON.parse(saved);
          if (s.speed) document.getElementById('set-speed').value = String(s.speed);
          if (s.gridSize) document.getElementById('set-grid').value = String(s.gridSize);
          if (s.theme) document.getElementById('set-theme').value = s.theme;
          if (typeof s.sound === 'boolean') document.getElementById('set-sound').checked = s.sound;
          if (typeof s.gridLine === 'boolean') document.getElementById('set-gridline').checked = s.gridLine;
          if (s.foodStyle) document.getElementById('set-food').value = s.foodStyle;
        } catch (_) { /* ignore */ }
      }

      // 更新版本号旁的盾牌图标(根据当前无敌状态)
      updateVersionShield();
    }

    function saveSettings() {
      const speed = parseInt(document.getElementById('set-speed').value, 10);
      const gridSize = parseInt(document.getElementById('set-grid').value, 10);
      const theme = document.getElementById('set-theme').value;
      const sound = document.getElementById('set-sound').checked;
      const gridLine = document.getElementById('set-gridline').checked;
      const foodStyle = document.getElementById('set-food').value;

      CONFIG.speed = speed;
      CONFIG.gridSize = gridSize;
      CONFIG.theme = theme;
      CONFIG.sound = sound;
      CONFIG.gridLine = gridLine;
      CONFIG.foodStyle = foodStyle;

      // 应用主题
      applyTheme();

      // 持久化
      localStorage.setItem('snake_settings', JSON.stringify({
        speed, gridSize, theme, sound, gridLine, foodStyle
      }));
    }

    function applyTheme() {
      const t = THEMES[CONFIG.theme];
      const root = document.documentElement.style;
      root.setProperty('--bg', t.bg);
      root.setProperty('--primary', t.primary);
      root.setProperty('--secondary', t.secondary);
      root.setProperty('--grid-line', t.gridLine);
      root.setProperty('--text', t.text);
      root.setProperty('--snake-head', t.snakeHead);
      root.setProperty('--snake-body', t.snakeBody);
      root.setProperty('--food-color', t.food);
      root.setProperty('--panel-bg', t.bg);
      root.setProperty('--border-glow', `0 0 10px ${t.primary}80, 0 0 20px ${t.primary}33`);
      root.setProperty('--text-glow', `0 0 10px ${t.primary}cc`);
      document.body.style.background = t.bg;
    }

    // ====================================================================
    // 7. 隐藏无敌模式
    // ====================================================================

    let versionClickCount = 0;
    let versionClickStart = 0;
    let versionClickTimer = null;

    document.getElementById('version-text').addEventListener('click', () => {
      const now = Date.now();
      if (!versionClickStart || (now - versionClickStart) > 3000) {
        versionClickStart = now;
        versionClickCount = 1;
      } else {
        versionClickCount++;
      }

      if (versionClickTimer) clearTimeout(versionClickTimer);
      versionClickTimer = setTimeout(() => {
        versionClickCount = 0;
        versionClickStart = 0;
      }, 3000);

      if (versionClickCount >= 7 && !CONFIG.invincible) {
        CONFIG.invincible = true;
        console.log('无敌模式已激活');
        versionClickCount = 0;
        versionClickStart = 0;
        const verEl = document.getElementById('version-text');
        verEl.innerHTML = 'v1.0.0 <span class="shield-icon">🛡️</span>';
        updatePauseButtonGodMode();
      }
    });

    // ====================================================================
    // 8. 数据持久化 - 最高分加载
    // ====================================================================

    function loadBestScore() {
      try {
        const val = localStorage.getItem('snake_best_score');
        if (val) state.bestScore = parseInt(val, 10);
      } catch (_) { /* ignore */ }
    }

    // ====================================================================
    // 9. 辅助函数
    // ====================================================================

    function hexR(hex) { return parseInt(hex.slice(1, 3), 16); }
    function hexG(hex) { return parseInt(hex.slice(3, 5), 16); }
    function hexB(hex) { return parseInt(hex.slice(5, 7), 16); }

    function roundRect(ctx, x, y, w, h, r) {
      ctx.beginPath();
      ctx.moveTo(x + r, y);
      ctx.lineTo(x + w - r, y);
      ctx.quadraticCurveTo(x + w, y, x + w, y + r);
      ctx.lineTo(x + w, y + h - r);
      ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
      ctx.lineTo(x + r, y + h);
      ctx.quadraticCurveTo(x, y + h, x, y + h - r);
      ctx.lineTo(x, y + r);
      ctx.quadraticCurveTo(x, y, x + r, y);
      ctx.closePath();
      ctx.fill();
    }

    // ====================================================================
    // 10. 启动
    // ====================================================================

    (function init() {
      loadBestScore();
      loadSettings();
      applyTheme();
      updateScoreDisplay();
    })();
  </script>
</body>
</html>
相关推荐
小虎AI生活13 小时前
WorkBuddy 加 WorkRally,AI 帮你拍短剧的全流程拆解,附保姆级教程
ai编程
阿新聊ai14 小时前
Hook 怎么写才不翻车:小、确定、可解释、可回滚
ai编程
小李@Free2FA14 小时前
AI Agent 在操作你的账号时,2FA 怎么配合
人工智能·ai编程·2fa·账号安全·二次验证
ZhaoJuFei15 小时前
接手新项目让AI生成辅助文档一览(后端)
ai编程
腻害兔16 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 框架层:15 个 Starter 到底在干什么?
前端·vue.js·产品经理·ai编程
「QT(C++)开发工程师」17 小时前
AI Agent 核心组件
人工智能·ai·aigc·ai编程·ai写作
右耳朵猫AI17 小时前
Claude Code 智能体循环入门
ai编程·claude code
李剑一19 小时前
vibe coding开发的系统无法运维?全面AI的公司迟早要在这上面跌个大跟头
aigc·openai·ai编程
Tokaie20 小时前
2026最新5款AI编程工具平替实测合集
ai编程