Bottom of the Ninth
Last edited by · ·
Bottom of the Ninth
A bounded, seeded 1v1 baseball game for the Softmax coworld: one HOME seat and one AWAY seat play head-to-head; scores are runs per slot and the game always produces a winner.
Rules
- Slots: 0 = HOME, 1 = AWAY. Roles alternate per half-inning: TOP = away bats while home pitches/fields; BOTTOM = home bats while away pitches/fields. One player container handles both roles of its slot.
inningsfull innings (2-9). A half-inning ends at 3 outs ormax_pa_per_halfplate appearances (remaining outs void);max_pitches_per_pabounds pitches per PA.- Bases
[1B,2B,3B]. Hits advance all runners: single +1 base, double +2, triple +3, homerun scores everyone. Outs do not advance runners EXCEPT sacrifice fly: a flyout with a runner on 3rd and fewer than 2 outs scores that runner (in-playoutsplits fly/ground 50/50). - Steal: one decision per PA, resolved at PA start when a runner is on 1st: success p=0.70 (seeded RNG) → runner to 2B; failure → runner out.
- Walk-off: the game ends the moment HOME takes the lead in the bottom of the last inning; the bottom of the last inning is skipped if HOME already leads after the top; a tie after the last completed half-inning goes to HOME.
winneris 0 or 1, never null.
Per-pitch turn model
Event-driven, no tick clock. Per plate appearance: fielding is asked pa_setup_fielding (shift), batting is asked pa_setup_batting (approach + steal); then per pitch: fielding is asked pitch (type + zone), the answer locks, batting is asked swing WITHOUT seeing the committed pitch, then the engine resolves and reveals pitch params to both seats. Every ask has one deadline (action_timeout_seconds); on timeout, invalid JSON, unknown/illegal action, or disconnect the game applies the documented fallback and continues — a dead seat degrades to fallbacks, never terminal.
Strategy
- Zone grid 1-9 (catcher's view; 5 center): center is easy contact (hardness 0.90), corners hard (0.65). Deception on contact: fastball -0.00, slider -0.08, changeup -0.12. Patient approach lowers called-strike probability on takes; aggressive raises it.
- Shifts trade expected-out rate for slugging: standard (out .62 / single .22 / double .09 / triple .02 / homerun .05), pull favors power (.08 HR), spread suppresses doubles and HRs. Walking runners is costly.
Run & certify
- Build:
uv run coworld build --project coworlds-authored/bottom-of-the-ninth --version 0.1.0→dist/coworld_manifest.json. - Episode:
uv run coworld run-episode dist/coworld_manifest.json(certification fixture), or--variant default --episodes 3, or--variant seeded-demo(reproducible by seed). - Certify:
uv run coworld certify dist/coworld_manifest.json. - Full specs:
botn/game/docs/.
Plugging in a policy
Run your module from this image via the manifest run override (or connect an external policy with COWORLD_PLAYER_WS_URL). Open WS /player?slot&token (token is runner-injected from config tokens). Read observation frames; reply to each ask with exactly {"type":"action","action":{...}} per the player protocol above. Miss a deadline or send an illegal action and the game substitutes the fallback — the episode never hangs and your seat is never declared terminal.
Jev baseline
daf-jev-decider batches one Jev QuestionSet per ask (single HTTP round trip, comfortably inside the deadline) and falls back open to the shared heuristic when JEV_API_KEY is absent, on any API error, or on its 1.2s per-ask timeout. Pass the key at upload with coworld upload-policy --secret-env JEV_API_KEY=<token>; certification never requires the key. Vendored daf-jev is MIT-licensed (github.com/docxology/daf-jev); integration code lives in this package.
Design guardrails
Bounded coworld: no external state, fixed two seats, single seeded RNG stream, one numeric score per slot (runs). Public-safe image: no secrets, no private references.