TinyGames · how it works

Rockfall

Boulder Dash. Dig through the earth for diamonds, reach the exit before the clock does, and do not be underneath anything heavy when it lets go.

Open index.html. No build step, no dependencies, no images — the cave is drawn.

rockfall/
  index.html      markup, and the d-pad
  style.css       the frame around the canvas
  js/cave.js      gravity, the cave generator and the arithmetic
                  - no pixels, no engine, no DOM
  js/rockfall.js  the playable layer
  test-cave.js    node test-cave.js

The whole game is one function, and it has two traps in it

settle() runs gravity for one tick. Both of the mistakes below produce exactly the same symptom — something moving further in one tick than it should — and only one of them is the famous one.

The first trap: scan order

Walk the grid top to bottom, move a falling boulder down one cell, and you meet that same boulder again further down the same pass. It falls again. And again. It reaches the floor in a single tick and the player never sees it move.

Scanning from the bottom up fixes it: a boulder that moves down lands in a row the pass has already finished with.

The second trap: the one bottom-up does not fix

A boulder resting on another boulder rolls sideways into a gap. Rolling right moves it to a higher x in the row currently being scanned — which the scan has not reached yet. It gets picked up again and falls as well. Two moves, one tick.

Bottom-up says nothing about left-to-right, so this needs its own guard: a cell moved into this tick is skipped.

Two lines of code, two different bugs, one symptom.


So the property is stated once and tested three ways

Nothing moves more than one cell per tick.

The main test builds three thousand small caves containing exactly one boulder — so there is no question of which one moved where — and measures its displacement after a single tick. It also asserts the worst case observed was one, because a test where nothing ever moves would pass just as quietly.

Then each trap is rebuilt on purpose and watched breaking it:

rebuilt mistake what the test sees
top-down scan the boulder is on the floor after one tick, not one cell down
no moved-guard the same boulder rolls right and falls, two cells of travel

Alongside: nothing is ever created or destroyed over thirty ticks of a crowded cave, nothing ends up inside the steel, and a cave that has finished falling stays where it is.

$ node test-cave.js
69 passed, 0 failed

A rule that looks like a bug

A boulder only kills what is under it if it was already falling. One that has merely come to rest on your head is furniture — you can walk out from under it.

That is the original's rule and it is why every rounded object carries a falling flag instead of the code inferring danger from "is the cell below the player". Killing on contact would make half the caves unplayable, and the test builds both situations to prove the difference is real.


What the caves promise, and what they do not

caveFor generates and rejects: a cave ships only if the exit can be dug to and there are at least as many reachable diamonds as it asks for. The quota is then set from that count rather than guessed, so need is always a promise the cave can keep.

reachable() is deliberately pessimistic — boulders count as walls — so what it approves is approved without relying on physics at all.

It is not a promise the cave is winnable. Boulder Dash is a game about a cave rearranging itself, and no cheap check can tell you whether a particular collapse will trap you. Saying otherwise would be claiming something this does not do.

What it does rule out is the failure a generator actually produces: a diamond bricked up behind bedrock, or an exit sealed off. Caves also never start with a boulder poised directly over the player, which the test checks across 320 of them.


What the tests are worth

Eight mutants. Seven broke something real; the eighth changed nothing on purpose.

mutation result
settle scans top-down caught (6 assertions)
boulders can be pushed vertically caught (4)
settle without the moved-guard caught (2)
kills on contact rather than on impact caught (2)
reachable walks through boulders caught (1)
the quota is a fixed number, not a measured one caught (1)
settle wrapped, behaviour unchanged passes, as it should

The vertical-push mutant survived the first version of the suite. The test put steel directly beyond the boulder, so it was only ever observing that the wall was in the way — a game that happily pushed boulders upward passed it. Both cases now leave somewhere for the push to go, and check the boulder did not move.


Playing it

Arrows or WASD, held rather than tapped — Rockfall is played by leaning on a direction and letting go at the right moment. On a touch screen, the same three-by-three d-pad as Stow and Crossing. Tapping the cave also steps towards the tap. R gives up the cave, Escape or P pauses.

Push boulders sideways into space; you cannot push them up, down, or into another. Fireflies patrol with their left hand on the wall, kill you on contact, and turn into diamonds if you drop a boulder on them.