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