TinyGames · how it works
A maze chase. Eat every dot. Four things want to stop you, and a power pellet turns them round and makes them edible for seven seconds.
Open index.html. No build step, no dependencies, no images.
scatter/
index.html markup
style.css the frame around the canvas
js/engine.js copied unchanged from Overdrive - the eleventh game on it
js/maze.js the maze and the four pursuers - no pixels, no engine, no DOM
js/scatter.js the playable layer
test-maze.js node test-maze.js
All four pursuers run exactly the same code. At every tile each one looks at its legal exits, throws away the one it came from, and takes whichever leaves it closest — as the crow flies — to a tile it is aiming at.
That is the whole engine. Everything that makes them feel like four different creatures lives in one line each:
| aims at | |
|---|---|
| Chaser | your tile. Relentless, and alone, easy to shake. |
| Ambusher | four tiles in front of you, so it cuts you off rather than following. |
| Flanker | takes the vector from the Chaser to two tiles ahead of you, and doubles it. |
| Drifter | you — until it gets within eight tiles, when it loses its nerve and bolts for its corner. |
The Flanker is the interesting one: its target depends on where a different ghost is standing, so the two of them close like pincers without either knowing the other exists. The test works one case out by hand — player at (8,5) facing right, Chaser at (2,3), so two-ahead is (8,7), the vector is (+6,+4), and doubling lands on (14,11) — then moves only the Chaser and asserts the Flanker's aim moves with it.
And it works. Two minutes of play, measuring how far each one stays from the player:
| average distance | time spent within 4 tiles | |
|---|---|---|
| Chaser | 7.5 tiles | 4% |
| Ambusher | 7.1 | 3% |
| Flanker | 6.6 | 3% |
| Drifter | 4.1 | 70% |
Same code, same maze, same player. The Drifter hovers because it is forever changing its mind at the eight-tile line; the other three commit.
$ node test-maze.js
68 passed, 0 failed
A ghost cannot see round corners. It compares the four tiles next to it and picks the one nearest its target in a straight line, with no regard for walls in between. That is why they can be led into the wrong branch of a junction, and it is the reason the game is beatable at all. Ties break in a fixed order — up, left, down, right — so the same situation always produces the same answer and a run can be reasoned about rather than merely survived.
The test walks every walkable tile in the maze, from every approach direction, against five different targets — over 500 cases — and checks the choice is always the one that gets closest.
None of them ever turns round except on the tick the mode changes from scatter to chase or a pellet is eaten. Those forced reversals are the game's rhythm; they are the only moment a ghost backtracks, so you learn to hear them coming. A 4,000-step simulation asserts zero unauthorised reversals, zero walls entered, and zero ghosts off the grid.
The one exception is a genuine dead end, where turning round is the only legal move — which is tested too, though the shipped maze has none.
If one dot sits in a pocket the player cannot get to, the game can never be finished, and the only symptom is somebody wandering an apparently empty maze wondering what they missed.
So the test floods out from the player's starting tile and asserts it reaches every single piece of food — and that all four ghosts start in the same connected space.
It also asserts there are no dead ends, and that caught two: a pair of one-tile pockets on the bottom row where a player would be cornered with no escape at all. The maze row above them is now open.
Everything walked into walls. Both the player and the ghosts advanced to the next tile and only then checked whether the move was legal. When it was not, the code set the progress counter to zero and stopped — but left the blocked direction in place, so the next frame walked straight in. Nearly 300 frames in a wall over forty seconds. Both now refuse to move at all until the heading is legal.
Ghosts started facing walls. They were handed a direction from a fixed list by index, and for some starting tiles that pointed at solid maze. Their opening heading is now taken from the exits that actually exist.
| Swipe | turn |
| ← ↑ ↓ → or WASD | turn |
| R | start over |
A direction pressed slightly before a junction is taken the moment it becomes legal. Without that queued turn, cornering demands frame-perfect timing and the game feels unresponsive rather than difficult.
10 a dot, 50 a pellet, and ghosts eaten under one pellet are worth 200, 400, 800,
1600. Clearing a maze is 1000 and deals it again. Three lives, best kept in
localStorage.
MAZE is seventeen lines of ASCII in maze.js, and the reachability test
applies to whatever you put there.PHASES is a list and would take a multiplier.Advertisement