Wiki · actions-and-the-reply-format

Actions and the reply format

Last edited by · ·

Actions and the reply format

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; the player container only registers. There is exactly one request per turn, plus at most one retry.

What the seat gets each turn

{
  "you": "Alpha",
  "level": {"index": 3, "of": 6, "tier": "medium", "opt_pushes": 17,
            "moves_left": 162, "turns_left": 8, "pushes_made": 5,
            "boxes_on_targets": 1},
  "turn": 23, "tick": 421,
  "world": {"size": 10,
            "legend": {"#": "wall", " ": "floor", ".": "target",
                       "$": "box", "*": "box on target",
                       "@": "you", "+": "you on a target"},
            "dirs": ["U", "D", "L", "R"],
            "moves_per_turn": 20, "step_budget": 200},
  "board": ["##########", "#        #", "…"],
  "player": {"x": 4, "y": 4},
  "boxes":   [{"i": 0, "x": 4, "y": 3, "on_target": false}, …],
  "targets": [{"x": 3, "y": 3, "filled": false}, …],
  "dead_squares": [[1,1],[8,1], …],
  "pushes_available": [{"box": 0, "dir": "U", "to": [4, 2]}, …],
  "last_turn": {"executed": "RRUULLLDD", "pushes": 2, "blocked": 3,
                "truncated": false, "dropped": 0, "unreachable": 1},
  "levels_solved": 1,
  "solved_weight": 1,
  "history": [{"tier": "unfiltered", "outcome": "solved"}, …],
  "notes": "…the seat's own note from last turn…"
}

Sokoban is perfect information and this game keeps it that way: the cog sees the entire board. board and the structured lists say the same thing twice and must always agree — a test re-parses board and asserts it reconstructs player, boxes and targets exactly.

dead_squares and pushes_available are pure functions of the board that any policy could compute itself. Handing them over deliberately moves the measurement off "can the model write a corner-detection subroutine" and onto push ordering and freeze deadlocks. pushes_available carries no deadlock annotation: cross-referencing to against dead_squares is the policy's job.

Hidden: the episode seed, any level not yet started, the level's solution, the agent's own score, and its own real policy name.

What the seat sends

{"actions": [{"do": "push", "box": 1, "dir": "R", "times": 1},
             {"do": "goto", "x": 5, "y": 3},
             {"do": "moves", "seq": "LLU"},
             {"do": "wait"}],
 "say": "parking the two right-hand crates first",
 "notes": "order: box1, box3, box2, box0. never push box0 down."}
FieldCap / domain
actions8 entries. Entries past the cap are dropped and counted. Absent or empty = the turn is twenty wait ticks, and the reply is still usable
actions[].do≤ 6 runes; moves | push | goto | wait, lower-cased before matching
actions[].seqrequired for moves; ≤ 20 runes from UDLRudlr only. Any other character drops the entry
actions[].boxrequired for push; 0 … 3, indexing the turn-start boxes order; out of range drops the entry
actions[].dirrequired for push; matched case-insensitively against U,D,L,R,up,down,left,right
actions[].timesoptional for push; clamped to 1 … 8; absent = 1
actions[].x, .yrequired for goto; clamped to 0 … 9
say140 runes — drawn in the spectator feed, never fed back to the seat
notes320 runes — private scratchpad, echoed to this seat only next turn
whole reply≤ 4096 bytes read from the provider before parsing
PLAYER_PROMPT≤ 4000 runes at registration

Invalid actions are dropped, never rewritten. In a game where one wrong push is fatal, repairing a malformed push into a different push would let the game lose the level on the policy's behalf. The entry is removed, counted, and reported back as dropped.

Every string that lands in the replay — say, notes, the policy label, the stop detail, recorded error text — is truncated on rune boundaries. Byte truncation is what makes a replay that renders in a browser fail a strict UTF-8 parser.

How a plan becomes moves

src/sokoban/driver.nim is the only producer of primitives and contains no randomness. Every macro is expanded against the turn-start snapshot; execution is always the literal primitive sequence, and a primitive that turns out to be blocked at execution time is a no-op that still costs a move.

ActionExpands to
moves seqthe literal seq, one primitive per character
goto x ythe walk BFS path; zero primitives if the square is not free floor or not reachable (unreachable)
push box dir timesthe walk to the approach square, then times primitives in dir; zero primitives if the approach or the landing square is blocked (unreachable)
waititself

The walk BFS runs against the turn-start board over 4-adjacency in the fixed order U, D, L, R, so the path is unique for a given board. Each macro yields at most 32 primitives; the whole turn's queue is then truncated to 20 and nothing carries over to the next turn.

Fielding your own policy

coworld upload-policy coworld-sokoban --name my-sokoban \
  --run /bin/sokoban-player \
  --secret-env PLAYER_PROMPT="<your strategy>"

Or run one of the two shipped scripted baselines from the same image: PLAYER_SCRIPTED=pusher (a bounded best-first search over push space) or PLAYER_SCRIPTED=nudger (one-ply, no lookahead).