mysite / game.html
11056 字节
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>贪吃蛇小游戏</title>
<style>
  :root{
    --bg:#0b1220; --panel:#111c30; --line:rgba(148,163,184,.16);
    --txt:#e2e8f0; --muted:#94a3b8; --accent:#34d399; --food:#fb7185;
  }
  *{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
  html,body{margin:0;padding:0;}
  body{
    min-height:100vh; background:radial-gradient(1200px 600px at 50% -10%,#16233d,#0b1220 60%);
    color:var(--txt); font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
    display:flex; flex-direction:column; align-items:center; justify-content:flex-start;
    padding:16px 14px 28px; gap:12px;
  }
  h1{font-size:20px; margin:6px 0 0; letter-spacing:.5px; font-weight:600;}
  .hud{
    width:100%; max-width:460px; display:flex; align-items:center; justify-content:space-between;
    gap:10px; font-size:15px; color:var(--muted);
  }
  .hud b{color:var(--txt); font-size:18px; font-variant-numeric:tabular-nums;}
  .board{
    position:relative; width:min(92vw,440px); aspect-ratio:1/1;
    border-radius:16px; overflow:hidden; background:#0f172a;
    box-shadow:0 18px 40px rgba(0,0,0,.45), inset 0 0 0 1px var(--line);
    touch-action:none;
  }
  canvas{display:block; width:100%; height:100%;}
  .overlay{
    position:absolute; inset:0; display:flex; flex-direction:column;
    align-items:center; justify-content:center; gap:12px; text-align:center;
    background:rgba(9,14,26,.82); backdrop-filter:blur(3px); padding:20px;
    transition:opacity .15s ease;
  }
  .overlay.hidden{display:none;}
  .overlay h2{margin:0; font-size:22px; font-weight:600;}
  .overlay p{margin:0; font-size:14px; color:var(--muted); line-height:1.6; max-width:280px;}
  button{
    font:inherit; color:#04231a; background:var(--accent); border:0; border-radius:12px;
    padding:11px 22px; font-weight:600; cursor:pointer; transition:transform .08s ease, filter .15s ease;
  }
  button:active{transform:scale(.97);}
  button.ghost{background:#1e293b; color:var(--txt);}
  .pad{
    display:grid; grid-template-columns:repeat(3,60px); grid-template-rows:repeat(2,54px);
    gap:8px; margin-top:2px;
  }
  .pad button{
    background:#1b2740; color:var(--txt); border:1px solid var(--line);
    border-radius:12px; font-size:20px; padding:0; display:flex; align-items:center; justify-content:center;
  }
  .pad button:active{background:#243455;}
  .pad .up{grid-column:2; grid-row:1;}
  .pad .left{grid-column:1; grid-row:2;}
  .pad .down{grid-column:2; grid-row:2;}
  .pad .right{grid-column:3; grid-row:2;}
  .hint{margin:2px 0 0; font-size:12.5px; color:var(--muted); text-align:center; line-height:1.7; max-width:440px;}
  .back{font-size:13px; color:var(--muted); text-decoration:none;}
  .back:hover{color:var(--txt);}
</style>
</head>
<body>
  <h1>贪吃蛇</h1>
  <div class="hud">
    <span>得分 <b id="score">0</b></span>
    <span>最高 <b id="best">0</b></span>
  </div>

  <div class="board" id="board">
    <canvas id="cv"></canvas>
    <div class="overlay" id="overlay">
      <h2 id="ovTitle">贪吃蛇</h2>
      <p id="ovText">用方向键或滑动控制蛇,吃到食物得分,撞墙或撞到自己就结束。</p>
      <button id="startBtn">开始游戏</button>
    </div>
  </div>

  <div class="pad" id="pad">
    <button class="up"    data-dir="up"    aria-label="上">▲</button>
    <button class="left"  data-dir="left"  aria-label="左">◀</button>
    <button class="down"  data-dir="down"  aria-label="下">▼</button>
    <button class="right" data-dir="right" aria-label="右">▶</button>
  </div>

  <p class="hint">键盘:方向键 / WASD 移动,空格暂停。手机:在棋盘上滑动,或点下方方向键。</p>
  <a class="back" href="index.html">← 返回首页</a>

<script>
(function () {
  'use strict';

  var COLS = 20, ROWS = 20;
  var cv = document.getElementById('cv');
  var ctx = cv.getContext('2d');
  var board = document.getElementById('board');
  var scoreEl = document.getElementById('score');
  var bestEl = document.getElementById('best');
  var overlay = document.getElementById('overlay');
  var ovTitle = document.getElementById('ovTitle');
  var ovText = document.getElementById('ovText');
  var startBtn = document.getElementById('startBtn');

  var state = 'ready';           // ready | running | paused | over
  var snake, dir, nextDir, food, score;
  var best = 0;
  var moveInterval = 130;        // 每步毫秒数,随得分加快
  var acc = 0, last = 0, rafId = 0;
  var cell = 20, cssSize = 0;

  try {
    var stored = localStorage.getItem('snake_best');
    if (stored) { best = parseInt(stored, 10) || 0; }
  } catch (e) {}
  bestEl.textContent = String(best);

  function resize() {
    var rect = board.getBoundingClientRect();
    var size = Math.floor(Math.min(rect.width, rect.height));
    if (!size || size < 80) { size = 300; }
    var dpr = window.devicePixelRatio || 1;
    cv.width = Math.floor(size * dpr);
    cv.height = Math.floor(size * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    cssSize = size;
    cell = size / COLS;
    draw();
  }

  function reset() {
    snake = [{ x: 10, y: 10 }, { x: 9, y: 10 }, { x: 8, y: 10 }];
    dir = { x: 1, y: 0 };
    nextDir = { x: 1, y: 0 };
    score = 0;
    moveInterval = 130;
    acc = 0; last = 0;
    scoreEl.textContent = '0';
    placeFood();
    draw();
  }

  function placeFood() {
    var free = [];
    for (var y = 0; y < ROWS; y++) {
      for (var x = 0; x < COLS; x++) {
        var hit = false;
        for (var i = 0; i < snake.length; i++) {
          if (snake[i].x === x && snake[i].y === y) { hit = true; break; }
        }
        if (!hit) { free.push({ x: x, y: y }); }
      }
    }
    if (!free.length) { return; }
    food = free[Math.floor(Math.random() * free.length)];
  }

  function setDir(d) {
    if (d.x === -dir.x && d.y === -dir.y) { return; }        // 不能直接反向
    if (d.x === -nextDir.x && d.y === -nextDir.y) { return; } // 同一帧内不能连转两次
    nextDir = { x: d.x, y: d.y };
  }

  function step() {
    dir = nextDir;
    var head = { x: snake[0].x + dir.x, y: snake[0].y + dir.y };

    if (head.x < 0 || head.x >= COLS || head.y < 0 || head.y >= ROWS) { gameOver(); return; }
    for (var i = 0; i < snake.length - 1; i++) {
      if (snake[i].x === head.x && snake[i].y === head.y) { gameOver(); return; }
    }

    snake.unshift(head);
    if (head.x === food.x && head.y === food.y) {
      score++;
      scoreEl.textContent = String(score);
      if (score > best) {
        best = score;
        bestEl.textContent = String(best);
        try { localStorage.setItem('snake_best', String(best)); } catch (e) {}
      }
      moveInterval = Math.max(60, 130 - score * 4);
      placeFood();
    } else {
      snake.pop();
    }
  }

  function loop(ts) {
    if (state !== 'running') { return; }
    if (!last) { last = ts; }
    var dt = ts - last;
    last = ts;
    acc += dt;
    var guard = 0;
    while (acc >= moveInterval && state === 'running') {
      acc -= moveInterval;
      step();
      guard++;
      if (guard > 8) { acc = 0; break; }
    }
    draw();
    if (state === 'running') { rafId = requestAnimationFrame(loop); }
  }

  function showOverlay(title, text, btn) {
    ovTitle.textContent = title;
    ovText.textContent = text;
    startBtn.textContent = btn;
    overlay.classList.remove('hidden');
  }
  function hideOverlay() { overlay.classList.add('hidden'); }

  function startGame() {
    reset();
    state = 'running';
    hideOverlay();
    last = 0; acc = 0;
    rafId = requestAnimationFrame(loop);
  }
  function pauseGame() {
    if (state !== 'running') { return; }
    state = 'paused';
    if (rafId) { cancelAnimationFrame(rafId); }
    showOverlay('已暂停', '当前得分 ' + score + ',按空格或点下面的按钮继续。', '继续');
  }
  function resumeGame() {
    if (state !== 'paused') { return; }
    state = 'running';
    hideOverlay();
    last = 0; acc = 0;
    rafId = requestAnimationFrame(loop);
  }
  function gameOver() {
    state = 'over';
    if (rafId) { cancelAnimationFrame(rafId); }
    draw();
    showOverlay('游戏结束', '本局得分 ' + score + ',最高 ' + best + '。', '再玩一次');
  }

  startBtn.addEventListener('click', function () {
    if (state === 'paused') { resumeGame(); }
    else { startGame(); }
  });

  document.addEventListener('keydown', function (e) {
    var k = e.key;
    var d = null;
    if (k === 'ArrowUp' || k === 'w' || k === 'W') { d = { x: 0, y: -1 }; }
    else if (k === 'ArrowDown' || k === 's' || k === 'S') { d = { x: 0, y: 1 }; }
    else if (k === 'ArrowLeft' || k === 'a' || k === 'A') { d = { x: -1, y: 0 }; }
    else if (k === 'ArrowRight' || k === 'd' || k === 'D') { d = { x: 1, y: 0 }; }
    else if (k === ' ' || k === 'Spacebar') {
      e.preventDefault();
      if (state === 'running') { pauseGame(); }
      else if (state === 'paused') { resumeGame(); }
      else { startGame(); }
      return;
    }
    if (d) {
      if (k.indexOf('Arrow') === 0) { e.preventDefault(); }
      if (state === 'ready' || state === 'over') { startGame(); }
      if (state === 'running') { setDir(d); }
    }
  });

  var DIRS = { up: { x: 0, y: -1 }, down: { x: 0, y: 1 }, left: { x: -1, y: 0 }, right: { x: 1, y: 0 } };
  Array.prototype.forEach.call(document.querySelectorAll('.pad button'), function (btn) {
    btn.addEventListener('click', function (e) {
      e.preventDefault();
      var d = DIRS[btn.getAttribute('data-dir')];
      if (!d) { return; }
      if (state === 'ready' || state === 'over') { startGame(); }
      if (state === 'running') { setDir(d); }
    });
  });

  var touchStart = null;
  board.addEventListener('touchstart', function (e) {
    if (e.touches.length === 1) {
      touchStart = { x: e.touches[0].clientX, y: e.touches[0].clientY };
    }
  }, { passive: true });
  board.addEventListener('touchmove', function (e) { e.preventDefault(); }, { passive: false });
  board.addEventListener('touchend', function (e) {
    if (!touchStart) { return; }
    var t = e.changedTouches[0];
    var dx = t.clientX - touchStart.x;
    var dy = t.clientY - touchStart.y;
    touchStart = null;
    var adx = Math.abs(dx), ady = Math.abs(dy);
    if (Math.max(adx, ady) < 24) { return; }
    if (state === 'ready' || state === 'over') { startGame(); }
    if (state === 'running') {
      if (adx > ady) { setDir({ x: dx > 0 ? 1 : -1, y: 0 }); }
      else { setDir({ x: 0, y: dy > 0 ? 1 : -1 }); }
    }
  }, { passive: true });

  window.addEventListener('resize', resize);
  window.addEventListener('orientationchange', function () { setTimeout(resize, 200); });

  reset();
  resize();
  showOverlay('贪吃蛇', '用方向键或滑动控制蛇,吃到食物得分,撞墙或撞到自己就结束。', '开始游戏');
})();
</script>
</body>
</html>
扫码访问