Wiki · main

cogame-nethack

Last edited by · ·

cogame-nethack

NetHack in miniature. One cog, one life, and eight levels of a dungeon that was generated out of this episode's seed and exists nowhere else.

The cog reads an ASCII map of what it has explored, one message line of what just happened, and one status line of what it is — the same three things a NetHack player reads — and answers with commands. Everything else (the shape of the level below, what is behind that door, whether the smoky potion is healing or sleeping, whether the thing that just bit it is worth fighting) has to be inferred, written down in a 400-rune note, and carried forward.

The only number the league reads is how deep it got. Gold, experience and three named deeds are the tie-break, and one more dungeon level always beats every one of them put together.

score = 100000 * (deepest level - 1)      # 0 .. 700 000
      +     10 * min(gold, 2000)          # 0 ..  20 000
      +     50 * min(experience, 1000)    # 0 ..  50 000
      +   5000 * deeds earned             # 0 ..  15 000

Death subtracts nothing. That is NetHack's own rule: you score the run you had, and dying merely stops you scoring more.

A policy is just a prompt

coworld upload-policy coworld-nethack:latest \
  --name my-nethack \
  --run /bin/nethack-player \
  --secret-env PLAYER_PROMPT="Dive. If > is on your map, travel to it and take it in the same turn."

The seat container is deliberately thin: it connects, sends one registration message, and receives. Every decision is made inside the game server, because that is the only container the platform injects the coworld's anthropic_api_key secret into. The same image ships two scripted baselines, selected by environment variable:

--secret-env PLAYER_SCRIPTED=delver     # the deterministic crawler (the floor)
--secret-env PLAYER_SCRIPTED=bumbler    # the reactive control

What the cog sees

{"you_are": "Alpha the Digger",
 "status_line": "Dlvl:3 $:214 HP:9(14) AC:7 Xp:3/42 T:517 Hungry",
 "map": ["                                                ",
         "  ------------                                  ",
         "  |..........|                                  ",
         "  +.........r|                                  ",
         "  |.......@..'#########                         ", "…"],
 "messages": ["You hit the sewer rat.", "The sewer rat bites!"],
 "visible": [{"glyph": "r", "name": "sewer rat", "x": 12, "y": 4, "kind": "monster"}],
 "inventory": [{"letter": "a", "name": "dagger", "kind": "weapon", "count": 1,
                "equipped": "wielded"}],
 "deeds": [{"name": "fed", "earned": true}], "depth_reached": 3,
 "notes": "DL3: came down at (35,10). no > yet. smoky potion untested."}

A space is a cell it has never seen. Monsters are never remembered: if one is not in visible, the cog does not know where it is.

What the cog sends

One JSON object, up to ten actions, which run one per dungeon turn:

{"actions": [{"do": "travel", "x": 31, "y": 8}, {"do": "down"}],
 "say": "stairs first, then the gold",
 "notes": "DL3: > at (31,8). west door still dark. 214 gold."}

docs/ACTIONS.md is the full verb list and the reply schema. docs/RULES.md is the game. docs/PORTING-NETHACK.md says exactly what this is and is not a port of — no score here is comparable to an NLE, NetHack Challenge or BALROG number, and the reasons are written down.

Layout

PathWhat
src/nethack/the sim (dungeon, mobs, items, minihack, sim), the driver, the baselines, the LLM decision layer, the replay codec, the board compositor and the mummy server
src/nethack.nimthe game entrypoint (/bin/nethack)
src/nethack_player.nimthe seat registrar (/bin/nethack-player)
client/the broadcast chrome, inherited from coworld-ctf with an appended nethack block
replay-viewer/the static wasm replay bundle: the same sim module compiled to WebAssembly
tools/the CI smoke, the viewer smoke, the replay-viewer build hook, the baseline sweep, the replay summariser
scripts/art/the nano-banana source sheets and the split script that produce data/art/
tests/the sim, generator, driver, engine, replay, manifest, viewer and label tests
docs/plans/the accepted design note, verbatim

Building and running it

The whole toolchain is pinned. CI is the harness:

nimby use 2.2.4 && nimby --global sync nimby.lock
nim r --hints:off --path:src tests/test_nethack_sim.nim   # any test file
./tools/ci/docker_smoke.sh coworld-nethack:ci             # one real episode
./tools/build_replay_viewer.sh "$PWD/dist/static-replay-viewer"

Replays

The replay is the binary COWLDNET format: the resolved config, the join record, one plan record per turn (this game's entire input log), the chat records and one gameHash per tick. The static wasm viewer re-simulates the whole episode from those bytes and compares the hash chain every tick, so a replay that has drifted says so in #mmwarn at the tick it drifted.

tools/replay_summary.py reads the same bytes with the Python standard library alone and prints one strict-UTF-8 JSON object.

MIT licensed.