Porting RWARE
Last edited by · ·
Porting RWARE
The rules this Coworld plays are
semitable/robotic-warehouse's
rware/warehouse.py and rware/__init__.py, transcribed into Nim. This page is
what was ported, how the port is proven, and every place it deliberately
diverges.
The pin
vendor/upstream/warehouse.py and vendor/upstream/__init__.py are
byte-pristine copies at a pinned commit, never edited. vendor/UPSTREAM.md
records the repo, the commit hash, the fetch URL and each file's sha256, and
vendor/LICENSE-rware carries the upstream licence.
src/rware/upstream.nim is the ONE place an upstream number is written down,
each constant beside the line it was read from.
The three fidelity gates
tests/test_rware_upstream.nim— the tripwire. It regex-parses the vendored files and asserts byte-equality against every constant inupstream.nim: the two grid-size formulas, the fourhighway_funcclauses, the goal-cell formula, the action enum and its integer values, the direction wrap list,column_height = 8, the size table{tiny (1,3), small (2,3), medium (2,5), large (3,5)}, the difficulty table{easy 2, normal 1, hard 0.5},max_steps = 500,sensor_range = 1,msg_bits = 0andrequest_queue_size = int(agents * d). A re-vendor that changes a number FAILS THE TESTS instead of silently desyncing the game.tests/test_rware_layout.nim— a direct transcription of upstream's layout loop is run for(rows, cols)in{(1,3), (2,3), (2,5), (3,5), (1,5)}and asserted equal, cell for cell, towarehouse.nim's generator;(1,3)is asserted to yield exactly 10x11, 32 shelves and goals (4,10) and (5,10),(1,5)exactly 16x11, 64 shelves and goals (7,10) and (8,10); and every vertical aisle is asserted to be exactly one cell wide.tests/test_rware_determinism.nim— the episode is re-derived from the replay's seed and order records alone, on a fresh sim, and every per-tickgameHashmust match. The same chain runs in the browser: the wasm viewer compiles the SAMEsrc/rware/sim.nimand checks the hash at every tick.
Documented divergences
-
Deterministic collision tie-breaks. Upstream delegates the cycle search and the longest path to networkx, whose choice among several cycles or several longest paths is implementation-defined. The port pins both: cells are ordered by index
y*W + x, the cycle search starts from the lowest-indexed node of the component and visits successors in ascending index order, and the DAG longest path is broken toward the path whose start node has the lowest index. Required for a re-derivable replay; the rules — 2-cycles fail, longer cycles rotate, DAG longest path advances — are upstream's exactly. -
Who chooses the action changed, not what the actions are. Per-tick RL policies are replaced by five high-level orders under a deterministic pilot. The five-action space, the direction wrap list, the loaded-move veto, the load/unload rules, the delivery rule and the request refill are upstream's.
-
sensor_range1 -> 3. A seat plans 20 ticks ahead and a 3x3 window cannot see a corridor conflict forming. -
Scoring is
100 x GLOBAL + INDIVIDUALrather than upstream's registeredINDIVIDUAL. The game is fully cooperative and the league needs a rankable per-seat integer. Both upstream quantities are recorded inresults(teamDelivered,delivered[]). -
max_inactivity_stepsdisabled. Upstream's optional inactivity termination is off, so a jam is watched rather than hidden. -
The request board shows home cells. Upstream marks requested shelves only inside the sensor window; a dispatcher that cannot be told where a shelf lives cannot issue
fetch. Home cells are static warehouse knowledge; dynamic facts (who is carrying what, which slots are free) stay radius-limited. -
One game per episode. The starter's multi-game side swap is not used: a cooperative game has no side to swap.
-
The
courteousfetch cost adds its contention penalty. The design note writes the choice as minimisingpath - contentionPenalty; minimising that would PREFER a shelf another robot is already closer to, which is the opposite of the rule the same paragraph describes. The port adds the penalty. -
courteoushas a release valve. The note's yield rule fires when a visible robot with a LOWER slot index is also blocked, so exactly one robot in any pair backs off. The lower-slot robot in a standoff may be ashuttle, which never yields, and then nobody does and the aisle is dead for the rest of the episode. A robot blocked for twiceyieldAftertherefore yields whatever its slot — late enough that the slot tie-break still decides every ordinary pair. -
yieldplans with the other robots as obstacles. Everywhere else the pilot ignores them on purpose (RWARE's chain rule lets a queue advance behind a mover), but the shortest route to the nearest junction is very often straight through the robot that is blocking you, which would turn the release valve into part of the deadlock. -
Two integer RNG streams, not numpy.
setupRngdraws the spawn cells, the facings and the opening request board;requestRng's k-th draw is a pure function of(seed, k)wherekis the number of deliveries so far, so which shelf is requested next cannot be steered by which seat delivered. That is the idea's "request stream seeded" integrity pin, andtests/test_rware_requests.nimasserts it by replaying the same seed with different seat behaviour and comparing the two request sequences. -
fetchsteers to the shelf's CURRENT standing cell, not its home cell. The note names the home cell. A shelf that has been delivered and then stowed somewhere else is no longer at home, and afetchaimed at the empty home cell would reportshelf_gonefor as long as that shelf stays re-stowed -- permanently, for a shelf the request board keeps drawing. The two cells differ only after somebody moves a shelf; for a carried shelf, which has no standing cell, the goal falls back to its home. The observation still advertises the home cell (decide.nim:93), which is the static warehouse fact the note wants a driver to plan from.docs/RULES.mdstates the shipped rule.
Divergences from the design note's viewer and packaging plan
The entries above are divergences from UPSTREAM. These are divergences from the DESIGN NOTE: the shipped code is deliberate and internally consistent, but it does not match what the note describes, so it is recorded here rather than left for a reader to discover in a diff.
-
No emscripten virtual filesystem:
--preload-file data@dataand-s FILESYSTEM=1are dropped. The note lists both among the link flags kept as one internally consistent set. The bootstrap-critical half of that set is untouched -- the module is emitted NON-modularized and the Worker setsModule.onRuntimeInitialized, which is the pairing whose mismatch deadlocked cogame-lantern -- but nothing in the bundle reads a preloaded file:Dockerfile.replay-viewercopies every asset next to the worker andclient/broadcast_core.jsfetches them over HTTP, so a.datapackage would ship each asset twice and add a second, silent load path.tests/test_rware_viewer.nimpins the absence rather than accepting either shape. -
Speed chips are
[1, 2, 4, 8], not the note's[0.5, 1, 2, 4, 8]. The playback speed is an INTEGER multiplier all the way down:sim_typesholdsPlaybackSpeedsas an integer array,wire_constants.nimemits it throughjsIntArrayso the page and the sim cannot disagree, andreplay_runtime.advanceReplayFramemultiplies the tick accumulator by it. A0.5chip would need a float on that wire and a fractional accumulator in the re-derivation path -- the one place this port keeps free of floating point (tests/test_rware_sim.nim's no-float grep). The default is still1, so the note's playback-length arithmetic (500 ticks at 30 fps = 16.7 s) is unchanged; the starter's own set was[1, 2, 3, 4, 8, 16], so this is a narrowing of an integer ladder, not a new kind of control. -
#stage.tinytoggles at 640 px, not the starter's 620. The note keeps the starter'srelayout()verbatim, and the--hudscaleclamp IS verbatim; only the density threshold moved. Checklist item 11 states "labels hidden under640px", and the game block's own CSS comment says the same, so leaving the toggle at 620 left the 621-640 px band as a strip where the plate labels stayed and every comment about them lied.client/page_script.jscarries the same note at the call site. -
No
rig_art.nim: the art is baked in JS at page load, andglobal.nimis a JSON payload module. The note forks the starter'srig_art.nimcompositor and bakes the robot chips, the crates and the floor with pixie at install. The starter's compositor exists to feed the Bitworld SPRITE PROTOCOL -- a binary sprite stream with server-side pools. This port's wire is one UTF-8 JSON state object per frame over an integer cell grid (global.nimemitsrobotsJson/shelvesJson/requestsJsonin cell space and keeps the pool bases as constants), so there is nothing server-side to composite into and a pixie dependency would have to be carried into the wasm build for nothing.The OUTCOME the note describes is produced, from the same byte-for-byte starter assets:
client/broadcast_core.jsbakes 96 robot chips (three sizes x four facings x loaded/empty,bakeRobotChips), the tinted crates (bakeCrates) and the tiled, 18 %-darkened floor with its chalk aisles and workstation pads (bakeFloor) once at load, so a frame is blits. It is the same file in the native page and in the Worker, so the two delivery modes cannot drift. -
The endcard forbidden-vocabulary sweep drops the word
kill. The note listskillamong the words that must not appear anywhere in the shipped labels and asserts zero matches. It cannot be zero: the same note lists#killfeedamong the starter chrome ids KEPT, and the page carries<div id="killfeed">.tests/test_rware_endcard_labels.nimtherefore sweeps the note's list minus that one word, and says so at the constant. The thing the word was there to catch -- ctf's kill beat -- is enforced separately and positively:tools/build_broadcast_page.pydeletes.beat-marker.killthroughREMOVED_SELECTORS, andtests/test_rware_viewer.nimasserts the beat CSS set is exactly{delivery, jam, fallback, end}. -
game.docsships inline{"type": "text"}values, not the note'suriform. The note writes the docs block asurireferences to files in the repo; ACCEPTANCE CHECKLIST item 10 (prompts/30-review-loop.md), the gate a release is judged against, spells it as{"readme": {"type": "text", "value": ...}, "pages": [{"id", "title", "content": {"type": "text", "value": ...}}]}. The platform validator accepts either shape; the checklist does not, so the manifest shipstext. Inlining duplicates files that also live in the tree, sotools/embed_manifest_docs.pyis the single writer of that block andtests/test_rware_manifest.nimasserts the embedded copy equals the file it was embedded from, byte for byte. -
yieldAfteris 4, not the note's 6. The note quotes the threecourteoustunables asyieldAfter = 6, penalty = 4, stowClearance = 2and, in the same paragraph, pins the MECHANISM that produced them: the head-to-head sweep intools/tune_baselines.nim, "not guessed", re-run byci.ymlso a controller change that invalidates the pick is red in CI rather than in a ladder round. The r1 fix for finding F17 -- a crediteddelivernow finishes and the robot parks off the workstation instead of squatting it -- is such a change, and it moved the pick:(4, 4, 2)now ranks 1st of 27 at the tool's 200-tick horizon and 4th at the test's 120-tick horizon, where(6, 4, 2)fell to 16th. The shipped defaults andtools/ci/baseline_tuning.jsoncarry the re-swept pick;penaltyandstowClearanceare unchanged. -
yield's passing-place table excludes the robot's own cell and every queue-lane cell. The note defines a passing place as "a highway cell with= 3 free orthogonal neighbours, i.e. an aisle junction (ties by lowest cell index)" and names neither exclusion; the >= 3 rule and the tie-break are exactly as pinned. Own cell: standing still is a length-1 cycle in the move graph, so everyone queued behind the yielding robot still cannot move -- a
yieldthat resolves to "stay here" is a no-op with adoneon it. Queue lane: the two columns below the shelf blocks are where every delivery queues, so backing INTO them moves the standoff onto the one lane that must stay clear. Both are stated indocs/RULES.mdand at the call site inrobots.passingPlacesByDistance/warehouse.initWarehouse. (Divergence 10 above covers the other half of the same order:yieldis the one order that plans with the other robots as obstacles.)
Not ported
The flattened (1 + 2*sensor_range)^2 observation vector, msg_bits
communication (the radio replaces it), the image observation modes, the
TWO_STAGE and GLOBAL reward modes as the score (both quantities are still
recorded), the small/medium/large layouts as shipped variants (the
generator and the layout test already cover them; they letterbox to 10 px per
cell in a 360 px embed, where a robot's facing stops reading), seat counts other
than 4, and Jumanji's JAX reimplementation — a cross-check reference in
vendor/UPSTREAM.md, not a second set of rules.