DEV Community

Cover image for Classic Snake and Tank Wars!
Bryan Hamilton
Bryan Hamilton

Posted on

Classic Snake and Tank Wars!

I wanted a couple of small games for my portfolio that anyone could play in ten seconds, so I rebuilt two classics: Snake and Tank Wars. Everything lives in a single index.html, written in plain JavaScript on a , with no framework, no bundler and no dependencies.

Play it: bryanhamilton.info/arcade.html

Here are the parts that were more interesting to build than I expected.

Tank Wars
Terrain from three sine waves

The ground is a height map: one y value per pixel column. I add three sine waves of different frequency, then raise the middle so there's always a hill between the two tanks and you can't fire straight across.

js
for (let i = 0; i < W; i++) {
const mid = 1 - Math.abs(i - W / 2) / (W / 2); // 1 in the center, 0 at the edges
h[i] = clamp(
300 - mid * 40
+ 40 * Math.sin(i * .009 + p[0])
+ 22 * Math.sin(i * .025 + p[1])
+ 9 * Math.sin(i * .07 + p[2]),
150, 410
);
}

The three phases p are random, so every battle gets a different map.

Shells, gravity and wind

Each frame the shell gets wind added to its horizontal velocity and gravity added to its vertical velocity, then moves. That's the whole physics model:

js
s.vx += wind; s.vy += G;
s.x += s.vx; s.y += s.vy;

Wind is re-rolled every turn, which is what makes a "perfect" shot from last turn a miss this turn. The animation loop runs three of these steps per frame so shells feel quick without changing the math.

Craters are just a height-map edit

Because the terrain is an array, destruction is cheap. When a shell lands, I carve a circle out of every column it touches:

js
for (let i = Math.max(0, Math.floor(px - R)); i < Math.min(W, px + R); i++) {
const dx = i - px;
const s = Math.sqrt(Math.max(0, R * R - dx * dx));
if (py - s < h[i]) h[i] = clamp(Math.max(h[i], py + s), 0, H);
}

Tanks don't store their own y. They look up h[x] whenever they're drawn, so when the ground disappears under one, it simply drops into the crater with no extra code. Damage falls off with distance from the blast center, so a near miss still hurts.

A CPU opponent that checks its work

I didn't want to hand-tune an AI. Instead the CPU guesses: it picks 260 random angle and power pairs, simulates each shot against the current terrain and wind, and keeps whichever lands closest to you. Then it adds a few degrees of random error so it isn't a perfect shot every time.

js
for (let i = 0; i < 260; i++) {
const a = 20 + Math.random() * 140, p = 20 + Math.random() * 80;
const r = sim(a, p, me.x, gy(me) - 12); // where would this land?
if (!r) continue;
const d = Math.hypot(r.x - foe.x, r.y - (gy(foe) - 6));
if (d < bestDist) { bestDist = d; best = { a, p }; }
}

Because it uses the same sim function as the real physics, it accounts for wind and craters automatically, and difficulty is a single number: how much noise you add afterwards.

Snake
The input queue

The classic Snake bug: you press up then left quickly, both land between two game ticks, and the snake turns back into itself. The fix is to queue turns and apply one per tick, rejecting any turn that reverses or repeats the last queued direction:

js
function turn(d) {
const v = { u: [0, -1], d: [0, 1], l: [-1, 0], r: [1, 0] }[d];
const last = queue.length ? queue[queue.length - 1] : dir;
if ((v[0] === -last.x && v[1] === -last.y) || (v[0] === last.x && v[1] === last.y)) return;
queue.push({ x: v[0], y: v[1] });
}

Fast double-turns now work, and reversing into yourself isn't possible.

Speed, storage and touch
Speed curve: the delay starts at 125 ms and drops 3 ms per point, with a 55 ms floor so it stays playable. I used a setTimeout chain instead of setInterval so the delay can change on every step.

Best score: saved in localStorage inside a try/catch. If a browser blocks storage (private modes, some embeds), the game still runs and just skips saving.

Touch: swipes on the canvas steer, a tap restarts after game over, and an on-screen d-pad appears only on devices without a precise pointer.

Tab switching: the game pauses when you switch to Tank Wars, so you don't come back to a dead snake.
Small things that paid off

One file, zero dependencies. It's easy to host, easy to embed in an iframe, and nothing can break because a package moved.

A tiny CI check. A standard-library Python script verifies the meta tags, parses the JSON-LD, confirms the share image is a 1200×630 PNG, checks that every element ID the script looks up exists, and runs node --check on the inline script. It's about 100 lines and has already caught typos.

Colors as CSS variables. The page matches my portfolio's palette because the tokens live in one place at the top of the stylesheet.

What I'd do next
Different weapons in Tank Wars, such as a shell that splits or one that digs.

Online two-player, which would mean moving the game state off the client.

A touch-friendlier aiming control, maybe drag-to-aim instead of sliders.

The code is MIT-licensed, so take it, break it and make it yours. If you have ideas for the CPU or the physics, I'd like to hear them in the comments or as issues on the repo.

Top comments (0)