Four cogs write one piece of music together on a 16-step sequencer.
Each seat owns one voice — Bass, Tenor, Alto or Soprano — and the seat-to-voice assignment is a
seeded permutation, redrawn every episode, so no policy can specialise on one register. The key, the
mode, the tempo and a bar-by-bar chord plan are drawn from the same seed and revealed to everybody
from turn 0. Every turn all four cogs write one bar at the same time, without seeing each
other's choice: either the new bar, or a rewrite of one of their own earlier bars — and a rewrite
spends the turn, because the new bar merely holds a copy of the last one. A bar is 16 integer
tokens: -1 is a rest and 0..13 is a scale degree in that voice's two-octave register; every note
lasts exactly one step.
When the piece is finished a fixed, public, deterministic metric scores it out of 100 — consonance 35 %, voice leading 25 %, rhythmic coherence 25 %, novelty against your own earlier bars 15 % — and each seat's score is a counterfactual: the piece as written, minus the piece with every note of that seat's voice deleted. There is no vote and nobody judges anybody, so there is nothing to collude on. The only way to raise your score is to raise the piece by more than your absence would, and a voice that is rougher than the piece's average, or that fills the grid until all four voices sound at once, scores below zero. Credits are leave-one-out differences and do not sum to the piece score.
A policy is just a prompt. The player container's only job is to deliver its prompt; every decision is made by the game server, which sends that prompt plus the seat's observation to Claude as one parallel batch per turn.
| path | what it is |
|---|---|
src/chorus/types.nim |
the config and the event record |
src/chorus/sim.nim |
the pure rules and the metric — no IO, no networking, no LLM. The server, the tests and the wasm viewer all drive this same module |
src/chorus/llm.nim |
prompt building, the parallel Claude batch, tolerant reply parsing, and the two scripted baselines |
src/chorus/server.nim |
the Coworld game contract: HTTP routes, the player and spectator websockets, the turn loop |
src/chorus.nim |
the game entrypoint (/bin/chorus) |
src/chorus_player.nim |
the player entrypoint (/bin/chorus-player) |
client/chrome.css |
cogame-bullwhip's broadcast chrome, unchanged, with one appended chorus block |
client/chrome_common.js |
the chrome half of cogame-bullwhip's renderer.js, copied character for character, plus one added function relayout() |
client/renderer.js |
the chorus stage: the sequencer piano roll, the chord ribbon, the playhead, the score strip, the feed, the endcard and the WebAudio playback |
client/global.html / player.html / replay_broadcast.html |
the starter's pages with the chorus game block appended |
replay-viewer/ |
the static wasm replay bundle: chorus_replay.nim compiles src/chorus/sim.nim to WebAssembly so a browser re-derives every frame from the replay bytes alone |
tools/build_replay_viewer.sh |
the coworld build hook that produces that bundle (committed executable) |
tools/ci/ |
the raw-docker episode smoke, the headless-browser viewer smoke, and the policy set |
tests/ |
sim unit tests and the scripted-baseline tests |
docs/plans/ |
the accepted design note |
The repo builds and runs entirely in Docker; CI is the harness.
docker build --platform=linux/amd64 -t coworld-chorus:ci .
./tools/ci/docker_smoke.sh coworld-chorus:ci # one real episode, four seats
./tools/build_replay_viewer.sh "$PWD/dist/static-replay-viewer"
node tools/ci/viewer_smoke.mjs \
--bundle dist/static-replay-viewer \
--replay dist/smoke/replay.json --timeout 90 --soak 10Nim tests, with a nimby-synced package tree:
nimby use 2.2.4 && nimby --global sync nimby.lock
nim r --hints:off --path:src tests/test_sim.nim
nim r --hints:off -d:release --path:src tests/test_bot.nimWatch a live episode at http://localhost:8080/client/global, and a recorded one at
/client/replay or in the static bundle at index.html?replay=<url>.
Reuse the published image and set PLAYER_PROMPT to your strategy:
coworld upload-policy coworld-chorus:latest \
--name my-chorus --run /bin/chorus-player \
--secret-env PLAYER_PROMPT="Own one voice. Put your onsets on steps 0, 4, 8 and 12, play the
chord tones of this bar's chord on the strong steps, move by step, and change about half your
motif each bar. Rest on any step where three voices already sound."Two scripted baselines ship in the same image and are selected with PLAYER_SCRIPTED:
| value | baseline |
|---|---|
arpeggio (or 1 / true / yes) |
chord tones spread across the four voices on complementary steps, rotated a place each bar. The strong baseline, and the fallback for any seat whose reply fails twice. |
pedal |
the chord root on the downbeat and a fifth halfway through the odd bars. Always legal, far too thin to score. |
They also play every seat when no LLM credentials are present, so an episode always completes and offline certification finishes in seconds.
- 4 seats, 4 voices (
Bass36,Tenor48,Alto60,Soprano72), seeded permutation. barsturns (default 8, range 4–16); every bar is 16 steps.targetmust satisfy0 ≤ target ≤ turn; a missingtargetmeans this turn's new bar.stepsis exactly 16 tokens, each-1or0..13. A string of 16 whitespace- or comma-separated tokens is also accepted, where.,-,r,Randrestall mean a rest.sayis one 100-rune line all three other cogs read next turn (talk variants only);notesis a 600-rune private notebook fed back verbatim.- Two endings and no others:
complete(all bars written) anddeadline(the play deadline was crossed between turns; the piece is honestly scored on the bars actually written).
Full rules and the metric with worked examples are in the manifest's rules.md and scoring.md
pages, and the design note is at docs/plans/2026-08-24-chorus-design.md.
Native game-owned model calls can write private decision evidence through
COGAME_SAVE_TRAJECTORY_URI. Capture requires COWORLD_EPISODE_ID,
COWORLD_GAME_VERSION, and COWORLD_SOURCE_REVISION. The game retains exact
request bodies, received response bytes and exposed platform IDs from the existing
parallel Curly batches, including both retry attempts. Recorded latency is the
observed parallel-batch wall time. Native parsing, notes, scheduling, fallbacks,
scoring and model requests are unchanged. A normalized proposal is retained but
has no selected training target; scripted actions carry unknown origin. Missing
usage or sampling evidence stays absent. Private evidence seals before public
results and replay. Receipt admission and independent scenario review remain
required; software fixtures do not provide model demonstrations.
After building the native game and player, run the full capture system check:
python3 tools/test_native_capture.py /absolute/path/to/chorus /absolute/path/to/chorus-player /new/private/outputIt plays all three complete manifest variants with capture enabled and disabled, comparing native outcomes and replays while checking retries, fallbacks, normalization exclusions, private operator separation, and absent usage fields.
promptVariant selects episode-stable rules wording. The training bank contains
canonical, plain, numbered, questions, and reordered; mixed selects
promptSeed mod 5 from that order. Mixed selection requires an explicit nonnegative
integer promptSeed, independent of the game seed. Defaults remain canonical.
heldout-briefing and heldout-reference require split: validation in language
bridge resets. The teacher exporter rejects them.
The bank chorus-rules-v1 varies explanations and section order. All variants share
verbatim scoring constants, pitch notation, piece parameters, and chord plans.
Actions, private notebooks, speech limits, and counterfactual scoring are unchanged.
Bump the bank version whenever any template changes.
Build the bridge with nim c --path:src tools/train_bridge.nim. Launch it as
tools/train_bridge coworld_manifest_template.json standard --language for full
JSON actions matching the displayed prompt. Omit --language for the factorized
numeric policy interface. Reset accepts promptVariant and promptSeed.
Every decision includes private semantic_view.prompt_template metadata with
bank, selected variant, requested selection, seed, and exact system/user SHA-256 hashes.
Build the exporter with nim c --path:src tools/export_posttrain.nim. From a clean
committed checkout, run tools/export_posttrain OUTPUT 10 1 standard 0 to export
ten games with wordings cycling independently within training and validation;
the requested game count is unchanged. Omit the final argument for canonical export.
All wordings and game variants share a chorus-<seed> family. Families divisible
by five enter validation; others enter training. Keep these assignments when
combining corpora. The original train/validation files contain exact messages,
and the manifest records source, bank, selection, complete-game scores and teacher.
For controlled evaluation, pin source, model, operator strategy, game variant, seeds, and budgets; compare canonical-trained and mixed-trained policies on the same held-out games. Report score distributions, invalid actions, retries, fallbacks, and completion rates separately. Scripted checks establish unchanged game semantics, not reduced model collapse or improved learning.
Complete-flow checks:
python tools/test_prompt_variants.py /absolute/path/to/bridge
python tools/test_prompt_export.py /absolute/path/to/export /new/private/outputThe 24 complete bridge games cover all three variants, all seven direct wordings and one mixed selection. They check simultaneous choices, rewrite turns, private notes, exact hashes and unchanged outcomes. Export checks compare 20 games per variant and retain scenario-family split checks. Existing native-client and Docker checks remain in place.