← back to Games Agentabrams

games/2048/Abrams2048.aa

214 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 2048</title>
<style>
  :root { --bg:#0a0e14; --ink:#e5e7eb; --board:#141b25; --cell:#1e2735; }
  * { 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:12px; padding:14px; 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:14px; align-items:center; }
  .stat { background:var(--cell); border-radius:10px; padding:6px 14px; text-align:center; min-width:76px; }
  .stat .k { font-size:10px; letter-spacing:.15em; text-transform:uppercase; color:#8b97a7; }
  .stat .v { font-size:19px; font-weight:700; font-variant-numeric:tabular-nums; }
  .btn { padding:8px 14px; border-radius:10px; border:1px solid #2a3441; background:var(--cell); color:#cbd5e1; cursor:pointer; font-size:13px; font-weight:600; }
  #wrap { position:relative; }
  #board { background:var(--board); border-radius:14px; padding:10px; display:grid; grid-template-columns:repeat(4,1fr); gap:10px; box-shadow:0 10px 40px rgba(0,0,0,.5); }
  .tile { width:70px; height:70px; border-radius:10px; display:flex; align-items:center; justify-content:center; font-weight:800; font-size:26px; background:var(--cell); color:#e5e7eb; transition:transform .09s ease; }
  .tile.pop { animation:pop .16s ease; }
  @keyframes pop { 0%{transform:scale(.6)} 60%{transform:scale(1.08)} 100%{transform:scale(1)} }
  .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,.8); border-radius:14px; backdrop-filter:blur(2px); }
  .overlay.hide { display:none; }
  .big { font-size:24px; font-weight:800; }
  .sub { font-size:13px; color:#9aa4b2; max-width:260px; }
  .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; }
  .hint { font-size:11px; color:#5b6673; }
</style>
</head>
<body>
  <h1>Abrams 2048</h1>
  <div class="hud">
    <div class="stat"><div class="k">Score</div><div class="v" id="score">0</div></div>
    <div class="stat"><div class="k">Best</div><div class="v" id="best">0</div></div>
    <button class="btn" id="newBtn">New</button>
  </div>
  <div id="wrap">
    <div id="board"></div>
    <div class="overlay hide" id="ov">
      <div class="tag" id="ovTag">Nice</div>
      <div class="big" id="ovTitle">2048!</div>
      <div class="sub" id="ovSub"></div>
      <button class="play" id="playBtn">New game</button>
      <div class="hint">Arrows / WASD · swipe on touch</div>
    </div>
  </div>

<script>
(function () {
  var boardEl = document.getElementById('board');
  var scoreEl = document.getElementById('score'), bestEl = document.getElementById('best');
  var ov = document.getElementById('ov'), ovTag = document.getElementById('ovTag'), ovTitle = document.getElementById('ovTitle'), ovSub = document.getElementById('ovSub');
  var playBtn = document.getElementById('playBtn'), newBtn = document.getElementById('newBtn');

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

  var TILE_COLORS = {
    2:'#334155', 4:'#3f5170', 8:'#5b7fb8', 16:'#38bdf8', 32:'#22c1a6', 64:'#4ade80',
    128:'#facc15', 256:'#fb923c', 512:'#f472b6', 1024:'#e879f9', 2048:'#a78bfa', 4096:'#fca5a5'
  };
  var grid, score, won, ended;
  var actx = null;
  function beep(freq, dur, vol) {
    try {
      if (!actx) actx = new (window.AudioContext || window.webkitAudioContext)();
      var o = actx.createOscillator(), g = actx.createGain();
      o.type = 'sine'; o.frequency.value = freq; g.gain.value = vol || 0.04;
      o.connect(g); g.connect(actx.destination);
      var t = actx.currentTime; o.start(t); g.gain.exponentialRampToValueAtTime(0.0001, t + (dur || 0.08)); o.stop(t + (dur || 0.08));
    } catch (e) {}
  }

  function empty() { var g = []; for (var i = 0; i < 16; i++) g.push(0); return g; }
  function spawn() {
    var free = [];
    for (var i = 0; i < 16; i++) if (grid[i] === 0) free.push(i);
    if (!free.length) return null;
    var idx = free[(Math.random() * free.length) | 0];
    grid[idx] = Math.random() < 0.9 ? 2 : 4;
    return idx;
  }
  function newGame() {
    grid = empty(); score = 0; won = false; ended = false;
    spawn(); spawn(); ov.classList.add('hide'); render([], -1);
    updateScore();
    if (actx && actx.state === 'suspended') actx.resume();
  }
  function updateScore() {
    scoreEl.textContent = score;
    if (score > best) { best = score; localStorage.setItem(BEST_KEY, best); bestEl.textContent = best; }
  }

  // slide+merge one row (array of 4) toward index 0; returns {row, gained, moved, merges:[positions]}
  function collapse(line) {
    var vals = line.filter(function (v) { return v !== 0; });
    var out = [], gained = 0, mergedAt = [];
    for (var i = 0; i < vals.length; i++) {
      if (i + 1 < vals.length && vals[i] === vals[i + 1]) {
        var m = vals[i] * 2; out.push(m); gained += m; mergedAt.push(out.length - 1); i++;
      } else out.push(vals[i]);
    }
    while (out.length < 4) out.push(0);
    var moved = out.some(function (v, k) { return v !== line[k]; });
    return { row: out, gained: gained, moved: moved, merges: mergedAt };
  }

  // dir: 'l','r','u','d'. Returns true if anything moved.
  function move(dir) {
    if (ended) return false;
    var moved = false, gained = 0, mergedCells = [];
    function get(r, c) { return grid[r * 4 + c]; }
    function set(r, c, v) { grid[r * 4 + c] = v; }

    for (var k = 0; k < 4; k++) {
      var line = [];
      for (var i = 0; i < 4; i++) {
        if (dir === 'l') line.push(get(k, i));
        else if (dir === 'r') line.push(get(k, 3 - i));
        else if (dir === 'u') line.push(get(i, k));
        else line.push(get(3 - i, k));
      }
      var res = collapse(line);
      if (res.moved) moved = true;
      gained += res.gained;
      for (var i2 = 0; i2 < 4; i2++) {
        var v = res.row[i2], r, c;
        if (dir === 'l') { r = k; c = i2; } else if (dir === 'r') { r = k; c = 3 - i2; }
        else if (dir === 'u') { r = i2; c = k; } else { r = 3 - i2; c = k; }
        set(r, c, v);
        if (res.merges.indexOf(i2) >= 0) mergedCells.push(r * 4 + c);
      }
    }
    if (moved) {
      score += gained; updateScore();
      var spawnIdx = spawn();
      var popCells = mergedCells.slice(); if (spawnIdx !== null) popCells.push(spawnIdx);
      render(popCells, -1);
      if (gained > 0) beep(400 + Math.min(600, gained), 0.07, 0.05);
      if (!won && grid.indexOf(2048) >= 0) { won = true; finish('2048!', 'You hit 2048 — keep going for a higher score.', 'Keep playing'); }
      else if (isStuck()) finish('Game Over', 'No moves left. Score ' + score + ', best ' + best + '.', 'New game');
    }
    return moved;
  }

  function isStuck() {
    if (grid.indexOf(0) >= 0) return false;
    for (var r = 0; r < 4; r++) for (var c = 0; c < 4; c++) {
      var v = grid[r * 4 + c];
      if (c < 3 && grid[r * 4 + c + 1] === v) return false;
      if (r < 3 && grid[(r + 1) * 4 + c] === v) return false;
    }
    return true;
  }

  function finish(title, sub, btn) {
    if (title === 'Game Over') ended = true;
    ovTag.textContent = title === 'Game Over' ? 'Done' : 'Milestone';
    ovTitle.textContent = title; ovSub.textContent = sub; playBtn.textContent = btn;
    ov.classList.remove('hide');
    beep(title === 'Game Over' ? 180 : 880, 0.25, 0.07);
  }

  function render(popCells, _) {
    boardEl.innerHTML = '';
    for (var i = 0; i < 16; i++) {
      var v = grid[i], d = document.createElement('div');
      d.className = 'tile' + (popCells && popCells.indexOf(i) >= 0 ? ' pop' : '');
      if (v) {
        d.textContent = v;
        d.style.background = TILE_COLORS[v] || '#a78bfa';
        d.style.color = v <= 4 ? '#cbd5e1' : '#0a0e14';
        d.style.fontSize = v >= 1024 ? '20px' : v >= 128 ? '23px' : '26px';
      }
      boardEl.appendChild(d);
    }
  }

  // input
  window.addEventListener('keydown', function (e) {
    var k = e.key.toLowerCase(), d = null;
    if (k === 'arrowleft' || k === 'a') d = 'l';
    else if (k === 'arrowright' || k === 'd') d = 'r';
    else if (k === 'arrowup' || k === 'w') d = 'u';
    else if (k === 'arrowdown' || k === 's') d = 'd';
    if (d) { move(d); e.preventDefault(); }
  });
  var tsx = 0, tsy = 0;
  boardEl.addEventListener('touchstart', function (e) { var t = e.touches[0]; tsx = t.clientX; tsy = t.clientY; }, { passive: true });
  boardEl.addEventListener('touchend', function (e) {
    var t = e.changedTouches[0], dx = t.clientX - tsx, dy = t.clientY - tsy;
    if (Math.abs(dx) < 20 && Math.abs(dy) < 20) return;
    if (Math.abs(dx) > Math.abs(dy)) move(dx > 0 ? 'r' : 'l'); else move(dy > 0 ? 'd' : 'u');
  }, { passive: true });

  playBtn.addEventListener('click', function () {
    if (won && !ended && playBtn.textContent === 'Keep playing') { ov.classList.add('hide'); return; }
    newGame();
  });
  newBtn.addEventListener('click', newGame);

  newGame();
})();
</script>
</body>
</html>