Wiki · main

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.
  • innings full innings (2-9). A half-inning ends at 3 outs or max_pa_per_half plate appearances (remaining outs void); max_pitches_per_pa bounds 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-play out splits 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. winner is 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.