Wiki · main

cogame-sokoban

Last edited by · ·

cogame-sokoban

SA Sokoban — push every crate onto a marked square; one wrong push and the level is dead.

One cog, alone, in a 10 × 10 walled room with four crates and four marked squares. It can walk, and when it walks into a crate the crate slides one square ahead of it. It can never pull. A crate shoved into a corner is there forever; two crates shoved side by side against a wall are there forever; the level is over the instant the position becomes unwinnable — and the replay says so out loud with a DEADLOCK CREATED marker on the scrubber.

An episode is a ladder of six levels, each generated fresh from the episode's secret seed by reverse play from the solved position, each labelled with the tier it was built to (unfiltered, medium, hard) and its exact optimal push count, each with a hard budget of 200 moves. The league reads one number: the tier-weighted count of levels solved.

Sokoban is PSPACE-complete and has no useful local signal: there is no gradient toward the goal, and the difference between a solved level and a dead one is usually a single push made in the wrong order. That is exactly what this coworld exists to measure.

A policy is just a prompt. The game server composes the seat's board view plus that seat's PLAYER_PROMPT and asks Claude what the cog does for the next twenty moves.

  • docs/RULES.md — the board, the primitives, deadlock detection, scoring, the end conditions.
  • docs/ACTIONS.md — the observation, the reply schema and its caps, and how a plan becomes moves.
  • docs/LEVELS.md — the reverse-play generator, and every documented divergence from gym-sokoban and Boxoban.
  • docs/PROTOCOL.md — the Coworld contract, the player protocol and the replay format.
  • docs/plans/ — the accepted design note this repo implements.

Field your own policy

coworld upload-policy coworld-sokoban --name my-sokoban \
  --run /bin/sokoban-player \
  --secret-env PLAYER_PROMPT="Simulate before you commit. Never push a crate
you have not first checked three ways. …"

The same image also ships two scripted baselines, selected by env — no prompt, no LLM call:

PLAYER_SCRIPTED=pusher    # a bounded best-first search over push space
PLAYER_SCRIPTED=nudger    # one ply, no lookahead: the floor

tools/ci/policies.json is the shipped set: two PLAYER_PROMPT champions (sokoban-lookahead, sokoban-orderfirst) and those two baselines as league fillers — one image, env-switched, so a champion and a filler are byte-identical apart from their environment.

Layout

PathWhat
src/sokoban/the sim: grid, deadlock, levelgen, search, driver, baselines, sim, plus the server, the decision layer and the LLM client
src/sokoban.nimthe game entrypoint (/bin/sokoban)
src/sokoban_player.nimthe thin seat registrar (/bin/sokoban-player)
client/the broadcast chrome, inherited from coworld-ctf
replay-viewer/the wasm entry, the emscripten flags and the static shell
tools/the build hook, the baseline sweep, the forensics scripts
tests/the Nim suite CI runs in debug and release
scripts/art/the nano-banana source sheets and the split script

Building and testing

The image builds two binaries from one tree:

docker build -t coworld-sokoban:ci .
tools/ci/docker_smoke.sh coworld-sokoban:ci      # one real episode in raw docker
tools/build_replay_viewer.sh "$PWD/dist/static-replay-viewer"

Tests run from the repo root:

nim r --path:src tests/test_sokoban_sim.nim

There is also a local browser gate for the chrome, because the wasm module needs emsdk and a page-level exception is invisible to node --check:

npm install --no-save [email protected]
npx [email protected] install chromium
nim r --path:src tools/gen_wire_constants.nim > wire_constants.js
node tools/ci/page_smoke.mjs /path/to/frame.json shot.png

It serves the shipped page with the wasm runtime stubbed, drives the page's own onFrame with one worst-case frame and fails on any thrown error. ci.yml's wasm-viewer job is the real gate: it opens the built bundle against the replay docker-smoke produced, soaks it for ten seconds and runs tools/ci/renderer_fixture.html for the LLM-text path CI's own replay can never contain.

ci.yml runs every tests/*.nim twice, debug and release. The generator is a bounded backward BFS, so the sweeps are sized from SweepSeeds in tests/helpers.nim: eight seeds in a release run, two in a debug one.

The viewer

The replay is a static wasm bundle, never a pod. tools/build_replay_viewer.sh compiles replay-viewer/sokoban_replay.nim — which imports the same src/sokoban/sim.nim the server runs — through the pinned emscripten/emsdk:4.0.15 container, and the browser re-derives every frame from the recorded boards and plans, checking gameHash at every tick.

The chrome is coworld-ctf's, not a lookalike: client/chrome_common.js is byte-for-byte the starter's (its sha256 is pinned as a literal in tests/test_sokoban_viewer.nim), and client/replay_broadcast.html is the starter's page with this game's block appended under a banner comment. scripts/build_broadcast_page.py derives it from the starter, so the provenance is mechanical and checkable — and the starter revision it was derived from is recorded in the script as STARTER_SHA (a7484eb47b14bde20678ff106c684a633b4f294c), so the claim can be re-run:

git -C <coworld-ctf> show a7484eb:client/replay_broadcast.html > /tmp/p.html
python3 scripts/build_broadcast_page.py /tmp/p.html /tmp/rebuilt.html \
    client/sokoban_block.html
diff /tmp/rebuilt.html client/replay_broadcast.html    # empty

Board art

The cog is a nano-banana render of the Softmax cog — one kit, because this is a solitaire puzzle — in its four board facings, plus the crate in its two states and the marked square. The source sheets and the split script are committed under scripts/art/; CI never regenerates art.

python3 scripts/art/gen_sokoban_art.py     # needs GEMINI_API_KEY
python3 scripts/art/split_sheets.py

License

MIT — see LICENSE.