Files
station-master/TODO.md
T
Jesse ec792dfc15 Say who is playing, what you can press, and where you are in the Stage
Mostly presentation, plus one rules fix found on the way.
2026-08-04 08:37:42 -04:00

185 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# To do
Things worth coming back to. Anything noted here should either get done or get an explicit decision
not to — the point is that nothing quietly evaporates.
Ordered within each section by how much it is currently costing us.
---
## Next
- [ ] **Measure with error bars from now on.** Revenue has a standard deviation of ~9, so a
100-game run carries about ±1.0 of noise — every single-change revenue claim in the changelog
before the Interlocking work is inside that. Use paired per-seed comparison (the harness deals
the same seeds either way) and 400+ games before calling a heuristic change good or bad. The
first attempt at the Running Track straight was read as a 0.6 REGRESSION on 100 games and is
a 0.7 improvement on 400.
- [ ] **Confirm the Classification Yard rule against the source.** Cars now return to the Division
Yard at the Day boundary — an ASSUMPTION, not a recovered rule. Gap 2c says everything but
cabooses goes to Classification and never says how it empties. It is worth +2.32 revenue a
game, so if the real rule differs the balance numbers move with it.
- [x] ~~**Enhancements are placed but mostly do nothing.**~~ Measured: forbidding every Enhancement
except Interlocking is worth **-0.01 ± 0.41 (t = -0.04)** over 400 paired seeds. They neither
pay nor cost. Left alone. Unlocking the Running Track straight put
nine kinds on the board (telegraph 0.73, waterColumn 0.57 …), but only Interlocking has a
measured effect. The Telegraph/Telephone/Radio chain adds to the other train's number when
dispatching facing trains, which may be worth nothing in solitaire; Water Column removes a
Watertower; Facing Point Locks prevents Derail, which is multiplayer-only. Worth measuring
what each is actually worth before the bot spends actions on them.
- [ ] **The marginal Local Operations action is worth ~0, and that is the real ceiling.** Three
separate attempts to spend the 60 actions better — capping the draw, pairing the two halves of
a load, restricting Enhancements — each measured within noise of zero over 400 paired seeds.
76% of the time an outbound industry has neither a stocked green box nor a spotted car, and
only 5% of Stages have a single workable facility anywhere, yet redirecting actions at that
does nothing. Something upstream limits how much work exists to do at all; find out what
before spending more effort on the option mix.
- [ ] **Freight was stuck at ~2.7 loads a game and three fixes have not moved it.** Sidings,
facility placement, car selection and the discarded-load leak all raised revenue (3.2 → 6.5)
without raising `loadStarted` past 2.7. The chain is not leaking and the cars are arriving
correctly (57% of drops land on a facility that wants them, 0% on one that does not). The
binding constraint is now upstream of routing: 60 Local Operations actions a game, and a load
needs a stocked green box AND a spotted car AND a free Laborer to line up in the same Stage.
Measure how many Stages have all three before changing any heuristic — the answer may be that
the economy, not the bot, is what caps freight.
- [ ] **`stats.ts` undercounts freight.** `rev.freightUnload` is assigned `eventCounts['unloadBegan']`
— unloads *begun*, not revenue *earned* — and `grossFreight` uses `freightLoad` alone, so
`freightShare` omits unload revenue entirely. The comment justifying it ("an unload scores
through the same event as a load completion") is wrong: `apply.ts:947` emits a distinct
`freightUnload` reason. This is why freight was recorded at 13–18% of gross.
---
## Open questions for Jesse
Blocked on a decision, not on work.
- [ ] **Q13 — rear-end collisions on a Mainline card.** §10 says a Mainline collision is the
Superintendent's fault and removes both trains, and ABS Signals exists to prevent rear-enders —
but §8.3's trigger list does not include one, and **none is implemented**. Granting clearance is
currently free: verified, both trains survive, no penalty. Three candidates: collide on entry
(clearance becomes a gamble), collide on catching up (rewards judging the gap), or accept that
clearance is safe and ABS Signals is worth less than it reads. The buttons currently describe
only what the engine does, so nothing promises a consequence that cannot happen.
- [ ] **Poling.** The only card in the deck with no defined behaviour — the sheet records its effect
as "TBD in the source". A test asserts it stays TBD so nobody invents one.
- [ ] **Heavy Grade orientation at setup.** The card says "Player sets orientation", but `createGame`
is synchronous and has no decision point, so it is currently rolled from the seed. Should become
a real choice when setup gains an interactive phase.
---
## Balance, provisional
Numbers chosen to fix a measured problem rather than taken from the design. Revisit once the victory
target is settled and freight carries its intended share.
- [ ] **Office card density** (Depot 4→8, Station 2→4, Terminal 1→2). Chosen to remove a 25% chance
of an unwinnable opening deal. Blunt: it lifts the whole ladder and dilutes every other
category. The better answer may be fewer Terminals, a cheaper first upgrade, or more A/D
capacity at the Whistle Post itself.
- [ ] **Industry density** (9 → 27, Gap 12). Restored roughly the prototype ratio. The "freight is
only 13–18% of gross" figure that motivated this was partly a measurement bug (see the
`stats.ts` item) and partly the car-selection bug; freight now runs at 37%. Worth re-deciding
whether 27 is still the right number now that the industries are actually served.
- [ ] **Train density.** Left alone by decision, but noted: 22 train cards in 140 are drawn less often
than 22 in 115 were, and trains scheduled fell 2.9 → 2.1 as a side effect of the other density
changes.
- [ ] **The victory target itself** (20 over 5 Days). 5 wins in 100, up from 1, and the bot now does
exploit sidings — so that unclaimed gain has been claimed and the target is still missed by a
wide margin (mean 6.0 against 20). This is the next real balance question.
---
## Not yet built
- [ ] **Let the game join a call and talk to the table.** Long-term. If the game could join a Zoom,
Teams or Jitsi call and post into its chat, it could carry the whole table's shared state
without anyone alt-tabbing: the history of actions as they happen, and a prompt when someone
is holding the game up — "Now waiting on player Alice to complete the Cargo phase."
- Further out, audio into the same call: a crash when a collision happens, a bell as the Stage
clock turns over.
- Further out still, a nudge on a timer — if a player has not moved within some interval, the
game says so, by beep or by spoken line: "Still waiting on Alice to complete the Cargo
phase." That turns the turn chart's "waiting on" chip into something a distracted table
actually notices.
- [ ] **Multiplayer train make-up is a round, not one player's job.** When a new train is built,
players take turns adding cars to the consist; in solitaire one player does all of it. The
engine currently has no per-player turn within the New Train phase, so this is unbuilt rather
than wrong.
- [ ] **Action cards (10) and Space-use cards (12).** Genuinely multiplayer-only — they are played AT
an opponent. Rejected with `NOT_IMPLEMENTED`.
- [ ] **Multiplayer proper.** The engine runs 2–5 player games and the bot plays them, but there is no
server, no turn submission, and no per-player view.
---
## Smaller things
- [ ] **Carry `links` forward in replay frames.** Static per card and never changes once laid, but
re-sent whenever anything on the board changes. The replay is 3.4 MB, mostly board state.
- [ ] **The 5 MB replay size limit is arbitrary.** Invented, not a browser constraint. It has earned
its place — it caught a 5.2 MB payload that turned out to be the whole grid re-serialised every
frame — but the number itself deserves a reason.
- [ ] **Curves are drawn as two straight segments meeting**, not true arcs. Fine at this size, angular
close up.
- [ ] **Wide boards scroll.** A 40-card district and a 13-section Division both need horizontal
scrolling. Legible, not compact.
- [ ] **No way to start a fresh game from inside the page.** `?seed=` gives a reproducible deal and
"new game" only appears once a game has ended, so abandoning a bad opening means editing the URL.
- [ ] **Save/restore is not version-aware.** A save from an older ruleset stops replaying rather than
failing loudly, which is the safe direction but says little about what changed.
---
## Done, kept for the reasoning
- [x] **Put rolling stock back into circulation.** The Classification Yard was write-only — seven
writers, no readers — so 37% of all rolling stock left the game by Day 5. Returning it at the
Day boundary is **+2.32 ± 0.52 (t = 8.79)**, the largest single change measured on this bot,
and it was ranked THIRD and predicted not to matter because the Division Yard never runs dry.
The aggregate was the wrong measure; having the right commodity at the right moment is what
counts.
- [x] **Make Enhancements reachable at all.** The bot never laid a straight on the Running Track
(0.00 in 100 games) because two-arc run-arounds do not need one — so 13 of the 18 Enhancement
cards had nowhere to go, including Interlocking, the only cure for the only penalty in the
game (`no free A/D track`, 27% of gross). One scored straight fixed it: enhancements placed
0.64 → 3.01, collision cost 2.70 → 1.91, worst game −47 → −24. Revenue +0.70 ± 0.74 paired
over 400 seeds — real but not significant alone; the variance reduction is the clearer win.
- [x] **Stop the bot discarding its own freight.** `canStockProductively` did not check the Division
Yard while the engine's `stockOutbound` does, so Freight Agent was chosen when nothing could be
stocked and the follow-through fell through to an unjam that threw a waiting load out of the
green box — 3.10 a game against 2.71 started. Now 0.00. Revenue 6.0 → 6.5, wins 5 → 8 in 100.
Also confirmed **routing was never the problem**: 0% of drops land on a facility that does not
want the car.
- [x] **Why switching work did not become Revenue.** Answered: it was the freight the crew shuffled,
not the shuffling. The chain never leaked — 95% of started loads finished — it was barely
entered, because a load needs a matching empty car spotted and half the industries never asked
for one. Three fixes later (sidings, facility placement, car selection) revenue is 3.2 → 6.0
and freight 26% → 37% of gross.
- [x] **Fix car selection.** Three of six industries were invisible to `wantedCars` — a hand-written
industry→car map naming two industries that do not exist and omitting three that do — so tank
cars were dropped **0 times in 100 games**. Derived from `INDUSTRY_PROFILES` now, and the
second commodity of the two-commodity industries is reachable. Revenue 5.0 → 6.0, freight
share 25% → 37%, wins 1 → 5 in 100.
- [x] **Put the industries on the run-around.** Facility placement was unscored — the first legal
square — so 0.00 facilities a game sat on a loop; now 1.08. The instructive part was the
second bug: scoring facilities onto the siding row dropped run-arounds 91→36, because the
anchor test asked a card's KIND rather than its PORTS and an industry in the line read as a
dead end. Revenue 4.1 → 5.0. Freight did **not** follow, which is the item above.
- [x] **Make the bot build sidings that are sidings.** 0 run-arounds in 100 games → 91. Three bugs,
all scoring on local shape without checking it reached anything; the decisive one was that
`bestTrackLay` never declined a piece, so it spent the track supply on whatever was legal.
- [x] **Teach the bot what a siding is for.** Nose coupling (§A.3) implemented, so approach direction
decides which car is droppable; the bot runs around rather than setting out, when the drop can
follow. Switching activity transformed, revenue unchanged.
- [x] **Curve geometry.** Curves were topologically identical duplicates of turnouts, and nothing
reached north, so a district could only be a vertical column. Now two-port rotatable arcs.
- [x] **Q10 — when track may be laid.** During the "draw a card" option, one piece a turn. Track was a
card when §6.2 was written; a 26-piece supply has no hand to bound it.
- [x] **Q11 — which way a Heavy Grade climbs.** Answered from the card: it prints "(Up)" and "Player
sets orientation", so it is a property of the placed card, not a compass constant.
- [x] **Q12 — Whistle Post lock-in.** Players always start at a Whistle Post; office density doubled
instead.