← 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>