Climb the gantry. Mind the drones.

Your job is to reach the glowing platform at the top of the derelict gantry. The drones are the point: leave the overlay on (press TAB to toggle) and you'll see the navigation graph drawn over the level — teal links you can walk, gold links are ladders, red are drop-offs — and each drone's current planned route picked out in bright red. Watch one decide to cross a catwalk, climb two ladders, and come for you.

Trouble with the embed? Open it full screen ↗  ·  arrows/WASD to move, up/down to climb, SPACE to fire

Get up high and the drone that was below you doesn't give up — it finds the nearest ladder and starts climbing. That single behaviour, an enemy that pursues you vertically through the level rather than just left and right, is the whole difference between this and the platformers that taught us what enemies were.

What an enemy was in 1985

Classic platformer enemies were marvels of economy and almost entirely stateless. A Goomba had one rule. A Hammer Bro had a handful. None of them had any representation of the level they lived in — they couldn't, on the hardware — so they never navigated. They executed motion. The challenge came from arranging dumb hazards cleverly, and a generation of brilliant level design grew out of working around exactly that limitation.

// fixed behaviour (1985)

Walk until a wall, then turn. No model of the level; ladders and gaps are invisible to the enemy. It cannot pursue you anywhere it isn't already heading. Difficulty is authored entirely by placement.

// navigation (this)

The level is a graph. The enemy runs A* to plan a real route to your position, then follows it — walking, climbing, dropping. It pursues you through the whole space. The level designer arranges the geometry; the enemy works out the rest.

Two systems make the drones tick. The star is navigation; riding on top is a light adaptive layer that tunes the pressure to how you're doing. Neither is a neural network — it all runs offline in one HTML file, and it's drawn on screen so you can see it work.

Turning a level into a graph

An enemy can't reason about a wall of pixels. So the first job is to distil the level into the handful of places where a movement decision happens: the top and bottom of each ladder (where you mount or dismount), and the ends of each platform (where you can drop off). Those become nodes. Then we connect them with edges that record how you'd travel between them — and that "how" is the part the old enemies never had.

rust.html — edges are typed by the move they require
// CLIMB edges: a ladder links its bottom node to its top node, both ways
top.edges.push({ to: bot.id, move: "climb", cost });
bot.edges.push({ to: top.id, move: "climb", cost });

// WALK edges: adjacent nodes on the same platform, both ways
a.edges.push({ to: b.id, move: "walk", cost });
b.edges.push({ to: a.id, move: "walk", cost });

// DROP edges: a platform edge falls onto the nearest platform below — ONE way
n.edges.push({ to: landing.id, move: "drop", cost });

The asymmetry matters: climbs go both ways, but a drop is one-way — you can fall down a gap you can't jump back up. Encoding that in the graph is what stops an enemy from planning an impossible route, and it's why the pathfinder produces movement that looks like it understands gravity.

A*: planning the route, then walking it

With a graph in hand, pursuit becomes a standard shortest-path problem. The enemy runs A* from its nearest node to the player's nearest node, using a cheap distance estimate as the heuristic, and gets back an ordered list of nodes. Then each frame it looks at the next node, reads the edge type, and does the matching thing: walk toward it, climb toward it, or drop.

rust.html — following the planned path
const edge = NAV[e.path[e.pathStep]].edges.find(ed => ed.to === tgt.id);
e.move = edge ? edge.move : "walk";

if (e.move === "climb") {
  e.climbing = true; e.vy = 0;
  e.x += Math.sign(L.x - e.x);                 // centre on the rungs
  e.y += Math.sign(dy) * CLIMB_SP;             // then climb
  if (Math.abs(dy) < CLIMB_SP + 1) advance(e);     // reached the node? next one
} else if (e.move === "drop") {
  e.vx = Math.sign(dx) * MOVE; stepPhysics(e);  // walk off and let gravity work
} else {
  e.vx = Math.sign(dx) * MOVE; stepPhysics(e);  // walk along the platform
  if (Math.abs(dx) < 6) advance(e);
}

The enemy re-plans a few times a second, so if you move, it adapts — the bright red path you see in the overlay is this route, recomputed live. A real chase from the ground floor to the top ledge comes out as something like walk → walk → climb → walk → climb → walk → climb: cross to a ladder, go up, cross again, repeat. Nobody scripted that sequence. It fell out of the graph.

The governor: pressure that tracks you

Pure navigation would make the drones relentless but the amount of pressure static. So a light governor — the same idea as a thermostat — watches a performance signal and nudges two dials: how often drones spawn, and how fast they move. Clear them easily and the gantry gets busier; struggle and it eases off. It never touches how the drones navigate, only how hard they lean, which keeps the adaptation feeling fair rather than rubber-banded.

rust.html — the difficulty governor
function tuneDifficulty() {
  const target = 0.5;
  difficulty = clamp(difficulty + (perf - target) * 0.01, 0.6, 1.8);
  const cap = Math.round(2 + difficulty * 2);          // more drones when you're winning
  if (spawnTimer-- <= 0 && enemies.length < cap) {
    spawnEnemy();
    spawnTimer = Math.round(clamp(180 - difficulty * 60, 70, 180));
  }
}

Three bugs, three different ways of catching them

A navigating enemy can fail in ways that are invisible to the eye, so the navigation is tested like the load-bearing code it is. The headline test is reachability: from a spawn on the ground, can an enemy actually path to every platform in the level? A single broken graph link is an enemy stuck forever, and you'd never reliably spot it by playing. nav.test.js asserts the whole level is reachable; harness.js then loads the real game with no browser and runs thousands of frames, confirming enemies plan routes, climb to hunt a player above them, and that difficulty stays in bounds.

What's honest to report is that the three real bugs this build threw up were each caught a different way — and that's the actual lesson about testing games.

node nav.test.js
  ok  every ladder connects two real platforms
  ok  no node has a dangling edge (all targets exist)
  ok  an enemy on the ground can reach EVERY platform
  ok  enemy can path from ground to the top goal ledge
  ok  that route actually uses ladders (climbs > 1)
       chase route: walk -> walk -> climb -> walk -> walk -> climb -> walk -> walk -> climb
  ... 9 passed, 0 failed
// the honest version

A headless harness is brilliant at the things it can measure — reachability, bounds, "does an enemy ever climb" — and useless at the things only a human notices, like a ladder that feels wrong to dismount. The win isn't "tests instead of playing." It's tests for the invisible structural failures, playtesting for feel, and a targeted regression test written the moment play turns up something, so it stays fixed.

Where to take it

The navigation is the reusable part. Because the graph is built from a plain list of platforms and ladders, you can hand it a different level and the enemies just navigate it — no AI changes. Natural extensions: add a jump edge type for gaps an enemy can leap, and the pathfinder folds it into routes for free; give drones a flow-field instead of individual A* if you want dozens of them cheaply; add line-of-sight so they only chase what they can see, and have to search when they lose you; or let the governor read a second signal, like how long you linger, to vary where they spawn rather than just how many.

The deeper version, for another day: the nav-graph is exactly the substrate a learning agent needs. Reward an enemy for cutting off your route to the exit and you'd have drones that learn to herd rather than chase — the same step from the shoot-'em-up's bandit toward something that reasons about the level over time. The graph is already the hard part; the learning sits on top of it.