TinyGames · how it works
A warehouse puzzle. Push the crates onto the marked squares. You can only ever push, never pull, so one careless shove can end a level — which is why undo is a main button and not a menu item.
Open index.html. No build step, no dependencies, no images.
stow/
index.html markup
style.css the frame around the canvas
js/engine.js copied unchanged from Overdrive - the twelfth game on it
js/sokoban.js the rules AND a solver - no pixels, no engine, no DOM
js/stow.js the playable layer
test-sokoban.js node test-sokoban.js
A push puzzle fails silently. An unsolvable level does not crash and does not look wrong — it just makes somebody play for twenty minutes and conclude they are stupid.
So the solver lives beside the rules, and the test suite runs a breadth-first search over every shipped level. It proves each one is winnable, reports the shortest possible solution, and then replays that solution move by move through the ordinary game rules to confirm it really wins:
$ node test-sokoban.js
solving every level:
level 1 par 1 moves 3 positions 0ms replay wins
level 2 par 8 moves 515 positions 2ms replay wins
level 3 par 22 moves 2517 positions 6ms replay wins
level 4 par 9 moves 533 positions 1ms replay wins
level 5 par 33 moves 2348 positions 2ms replay wins
level 6 par 16 moves 1729 positions 2ms replay wins
level 7 par 5 moves 37 positions 0ms replay wins
level 8 par 6 moves 79 positions 0ms replay wins
56 passed, 0 failed
An unsolvable level cannot be shipped, because the suite refuses to go green.
That is not hypothetical. Level 6 shipped unsolvable in its first draft and this test caught it: both goals sat in the same column, so the second crate could only ever be pushed onto a square the first one was already standing on. It is redrawn now so each crate has its own line of approach. Nothing about playing it would have told me — it looks perfectly reasonable.
The same solver runs in the shipped game. Par appears in the HUD, and it is calculated on load rather than typed into a table beside the level — so it cannot drift away from the level it describes. Edit a level and its par edits itself.
The Hint button rebuilds your current position as a fresh puzzle, solves that, and takes one move of the answer. So it works from wherever you have got to, including positions the level designer never imagined — and if there is no way home from where you are standing, it says so instead of pretending.
Sokoban is PSPACE-complete, which is why the levels here are deliberately small. The largest takes 2,500 positions and six milliseconds.
A crate shoved into a corner can never come out — only the player can push, and there is nowhere to stand behind it. The game spots that immediately and says stuck rather than letting you play on in a position that is already lost.
The same check prunes the search. That is not merely a speed-up: without it the solver wanders enormous regions of hopeless positions on levels a person would abandon in seconds.
The tests hold it to three cases: a crate in a corner is dead, a crate against a single wall is not, and a crate in a corner that is a goal is exactly right.
Not approximately right — exact. The test makes sixty muddled moves through a real level, undoes all of them, and asserts the position encodes identically to the start, counters included.
#########
# #
# ### #
# $. .$ #
# ### #
# @ #
#########
# wall, $ crate, . goal, @ you, * a crate already home, + you standing on a goal.
Anyone can add one to LEVELS in sokoban.js — and the test suite will immediately tell them
whether it can be solved, and in how few moves.
The suite also checks each level is sealed in by walls, has as many crates as goals, and survives being written back out to ASCII and read in again.
| Swipe, or ← ↑ ↓ → / WASD | move |
| Z | undo |
| R | reset the level |
| H | one move of a real solution |
| N | next level |
A crate that is home turns green and gets a tick, because "done" is the one thing a player tracks the whole time and colour alone is a poor way to say it.
Advertisement