← back to Games Agentabrams

games/tron/AbramsTron.aa

455 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 Tron</title>
<style>
  :root {
    --bg:#0a0e14; --arena:#0b1018; --grid:#122130; --gridGlow:#0e3a4a;
    --p1:#22d3ee; --p1glow:#0891b2; --p2:#f59e0b; --p2glow:#b45309;
    --ink:#e5e7eb; --dim:#8aa0b2;
  }
  * { box-sizing:border-box; margin:0; padding:0; }
  html,body { height:100%; }
  body {
    background:radial-gradient(1200px 700px at 50% -12%, #10202e, var(--bg));
    color:var(--ink); font:14px/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;
    -webkit-user-select:none; user-select:none;
  }
  h1 {
    font-size:15px; letter-spacing:.42em; text-transform:uppercase; font-weight:700;
    color:#e0f7ff; text-shadow:0 0 12px rgba(34,211,238,.55), 0 0 30px rgba(34,211,238,.25);
    padding-left:.42em;
  }
  .hud {
    display:flex; gap:16px; align-items:center; font-variant-numeric:tabular-nums;
    font-size:12px; color:var(--dim); flex-wrap:wrap; justify-content:center;
  }
  .hud .pill { display:flex; align-items:center; gap:6px; padding:3px 10px; border-radius:999px;
    border:1px solid #1c2a38; background:rgba(12,18,26,.6); }
  .hud b { color:var(--ink); font-weight:700; }
  .dot { width:9px; height:9px; border-radius:50%; }
  .dot.p1 { background:var(--p1); box-shadow:0 0 8px var(--p1); }
  .dot.p2 { background:var(--p2); box-shadow:0 0 8px var(--p2); }
  .streak b { color:#fde68a; }
  #wrap { position:relative; line-height:0; }
  canvas {
    background:var(--arena); border-radius:14px; display:block;
    box-shadow:0 0 0 1px #16354a, 0 0 40px rgba(34,211,238,.12), 0 14px 50px rgba(0,0,0,.6);
    max-width:96vw; max-height:70vh; touch-action:none;
  }
  .overlay {
    position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
    gap:10px; text-align:center; background:rgba(7,11,17,.78); border-radius:14px; backdrop-filter:blur(2px);
    padding:16px;
  }
  .overlay.hide { display:none; }
  .tag { font-size:11px; letter-spacing:.28em; text-transform:uppercase; color:var(--p1);
    text-shadow:0 0 10px rgba(34,211,238,.5); }
  .big { font-size:24px; font-weight:800; letter-spacing:.04em; }
  .big.p1c { color:#67e8f9; text-shadow:0 0 16px rgba(34,211,238,.5); }
  .big.p2c { color:#fbbf24; text-shadow:0 0 16px rgba(245,158,11,.5); }
  .sub { font-size:12.5px; color:var(--dim); max-width:300px; }
  button.play {
    margin-top:4px; padding:11px 26px; border:0; border-radius:999px; cursor:pointer;
    background:linear-gradient(180deg,#22d3ee,#0891b2); color:#031419; font-weight:800; font-size:14px;
    letter-spacing:.06em; text-transform:uppercase; box-shadow:0 6px 20px rgba(34,211,238,.35);
  }
  .btns { position:absolute; top:8px; right:10px; display:flex; gap:6px; z-index:3; }
  .btns button { width:32px; height:32px; border-radius:9px; border:1px solid #21384a;
    background:rgba(15,24,34,.85); color:#bfe6f2; cursor:pointer; font-size:15px; }
  .hint { font-size:11px; color:#5b6673; }
  /* touch turn buttons */
  .turns { display:none; gap:14px; margin-top:2px; }
  .turns button {
    width:74px; height:52px; border-radius:14px; border:1px solid #21384a;
    background:rgba(15,24,34,.9); color:var(--p1); font-size:24px; cursor:pointer;
    box-shadow:0 0 14px rgba(34,211,238,.15);
  }
  .turns button:active { background:rgba(34,211,238,.18); }
  body.touch .turns { display:flex; }
  body.touch .hint { display:none; }
</style>
</head>
<body>
  <h1>Abrams Tron</h1>
  <div class="hud">
    <span class="pill"><i class="dot p1"></i>YOU <b id="s1">0</b></span>
    <span class="pill"><i class="dot p2"></i>CPU <b id="s2">0</b></span>
    <span class="pill">Round <b id="round">1</b>/<b id="rounds">5</b></span>
    <span class="pill streak">Streak <b id="streak">0</b> · Best <b id="best">0</b></span>
  </div>
  <div id="wrap">
    <canvas id="c" width="480" height="480"></canvas>
    <div class="btns">
      <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 Tron</div>
      <div class="sub" id="ovSub">Ride the cyan cycle. Steer to box in the CPU and avoid every trail. First to 3 rounds wins the match.</div>
      <button class="play" id="playBtn">Play</button>
      <div class="hint">Arrows / WASD to turn · P pause</div>
    </div>
  </div>
  <div class="turns">
    <button id="tLeft" aria-label="Turn left">↺</button>
    <button id="tRight" aria-label="Turn right">↻</button>
  </div>

<script>
(function () {
  "use strict";
  function boot() {
    var cv = document.getElementById('c');
    if (!cv || !cv.getContext) return;
    var ctx = cv.getContext('2d');

    // ---- config ----
    var COLS = 60, ROWS = 60;      // logical grid
    var CELL = cv.width / COLS;    // pixel size per cell
    var WIN_ROUNDS = 3;            // first to N rounds wins match
    var MAX_ROUNDS = 5;
    var TICK_MS = 55;              // movement cadence

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

    var best = 0;
    try { best = parseInt(localStorage.getItem('abrams-tron-best') || '0', 10) || 0; } catch (e) {}

    // ---- state ----
    var grid;                 // occupancy: 0 empty, 1 player, 2 ai
    var player, ai;
    var score1 = 0, score2 = 0, roundNo = 1, streak = 0;
    var state = 'ready';      // ready | play | paused | roundover | matchover
    var raf = null, acc = 0, last = 0;
    var flash = 0, flashOwner = 0;

    var $ = function (id) { return document.getElementById(id); };
    var ov = $('ov');

    function newCycle(col, row, dir, id) {
      return { x:col, y:row, dir:dir, next:dir, id:id, alive:true, trail:[{x:col,y:row}] };
    }

    function resetRound() {
      grid = new Array(COLS * ROWS);
      for (var i = 0; i < grid.length; i++) grid[i] = 0;
      // player left, heading right; ai right, heading left
      var py = Math.floor(ROWS / 2);
      player = newCycle(6, py, 'right', 1);
      ai = newCycle(COLS - 7, py, 'left', 2);
      grid[idx(player.x, player.y)] = 1;
      grid[idx(ai.x, ai.y)] = 2;
      acc = 0; flash = 0;
    }

    function idx(x, y) { return y * COLS + x; }
    function inb(x, y) { return x >= 0 && y >= 0 && x < COLS && y < ROWS; }
    function free(x, y) { return inb(x, y) && grid[idx(x, y)] === 0; }

    // queue a turn (ignore reversals)
    function turn(dir) {
      if (state !== 'play') return;
      if (dir === OPP[player.dir]) return;
      player.next = dir;
    }
    // relative left/right turn for touch
    function turnRel(rel) {
      var order = ['up','right','down','left'];
      var i = order.indexOf(player.dir);
      var ni = (i + (rel > 0 ? 1 : 3)) % 4;
      turn(order[ni]);
    }

    // ---- AI ----
    // count how many free cells reachable within `limit` steps (flood) from a start cell,
    // treating a hypothetical occupied set. Cheap space estimate to avoid trapping self.
    function floodSpace(sx, sy, limit) {
      if (!free(sx, sy)) return 0;
      var seen = {};
      var q = [[sx, sy]];
      seen[idx(sx, sy)] = 1;
      var count = 0, head = 0;
      while (head < q.length && count < limit) {
        var c = q[head++]; count++;
        var nx, ny, dd = [[1,0],[-1,0],[0,1],[0,-1]];
        for (var k = 0; k < 4; k++) {
          nx = c[0] + dd[k][0]; ny = c[1] + dd[k][1];
          var id2 = idx(nx, ny);
          if (free(nx, ny) && !seen[id2]) { seen[id2] = 1; q.push([nx, ny]); }
        }
      }
      return count;
    }

    function aiChoose() {
      var options = ['up','down','left','right'];
      var cur = ai.dir;
      var best2 = null, bestScore = -1e9;
      for (var i = 0; i < options.length; i++) {
        var d = options[i];
        if (d === OPP[cur]) continue;              // no reversing
        var nx = ai.x + DIRS[d].x, ny = ai.y + DIRS[d].y;
        if (!free(nx, ny)) continue;               // immediate death
        // lookahead: space available after this move
        grid[idx(nx, ny)] = 2;                     // tentatively occupy
        var space = floodSpace(nx, ny, 260);
        grid[idx(nx, ny)] = 0;                     // undo

        var sc = space;
        // prefer keeping straight a bit (smoother), small bonus
        if (d === cur) sc += 6;
        // aggression: bias toward cutting the player off — move that reduces
        // player's open space is favored, but not at cost of our own survival.
        var pdist = Math.abs(nx - player.x) + Math.abs(ny - player.y);
        if (space > 40) sc += (30 - Math.min(pdist, 30)) * 0.6; // close in when safe
        // tiny randomness to avoid deterministic loops
        sc += Math.random() * 2;

        if (sc > bestScore) { bestScore = sc; best2 = d; }
      }
      if (best2) ai.next = best2;
      // if nothing safe, keep current dir (will crash — fair)
    }

    // ---- step ----
    function step() {
      aiChoose();
      player.dir = player.next;
      ai.dir = ai.next;

      var p1x = player.x + DIRS[player.dir].x, p1y = player.y + DIRS[player.dir].y;
      var a1x = ai.x + DIRS[ai.dir].x,       a1y = ai.y + DIRS[ai.dir].y;

      var pDead = !free(p1x, p1y);
      var aDead = !free(a1x, a1y);
      // head-on into same cell = both dead
      if (p1x === a1x && p1y === a1y) { pDead = true; aDead = true; }

      if (!pDead) { grid[idx(p1x, p1y)] = 1; player.x = p1x; player.y = p1y; player.trail.push({x:p1x,y:p1y}); }
      if (!aDead) { grid[idx(a1x, a1y)] = 2; ai.x = a1x; ai.y = a1y; ai.trail.push({x:a1x,y:a1y}); }

      if (pDead || aDead) {
        player.alive = !pDead; ai.alive = !aDead;
        endRound(pDead, aDead);
      }
    }

    function endRound(pDead, aDead) {
      var winner; // 1 player, 2 ai, 0 draw
      if (pDead && aDead) winner = 0;
      else if (pDead) winner = 2;
      else winner = 1;

      if (winner === 1) { score1++; streak++; if (streak > best) { best = streak; saveBest(); } }
      else if (winner === 2) { score2++; streak = 0; }
      // draw: no score, no streak change

      flash = 1; flashOwner = winner;
      render();

      var matchDone = score1 >= WIN_ROUNDS || score2 >= WIN_ROUNDS || roundNo >= MAX_ROUNDS;
      if (matchDone) {
        state = 'matchover';
        var youWin = score1 > score2;
        showOverlay(
          youWin ? 'Match Won' : (score1 === score2 ? 'Match Draw' : 'Match Lost'),
          youWin ? 'DERES.' : (score1 === score2 ? 'DEADLOCK.' : 'GAME OVER'),
          'Final ' + score1 + ' – ' + score2 + '.  Best win streak ' + best + '.',
          'Play Again', youWin ? 'p1c' : 'p2c'
        );
      } else {
        state = 'roundover';
        var msg = winner === 1 ? 'You boxed in the CPU.' : winner === 2 ? 'The CPU cut you off.' : 'Both cycles crashed.';
        showOverlay(
          'Round ' + roundNo + ' — ' + (winner === 1 ? 'You' : winner === 2 ? 'CPU' : 'Draw'),
          winner === 1 ? 'ROUND WON' : winner === 2 ? 'ROUND LOST' : 'DRAW',
          msg + '  ' + score1 + ' – ' + score2,
          'Next Round', winner === 1 ? 'p1c' : winner === 2 ? 'p2c' : ''
        );
      }
      updateHud();
    }

    function saveBest() { try { localStorage.setItem('abrams-tron-best', String(best)); } catch (e) {} }

    function nextRound() {
      roundNo++;
      resetRound();
      state = 'play';
      hideOverlay();
      updateHud();
    }

    function restartMatch() {
      score1 = 0; score2 = 0; roundNo = 1; streak = 0;
      resetRound();
      state = 'play';
      hideOverlay();
      updateHud();
    }

    // ---- rendering ----
    function drawGrid() {
      ctx.fillStyle = '#0b1018';
      ctx.fillRect(0, 0, cv.width, cv.height);
      ctx.strokeStyle = 'rgba(20,52,72,.55)';
      ctx.lineWidth = 1;
      ctx.beginPath();
      for (var x = 0; x <= COLS; x += 3) {
        var px = Math.round(x * CELL) + 0.5;
        ctx.moveTo(px, 0); ctx.lineTo(px, cv.height);
      }
      for (var y = 0; y <= ROWS; y += 3) {
        var py = Math.round(y * CELL) + 0.5;
        ctx.moveTo(0, py); ctx.lineTo(cv.width, py);
      }
      ctx.stroke();
      // border glow
      ctx.strokeStyle = 'rgba(34,211,238,.45)';
      ctx.lineWidth = 2;
      ctx.strokeRect(1, 1, cv.width - 2, cv.height - 2);
    }

    function drawTrail(cyc, color, glow) {
      var t = cyc.trail;
      ctx.save();
      ctx.shadowColor = glow; ctx.shadowBlur = 8;
      ctx.fillStyle = color;
      for (var i = 0; i < t.length; i++) {
        ctx.fillRect(t[i].x * CELL, t[i].y * CELL, CELL, CELL);
      }
      ctx.restore();
      // head brighter
      var h = t[t.length - 1];
      ctx.save();
      ctx.shadowColor = glow; ctx.shadowBlur = 16;
      ctx.fillStyle = '#ffffff';
      ctx.globalAlpha = 0.9;
      ctx.fillRect(h.x * CELL, h.y * CELL, CELL, CELL);
      ctx.restore();
    }

    function render() {
      drawGrid();
      drawTrail(ai, '#f59e0b', '#b45309');
      drawTrail(player, '#22d3ee', '#0891b2');
      if (flash > 0) {
        var c = flashOwner === 1 ? 'rgba(34,211,238,' : flashOwner === 2 ? 'rgba(245,158,11,' : 'rgba(200,210,220,';
        ctx.fillStyle = c + (0.18 * flash) + ')';
        ctx.fillRect(0, 0, cv.width, cv.height);
      }
    }

    // ---- loop ----
    function frame(ts) {
      raf = requestAnimationFrame(frame);
      if (!last) last = ts;
      var dt = ts - last; last = ts;
      if (flash > 0) flash = Math.max(0, flash - dt / 400);

      if (state === 'play') {
        acc += dt;
        while (acc >= TICK_MS && state === 'play') {
          acc -= TICK_MS;
          step();
        }
        render();
      } else if (flash > 0) {
        render();
      }
    }

    // ---- overlay / hud ----
    function showOverlay(tag, title, sub, btn, cls) {
      $('ovTag').textContent = tag;
      var t = $('ovTitle');
      t.textContent = title;
      t.className = 'big' + (cls ? ' ' + cls : '');
      $('ovSub').textContent = sub;
      $('playBtn').textContent = btn;
      ov.classList.remove('hide');
    }
    function hideOverlay() { ov.classList.add('hide'); }

    function updateHud() {
      $('s1').textContent = score1;
      $('s2').textContent = score2;
      $('round').textContent = roundNo;
      $('rounds').textContent = MAX_ROUNDS;
      $('streak').textContent = streak;
      $('best').textContent = best;
    }

    // ---- input ----
    function onPlayBtn() {
      if (state === 'ready' || state === 'matchover') restartMatch();
      else if (state === 'roundover') nextRound();
    }

    var keyMap = {
      ArrowUp:'up', ArrowDown:'down', ArrowLeft:'left', ArrowRight:'right',
      w:'up', s:'down', a:'left', d:'right', W:'up', S:'down', A:'left', D:'right'
    };
    window.addEventListener('keydown', function (e) {
      if (e.key === 'p' || e.key === 'P') { togglePause(); return; }
      if (e.key === 'Enter' || e.key === ' ') {
        if (state === 'ready' || state === 'roundover' || state === 'matchover') { e.preventDefault(); onPlayBtn(); }
        return;
      }
      var d = keyMap[e.key];
      if (d) {
        // prevent page scroll for arrows/space
        if (e.key.indexOf('Arrow') === 0) e.preventDefault();
        turn(d);
      }
    }, { passive:false });

    function togglePause() {
      if (state === 'play') { state = 'paused'; showOverlay('Paused', 'PAUSED', 'Press P or Play to resume.', 'Resume', ''); }
      else if (state === 'paused') { state = 'play'; hideOverlay(); }
    }

    $('playBtn').addEventListener('click', function () {
      if (state === 'paused') { state = 'play'; hideOverlay(); return; }
      onPlayBtn();
    });
    $('pauseBtn').addEventListener('click', togglePause);

    // touch turn buttons
    $('tLeft').addEventListener('click', function () { turnRel(-1); });
    $('tRight').addEventListener('click', function () { turnRel(1); });

    // tap-left-half / tap-right-half on canvas for touch
    cv.addEventListener('pointerdown', function (e) {
      if (e.pointerType === 'mouse') return; // mouse uses buttons/keys
      var r = cv.getBoundingClientRect();
      var rel = (e.clientX - r.left) < r.width / 2 ? -1 : 1;
      turnRel(rel);
      e.preventDefault();
    }, { passive:false });

    // detect touch to reveal on-screen controls
    function markTouch() { document.body.classList.add('touch'); window.removeEventListener('touchstart', markTouch); }
    window.addEventListener('touchstart', markTouch, { passive:true });
    if (('ontouchstart' in window) || (navigator.maxTouchPoints > 0)) document.body.classList.add('touch');

    // ---- init ----
    resetRound();
    updateHud();
    render();
    raf = requestAnimationFrame(frame);
  }

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
  else boot();
})();
</script>
</body>
</html>