<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贪吃蛇</title>
<style>
  *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
  
  body {
    background: #111;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    font-family: 'Segoe UI', system-ui, sans-serif;
    color: #e0e0e0;
  }

  .game-wrapper {
    background: #1a1a2e;
    border-radius: 16px;
    padding: 24px;
    box-shadow: 0 8px 40px rgba(0,0,0,0.6);
  }

  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 16px;
    padding: 0 4px;
  }

  .score-box {
    font-size: 18px;
    font-weight: 600;
    letter-spacing: 0.3px;
  }

  .score-box span {
    color: #f0c040;
  }

  .controls {
    display: flex;
    gap: 8px;
  }

  .controls button {
    background: #2d2d4e;
    border: none;
    color: #ccc;
    font-size: 14px;
    padding: 6px 18px;
    border-radius: 6px;
    cursor: pointer;
    transition: background 0.15s;
    font-family: inherit;
  }

  .controls button:hover { background: #3d3d5e; }
  .controls button:active { background: #4d4d6e; }

  canvas {
    display: block;
    background: #0f0f23;
    border-radius: 10px;
    width: 480px;
    height: 480px;
    image-rendering: pixelated;
  }

  .footer {
    margin-top: 12px;
    text-align: center;
    font-size: 13px;
    color: #666;
  }

  .footer kbd {
    background: #2a2a3e;
    padding: 2px 8px;
    border-radius: 4px;
    font-family: inherit;
    font-size: 12px;
    color: #aaa;
  }

  @media (max-width: 540px) {
    .game-wrapper { padding: 16px; border-radius: 12px; }
    canvas { width: calc(100vw - 64px); height: calc(100vw - 64px); }
  }
</style>
</head>
<body>
<div class="game-wrapper">
  <div class="header">
    <div class="score-box">🍎 <span id="score">0</span></div>
    <div class="controls">
      <button id="restartBtn">重新开始</button>
    </div>
  </div>
  <canvas id="gameCanvas" width="480" height="480"></canvas>
  <div class="footer"><kbd>↑</kbd> <kbd>↓</kbd> <kbd>←</kbd> <kbd>→</kbd> 或 <kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> 控制方向</div>
</div>

<script>
  (function(){
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');
    const scoreEl = document.getElementById('score');

    const GRID_SIZE = 20;       // 20×20 网格
    const CELL_SIZE = 24;       // 480/20
    const TICK_MS = 140;

    let snake, direction, nextDirection, food, score, gameOver, win;
    let timer = null;

    // ---- 初始化 / 重置 ----
    function init() {
      snake = [
        { x: 10, y: 10 },
        { x: 9,  y: 10 },
        { x: 8,  y: 10 },
      ];
      direction  = { x: 1, y: 0 };
      nextDirection = { x: 1, y: 0 };
      score = 0;
      gameOver = false;
      win = false;
      scoreEl.textContent = '0';
      spawnFood();
      draw();
    }

    // ---- 生成食物(不与蛇重叠) ----
    function spawnFood() {
      const occupied = new Set(snake.map(p => `${p.x},${p.y}`));
      const free = [];
      for (let y = 0; y < GRID_SIZE; y++)
        for (let x = 0; x < GRID_SIZE; x++)
          if (!occupied.has(`${x},${y}`)) free.push({ x, y });

      if (free.length === 0) {
        win = true;
        gameOver = true;
        food = null;
        return;
      }
      food = free[Math.floor(Math.random() * free.length)];
    }

    // ---- 游戏逻辑 ----
    function tick() {
      if (gameOver) return;

      direction = { ...nextDirection };
      const head = snake[0];
      const nx = head.x + direction.x;
      const ny = head.y + direction.y;

      // 撞墙
      if (nx < 0 || nx >= GRID_SIZE || ny < 0 || ny >= GRID_SIZE) {
        gameOver = true;
        draw();
        return;
      }

      // 撞自身
      for (const seg of snake) {
        if (seg.x === nx && seg.y === ny) {
          gameOver = true;
          draw();
          return;
        }
      }

      // 移动
      snake.unshift({ x: nx, y: ny });
      const ate = (food && nx === food.x && ny === food.y);
      if (ate) {
        score++;
        scoreEl.textContent = score;
        spawnFood();
      } else {
        snake.pop();
      }

      draw();
    }

    // ---- 绘制 ----
    function draw() {
      ctx.clearRect(0, 0, 480, 480);

      // 网格线(很淡)
      ctx.strokeStyle = 'rgba(255,255,255,0.03)';
      ctx.lineWidth = 0.5;
      for (let i = 0; i <= GRID_SIZE; i++) {
        const p = i * CELL_SIZE;
        ctx.beginPath(); ctx.moveTo(p, 0); ctx.lineTo(p, 480); ctx.stroke();
        ctx.beginPath(); ctx.moveTo(0, p); ctx.lineTo(480, p); ctx.stroke();
      }

      // 食物
      if (food) {
        const fx = food.x * CELL_SIZE, fy = food.y * CELL_SIZE;
        // 苹果渐变
        const grad = ctx.createRadialGradient(fx+6, fy+6, 2, fx+12, fy+12, 14);
        grad.addColorStop(0, '#f55');
        grad.addColorStop(1, '#c33');
        ctx.fillStyle = grad;
        ctx.beginPath();
        ctx.arc(fx + CELL_SIZE/2, fy + CELL_SIZE/2, CELL_SIZE/2 - 2, 0, Math.PI*2);
        ctx.fill();
        // 高光
        ctx.fillStyle = 'rgba(255,255,255,0.25)';
        ctx.beginPath();
        ctx.arc(fx + 8, fy + 8, 3, 0, Math.PI*2);
        ctx.fill();
      }

      // 蛇身
      for (let i = snake.length - 1; i >= 0; i--) {
        const seg = snake[i];
        const sx = seg.x * CELL_SIZE, sy = seg.y * CELL_SIZE;
        const pad = 1;
        const radius = 4;
        const t = i / Math.max(snake.length - 1, 1);
        // 头部亮绿 → 尾部暗绿
        const r = Math.round(30 + t * 20);
        const g = Math.round(200 - t * 80);
        const b = Math.round(80 - t * 40);
        ctx.fillStyle = `rgb(${r},${g},${b})`;

        // 圆角矩形
        const x = sx + pad, y = sy + pad;
        const w = CELL_SIZE - pad*2, h = CELL_SIZE - pad*2;
        ctx.beginPath();
        ctx.moveTo(x + radius, y);
        ctx.lineTo(x + w - radius, y);
        ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
        ctx.lineTo(x + w, y + h - radius);
        ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
        ctx.lineTo(x + radius, y + h);
        ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
        ctx.lineTo(x, y + radius);
        ctx.quadraticCurveTo(x, y, x + radius, y);
        ctx.closePath();
        ctx.fill();
      }

      // 蛇眼睛(头部)
      if (snake.length > 0 && !gameOver) {
        const head = snake[0];
        const hx = head.x * CELL_SIZE, hy = head.y * CELL_SIZE;
        ctx.fillStyle = '#fff';
        let e1x, e1y, e2x, e2y;
        if (direction.x === 1)  { e1x=hx+16; e1y=hy+6;  e2x=hx+16; e2y=hy+16; }
        else if (direction.x === -1) { e1x=hx+6;  e1y=hy+6;  e2x=hx+6;  e2y=hy+16; }
        else if (direction.y === 1)  { e1x=hx+6;  e1y=hy+16; e2x=hx+16; e2y=hy+16; }
        else                         { e1x=hx+6;  e1y=hy+6;  e2x=hx+16; e2y=hy+6; }
        ctx.beginPath(); ctx.arc(e1x, e1y, 3, 0, Math.PI*2); ctx.fill();
        ctx.beginPath(); ctx.arc(e2x, e2y, 3, 0, Math.PI*2); ctx.fill();
        ctx.fillStyle = '#111';
        ctx.beginPath(); ctx.arc(e1x, e1y, 1.5, 0, Math.PI*2); ctx.fill();
        ctx.beginPath(); ctx.arc(e2x, e2y, 1.5, 0, Math.PI*2); ctx.fill();
      }

      // 游戏结束遮罩
      if (gameOver) {
        ctx.fillStyle = 'rgba(0,0,0,0.55)';
        ctx.fillRect(0, 0, 480, 480);
        ctx.fillStyle = '#fff';
        ctx.font = 'bold 28px system-ui, sans-serif';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        const msg = win ? '🎉 你赢了!' : '💀 游戏结束';
        ctx.fillText(msg, 240, 210);
        ctx.font = '16px system-ui, sans-serif';
        ctx.fillStyle = '#aaa';
        ctx.fillText('点击「重新开始」再来一局', 240, 260);
      }
    }

    // ---- 键盘控制 ----
    function handleKey(e) {
      const key = e.key;
      if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight','w','W','s','S','a','A','d','D'].includes(key))
        e.preventDefault();

      if (gameOver) return;

      const opp = (d1, d2) => (d1.x === -d2.x && d1.y === -d2.y);

      let nd = null;
      if (key === 'ArrowUp' || key === 'w' || key === 'W')    nd = { x: 0, y: -1 };
      else if (key === 'ArrowDown' || key === 's' || key === 'S')  nd = { x: 0, y: 1 };
      else if (key === 'ArrowLeft' || key === 'a' || key === 'A')  nd = { x: -1, y: 0 };
      else if (key === 'ArrowRight' || key === 'd' || key === 'D') nd = { x: 1, y: 0 };

      if (nd && !opp(nd, direction)) nextDirection = nd;
    }

    // ---- 启动/停止循环 ----
    function startLoop() {
      stopLoop();
      timer = setInterval(tick, TICK_MS);
    }

    function stopLoop() {
      if (timer) { clearInterval(timer); timer = null; }
    }

    function restart() {
      init();
      startLoop();
    }

    // ---- 绑定事件 ----
    document.addEventListener('keydown', handleKey);
    document.getElementById('restartBtn').addEventListener('click', restart);

    // ---- 开始 ----
    init();
    startLoop();
  })();
</script>
</body>
</html>

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐