← back to Games Agentabrams

games/snake/AbramsSnake.aa

212 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Abrams Snake</title>
<style>
  :root { --bg:#0a0e14; --grid:#121821; --snake:#4ade80; --head:#a7f3d0; --food:#f472b6; --ink:#e5e7eb; }
  * { box-sizing:border-box; margin:0; padding:0; }
  html,body { height:100%; }
  body {
    background:radial-gradient(1200px 600px at 50% -10%, #16202e, var(--bg));
    color:var(--ink); font:15px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    min-height:100%; gap:10px; padding:12px; overscroll-behavior:none; touch-action:none;
  }
  h1 { font-size:14px; letter-spacing:.28em; text-transform:uppercase; color:#93c5fd; font-weight:600; }
  .hud { display:flex; gap:18px; font-variant-numeric:tabular-nums; font-size:13px; color:#9aa4b2; }
  .hud b { color:var(--ink); font-weight:600; }
  #wrap { position:relative; }
  canvas { background:var(--grid); border-radius:12px; box-shadow:0 10px 40px rgba(0,0,0,.5); display:block; }
  .overlay {
    position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
    gap:10px; text-align:center; background:rgba(8,12,18,.72); border-radius:12px; backdrop-filter:blur(2px);
  }
  .overlay.hide { display:none; }
  .big { font-size:22px; font-weight:700; letter-spacing:.02em; }
  .sub { font-size:13px; color:#9aa4b2; max-width:280px; }
  .tag { font-size:11px; color:#93c5fd; letter-spacing:.2em; text-transform:uppercase; }
  button.play {
    margin-top:4px; padding:10px 22px; border:0; border-radius:999px; cursor:pointer;
    background:linear-gradient(180deg,#60a5fa,#3b82f6); color:#fff; font-weight:700; font-size:14px;
  }
  .btns { position:absolute; top:8px; right:10px; display:flex; gap:6px; }
  .btns button { width:30px; height:30px; border-radius:8px; border:1px solid #2a3441; background:#141b25; color:#cbd5e1; cursor:pointer; font-size:14px; }
  .hint { font-size:11px; color:#5b6673; }
</style>
</head>
<body>
  <h1>Abrams Snake</h1>
  <div class="hud"><span>Score <b id="score">0</b></span><span>Best <b id="best">0</b></span><span>Speed <b id="lvl">1</b></span></div>
  <div id="wrap">
    <canvas id="c" width="420" height="420"></canvas>
    <div class="btns">
      <button id="mute" title="Mute (M)">🔊</button>
      <button id="pauseBtn" title="Pause (P)">⏸</button>
    </div>
    <div class="overlay" id="ov">
      <div class="tag" id="ovTag">Ready</div>
      <div class="big" id="ovTitle">Abrams Snake</div>
      <div class="sub" id="ovSub">Eat the pink. Don't hit the walls or yourself. Arrows / WASD to steer, swipe on touch.</div>
      <button class="play" id="playBtn">Play</button>
      <div class="hint">P pause · M mute</div>
    </div>
  </div>

<script>
(function () {
  var cv = document.getElementById('c'), ctx = cv.getContext('2d');
  var N = 21, CELL = cv.width / N;               // 21x21 grid
  var scoreEl = document.getElementById('score'), bestEl = document.getElementById('best'), lvlEl = document.getElementById('lvl');
  var ov = document.getElementById('ov'), ovTag = document.getElementById('ovTag'), ovTitle = document.getElementById('ovTitle'), ovSub = document.getElementById('ovSub');
  var playBtn = document.getElementById('playBtn'), pauseBtn = document.getElementById('pauseBtn'), muteBtn = document.getElementById('mute');

  var BEST_KEY = 'abrams-snake-best';
  var best = parseInt(localStorage.getItem(BEST_KEY) || '0', 10) || 0; bestEl.textContent = best;

  var snake, dir, nextDir, food, score, alive, running, paused, muted = false;
  var stepMs, acc = 0, last = 0;

  // ---- audio (procedural, zero-dep) ----
  var actx = null;
  function beep(freq, dur, type, vol) {
    if (muted) return;
    try {
      if (!actx) actx = new (window.AudioContext || window.webkitAudioContext)();
      var o = actx.createOscillator(), g = actx.createGain();
      o.type = type || 'square'; o.frequency.value = freq;
      g.gain.value = vol || 0.05;
      o.connect(g); g.connect(actx.destination);
      var t = actx.currentTime; o.start(t);
      g.gain.exponentialRampToValueAtTime(0.0001, t + (dur || 0.09));
      o.stop(t + (dur || 0.09));
    } catch (e) {}
  }

  function reset() {
    snake = [{ x: 10, y: 10 }, { x: 9, y: 10 }, { x: 8, y: 10 }];
    dir = { x: 1, y: 0 }; nextDir = dir;
    score = 0; alive = true; stepMs = 130; acc = 0;
    placeFood(); updateHud();
  }
  function placeFood() {
    do { food = { x: (Math.random() * N) | 0, y: (Math.random() * N) | 0 }; }
    while (snake.some(function (s) { return s.x === food.x && s.y === food.y; }));
  }
  function updateHud() {
    scoreEl.textContent = score;
    lvlEl.textContent = Math.max(1, Math.min(9, 1 + ((score / 5) | 0)));
  }

  function step() {
    dir = nextDir;
    var head = { x: snake[0].x + dir.x, y: snake[0].y + dir.y };
    // wall or self collision = death
    if (head.x < 0 || head.y < 0 || head.x >= N || head.y >= N ||
        snake.some(function (s) { return s.x === head.x && s.y === head.y; })) {
      die(); return;
    }
    snake.unshift(head);
    if (head.x === food.x && head.y === food.y) {
      score++; updateHud(); beep(660, 0.08, 'square', 0.06);
      stepMs = Math.max(60, 130 - score * 4);   // speed up
      placeFood();
    } else {
      snake.pop();
    }
  }

  function die() {
    alive = false; running = false; beep(160, 0.3, 'sawtooth', 0.08);
    if (score > best) { best = score; localStorage.setItem(BEST_KEY, best); bestEl.textContent = best; }
    ovTag.textContent = score >= best && score > 0 ? 'New Best!' : 'Game Over';
    ovTitle.textContent = 'Score ' + score;
    ovSub.textContent = 'Best ' + best + '. Press Play or hit Space to go again.';
    playBtn.textContent = 'Play again';
    ov.classList.remove('hide');
  }

  function draw() {
    ctx.clearRect(0, 0, cv.width, cv.height);
    // subtle grid dots
    ctx.fillStyle = 'rgba(255,255,255,0.03)';
    for (var i = 0; i < N; i++) for (var j = 0; j < N; j++) ctx.fillRect(i * CELL + CELL / 2 - 1, j * CELL + CELL / 2 - 1, 2, 2);
    // food
    ctx.fillStyle = '#f472b6';
    roundRect(food.x * CELL + 3, food.y * CELL + 3, CELL - 6, CELL - 6, 5); ctx.fill();
    // snake
    for (var k = snake.length - 1; k >= 0; k--) {
      ctx.fillStyle = k === 0 ? '#a7f3d0' : 'rgba(74,222,128,' + (0.55 + 0.45 * (1 - k / snake.length)) + ')';
      roundRect(snake[k].x * CELL + 2, snake[k].y * CELL + 2, CELL - 4, CELL - 4, 5); ctx.fill();
    }
  }
  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 loop(ts) {
    requestAnimationFrame(loop);
    if (!running || paused) { last = ts; return; }
    if (!last) last = ts;
    acc += ts - last; last = ts;
    while (acc >= stepMs) { acc -= stepMs; if (alive) step(); }
    draw();
  }

  function start() {
    reset(); running = true; paused = false; last = 0;
    ov.classList.add('hide'); draw();
    if (actx && actx.state === 'suspended') actx.resume();
  }
  function togglePause() {
    if (!running) return;
    paused = !paused;
    pauseBtn.textContent = paused ? '▶' : '⏸';
    if (paused) { ovTag.textContent = 'Paused'; ovTitle.textContent = 'Paused'; ovSub.textContent = 'Press P to resume.'; playBtn.textContent = 'Resume'; ov.classList.remove('hide'); }
    else { ov.classList.add('hide'); last = 0; }
  }

  function setDir(x, y) {
    // no 180° reversal
    if (x === -dir.x && y === -dir.y) return;
    nextDir = { x: x, y: y };
  }

  // ---- input ----
  window.addEventListener('keydown', function (e) {
    var k = e.key.toLowerCase();
    if (k === 'arrowup' || k === 'w') { setDir(0, -1); e.preventDefault(); }
    else if (k === 'arrowdown' || k === 's') { setDir(0, 1); e.preventDefault(); }
    else if (k === 'arrowleft' || k === 'a') { setDir(-1, 0); e.preventDefault(); }
    else if (k === 'arrowright' || k === 'd') { setDir(1, 0); e.preventDefault(); }
    else if (k === 'p' || k === 'escape') { togglePause(); }
    else if (k === 'm') { toggleMute(); }
    else if (k === ' ') { if (!running) start(); e.preventDefault(); }
  });
  playBtn.addEventListener('click', function () { if (paused) { togglePause(); } else { start(); } });
  pauseBtn.addEventListener('click', togglePause);
  function toggleMute() { muted = !muted; muteBtn.textContent = muted ? '🔈' : '🔊'; }
  muteBtn.addEventListener('click', toggleMute);

  // touch swipe
  var tsx = 0, tsy = 0;
  cv.addEventListener('touchstart', function (e) { var t = e.touches[0]; tsx = t.clientX; tsy = t.clientY; }, { passive: true });
  cv.addEventListener('touchend', function (e) {
    var t = e.changedTouches[0], dx = t.clientX - tsx, dy = t.clientY - tsy;
    if (Math.abs(dx) < 12 && Math.abs(dy) < 12) { if (!running) start(); return; }
    if (Math.abs(dx) > Math.abs(dy)) setDir(dx > 0 ? 1 : -1, 0); else setDir(0, dy > 0 ? 1 : -1);
  }, { passive: true });

  // pause when iframe loses focus
  window.addEventListener('blur', function () { if (running && !paused) togglePause(); });

  reset(); draw();
  requestAnimationFrame(loop);
})();
</script>
</body>
</html>