diff --git a/TODO.md b/TODO.md index 4b06fd2..d67a907 100644 --- a/TODO.md +++ b/TODO.md @@ -5,253 +5,65 @@ not to — the point is that nothing quietly evaporates. Grouped by what kind of work it is — Next, Replay/Save Games, Bot Performance, Play Balance, Multiplayer, Display, Rules Questions, Other — and ordered within each by how much it is currently -costing us. **Display** was split out of Other on 2026-08-22, when a session at the board produced -seven items about the screen rather than the rules; the "most recent action" entry moved with it. -Reorganized 2026-08-20 from a flat list; nothing below changed, only where it lives. Two duplicate -entries (Heavy Grade orientation, the Local's coach) were merged into one each, and the industry-table -item that had been sitting in a "these are all done" section without actually being done was moved out -to Rules Questions. +costing us. **Next is the queue**; the other sections are where the reasoning lives, and Next points +into them rather than repeating them. + +**Reorganized 2026-08-30**, and the reason is worth recording because it will happen again. Nothing +was wrong with the ordering — what had gone wrong is that the file had stopped tracking its own +closures. Next was a chronological list of 43 entries with 26 struck through, and eight items in +Display were still open `- [ ]` although Next already recorded them as superseded by Gitea#18 or +shipped in v0.7.9. **An item closed in one place and left open in another is worse than no entry at +all**, because it reads as live work. So: Next is now grouped by what each item is WAITING ON, the +closed entries moved to a "Settled" block at the end of it, and the five dead Division-map drawing +items collapsed into one that keeps only the two ideas which outlived them. 65 open items became 57. + +Earlier passes, kept: **Display** was split out of Other on 2026-08-22, when a session at the board +produced seven items about the screen rather than the rules. Reorganized 2026-08-20 from a flat list. +Two duplicate entries (Heavy Grade orientation, the Local's coach) were merged into one each, and the +industry-table item that had been sitting in a "these are all done" section without actually being +done was moved out to Rules Questions. --- ## Next -Queued from the 2026-08-20 multiplayer planning session (reasoning in Multiplayer below), in order: +**The live queue.** Every entry keeps the number it was given when it was raised — these are ids, +not positions, and the rest of this file refers to them by number ("decide with item 15", "same +drawing pass as 19-21"). Nothing is renumbered when something closes; settled entries move to +[Settled, kept for the reasoning](#settled-kept-for-the-reasoning) at the end of this section instead. -1. ~~**Fix the New Train phase car-placement round**~~ — done, see Multiplayer below. -2. ~~**Unify victory conditions across solitaire, competitive and coop**~~ — done, see Multiplayer - below. -3. ~~**Phase 2 of `docs/architecture/multiplayer.md` — server core**~~ — done, see Multiplayer below. +Regrouped 2026-08-30 by **what each is waiting on**, which is the question actually asked of this +list. It was a chronological queue of 43 entries, 26 of them struck through, and the live work had +stopped being visible in it. -Queued 2026-08-21, from playing the StartOS build: +### Blocked on a table, not on code -4. ~~**The lobby must offer every game parameter the solitaire New Game dialog does**~~ — done in - v0.6.0. -5. ~~**Decide what the four `optionalRules` are**~~ — done in v0.6.0: `sisterTrains` deleted, - `employeeRotation` implemented, the other two were already live. -6. ~~**Stop every release destroying every game in progress**~~ — done in v0.6.0, by replaying the - save rather than comparing version strings. +The largest gap in the project, and it is not a coding gap. Four features shipped without a person +ever meeting them. -Queued 2026-08-22, from playing on StartOS: +39. **NONE OF v0.7.4 HAS BEEN PLAYED BY A HUMAN.** The Yard Office offer, the Red Flag hold and its + out-of-phase prompt, and the loaded-Extra make-up rules are all tested end to end, packed, and + running on `phoenix.local` — and no person has met any of them at a board. Two are interruptions + that stop the Mainline Phase and put a question in front of somebody mid-thought, which is + exactly the kind of thing only play reveals. -7. **Make "Games in Progress" readable** — **ON HOLD, 2026-08-29 (Jesse).** StartOS 0.4.0.2 is - expected to improve how action results are displayed, which is most of what makes this unreadable - — so wait and see what the platform fixes before rewriting the action around a limitation that - may be gone. Re-open it against 0.4.0.2 and re-read the output before designing anything. - Reasoning in Multiplayer below. -8. **Give a player a way back into a game after losing their browser** — a fresh browser is still - locked out of a RUNNING game, even though the server knows who they are. Reasoning in Multiplayer - below; needs Jesse's call on whether a token in a URL is acceptable. **The lobby half of this was - fixed 2026-08-23** — a reload while seated no longer orphans the chair, and a seat can now be - given up rather than wedging the table. +35. **Extended play has never been played at a real table.** **Verified live on phoenix.local, + 2026-08-29**, against the installed v0.7.3:0 rather than in tests: a two-seat competitive game + (one human client, one bot) was dealt over the HTTP API with `days: 1`, played to the end of its + timetable, and reached `awaitingExtension` on Day 2 with `official = { win, winner 0, + daysElapsed }` frozen at Day 1 and votes `[null, null]`. Voting yes as seat 0 was accepted, the + bot followed as designed, and the game returned to `active` with `extraDays: 1` and the official + outcome **unchanged**. Both test games were deleted afterwards. -Queued 2026-08-22, from a v0.4.9d gameplay-testing report (six bugs, forwarded by Jesse): + **The save carry-over claim was checked rather than asserted**: phoenix held five saves before + the update, of which `WHISTLE-4086` resumed and three were already refused by the 0.7.2 deck + change. After updating to 0.7.3 the log is identical — same game resumed with the same 7 intents, + same three refusals at the same move with the same code. -9. ~~**Two trains in one station answer to one button**~~ — done in v0.4.9e / the release below. -10. ~~**A Freight House can unload the boxcar it just loaded; a platform can detrain the passengers - it just boarded**~~ — done in v0.4.9e / the release below. -11. ~~**The Grocer's Warehouse ships and the Refinery receives**~~ — done in v0.4.9e / the release - below: both are one-way again. -12. **PARTLY REPRODUCED: cars left behind when backing up over them** — see Rules Questions below. - Half of it turned out to be the second half of Gitea#17 and is fixed (2026-08-26): a train - pulling out through a 45° leg left its own cut standing. The "I can later drive right through - them" half is still unexplained and still needs a board from whoever filed it. - -Queued 2026-08-22, from the v0.4.9e gameplay-testing report filed as Gitea issues. - -12a. ~~**Gitea#4 — confirm that extra trains start properly**~~ — done in the release below. Either - Division Point, the Interchange with a direction chosen there, a Control Point gated by a new - `extraStart` house rule, and the Superintendent's hold on the way out of the Interchange yard. - Reasoning in `docs/rules/implications.md` §7. - -12d. ~~**Gitea#7 — coach counts on four train cards**~~ — done in the release below. 1/2 Crack - Limited 3 coaches → 2, 5/6 The Sparrow 2 → 3. A change to the cards, so `Trains3.pdf` and the - transcription in `docs/rules/implications.md` §5 keep the original numbers with a footnote; - `src/engine/content.ts` and `docs/StationMaster-Home-Deck-v0.4.5.md` carry what the game plays. - -12c. ~~**Gitea#6 — players may not discard train cards**~~ — done in the release below. Timetabled - and Extra alike; the forced play falls out of the hand limit rather than needing a mechanism of - its own. Reasoning in `docs/rules/implications.md` §6.2. - -12b. ~~**Gitea#2 — four porters, two passengers on the platform, and only one may be worked**~~ — - RULED AND FIXED in the release below, though not the way the report implies. The engine is - faithful to the written rules at every step; what bites is that BOTH directions of porter work - move coaches one-way into a Classification Yard that comes back only when the Division Yard is - bare of all ~60 cars. Sixteen coaches in the game, and the reported save runs dry on Day 5 with - eight of them stranded in Classification. **Jesse's ruling: the shortage stays** — "it is - possible to run out, that's part of the strategy" — so the three balance options in Play Balance - below are declined, not deferred. What was actually wrong is that the game said NOTHING: a Porter - action that cannot be taken is simply absent from the menu, and the "why is nothing moving?" - panel covered freight facilities only. That half is fixed. - -12e. ~~**Gitea#8 — the per-diem train could not couple a caboose**~~ — done in the release below. - `ROLLING_STOCK_SUPPLY` mints all six cabooses `loaded: true` because §2.2's "coloured is loaded, - white is empty" doubles as a PIECE COUNT there and there is no white caboose. X22 Pee-Dee, whose - whole card is "may only pick up MTs", read that literally and refused every caboose including the - one it was made up with — set it out and the train was stranded, which made it unplayable rather - than merely restricted. A caboose carries the crew, not freight, so it is never a load. - -12g. ~~**Gitea#9 — a Timetabled train may be discarded**~~ — done in the release below, and it - SUPERSEDES Gitea#6 (item 12c above), shipped three days earlier. A Timetabled train may be tossed - face-up to a Department slot, where a rival may pick it up — which needed no machinery, since - that is where every discard already goes. An Extra still may not: it never joins the timetable, - so it can never be what jams it. On this line the Timetabled half is a New Game setting - (`discardTimetabled`, on by default), because Jesse's reasoning is about games run longer than - five Days; the 0.4.9 line takes the plain rule. Reasoning in `docs/rules/implications.md` §6.2. - -12f. ~~**Gitea#10 — a dialog when the Day rolls over**~~ — done in the release below. "Hard to keep - track of time." Nothing on screen was wrong — the clock, the turn chart and the timetable all - said which Day it was — but a Day turns over inside the phases that run themselves, so it passes - between one click and the next, and the two transient signals the page had (the phase banner at - 2.6s, the announcement flash at 4.2s) are both gone before a player reading the board notices. - A modal stops and waits, and carries the standings, the Days left and the combined target. - Suppressed on the first frame, on Undo stepping back across a rollover, and on the Day the game - ends — the outcome panel is the thing to read then. - -13. **Watch the other players and the bots actually make their moves** — raised by Jesse - 2026-08-22, and **settled 2026-08-29 as the harder of the two readings**: "I want to be able to - watch other players and bots make their moves. It's not fun to do my turn and have magic happen - in the background and then have to figure out what others did." - - So this is not the log-legibility fix. It is the ordered, per-action presentation of everyone - else's turns — **the same mechanism Gitea#20 step 4 specifies for the common board**, routed to a - player's own screen as well. Jesse: "this relates to issue #20 and will require a lot more - thinking." Do not start it as a standalone piece; it wants designing with #20. Reasoning in - Multiplayer below. - -14. **INVESTIGATE: stamp the history with wall-clock time** — even if nothing displays it yet, so - "how long did that turn take" can be answered afterwards. Reasoning in Replay / Save Games below. - Half of it already exists server-side and is read by nothing. - -15. **INVESTIGATE: a "most recent action" line under the status block** — above the Division map, - saying what just happened in the same words the history uses. Reasoning in Display below; - overlaps item 13 and should be decided with it. - -15a. **Build documentation FROM the implementation, starting with a card reference** — raised by - Jesse 2026-08-22. Reasoning in Other below. The prompt for it was finding train card data spread - across five documents of three different vintages, one of them superseded. - -Queued 2026-08-22, from a session looking at the screen rather than the rules. All Display below. - -16. **Three explicit display options for the Office map** — always hidden, always on, auto-hide. All - three modes already exist; only the BUTTON is a cycle, and it cannot reach every one of them. -17. **The same three options for the Division map**, which today cannot be hidden at all. Auto there - means something different and useful: hide it now, bring it back at the end of the phase. -18. **Give every phase a visible beat.** The automatic phases are not too fast — they are never - drawn at all, because `pump` runs them all before the page renders once. -19. ~~**Turn the track art vertical on a Division card laid vertically**~~ — **SUPERSEDED by - Gitea#18** (Jesse, 2026-08-26). There are no vertical lanes any more: the Division draws as a - single row, west to east. -20. ~~**Run the inter-row connector round the OUTSIDE**~~ — **SUPERSEDED by Gitea#18.** No second - row, so nothing to connect. -21. ~~**The Division Point captions overflow the map**, and the buffer stops point the wrong way once - the route wraps~~ — **SUPERSEDED by Gitea#18.** Nothing wraps; both ends face outward. -22. ~~**Fill the dead centre of the Division map with the common board**~~ — **SUPERSEDED by - Gitea#18.** A row has no centre to fill. -23. **History: newest at the top?** Plus the timestamps question from item 14, which lands here. -24. ~~**Put the viewer's own district at the BOTTOM of the Division map** and wrap the table around - them~~ — **DEFINITIVELY SUPERSEDED by Gitea#18** (Jesse's word, 2026-08-26). A single row and a - table wrapped around the viewer cannot both hold, and the row wins: being able to rely on east - meaning right is worth more than being seated at the table. - -Queued 2026-08-23, from Jesse playing the v0.7.0 build on StartOS. **All three were the same drawing -pass as 19-21 and 24 above, and all three are answered by Gitea#18 rather than fixed:** - -25. ~~**The Division map does not draw track geometry at all**~~ — **SUPERSEDED by Gitea#18.** The - Division map stops drawing office-area detail altogether, so there is no Running Track on it to - draw geometry for. The geometry belongs to the Office map, which already draws it. -26. ~~**CONFIRMED IN PLAY: the buffer stop points the wrong way** at two players~~ — **SUPERSEDED by - Gitea#18**, with item 21. -27. ~~**CONFIRMED IN PLAY: east is not always to the right.**~~ — **FIXED OUTRIGHT by Gitea#18**, and - the reason it wins over item 24. One row means east is always to the right. - -28. **INVESTIGATE: move the game's settings off the top line and into a card of their own** — and - show ALL of them, not the four that fit. Reasoning in Display below. - -29. ~~**Put the Fedora at the right-hand end of the phase row**~~ — done 2026-08-30 in v0.7.9. It - rides at the end of the phase row now, and wraps under it rather than squeezing the chips on a - narrow screen. Went in beside the "waiting on nobody" fix below, since both are the top few rows - of status that Jesse asked to have re-looked at as a whole. - -Queued 2026-08-25, from releasing 0.7.1 / 0.4.9g. **Both are blocked on the two commits being made -and pushed** — they were still uncommitted when the session ended. - -30. ~~**Close the Gitea issues by hand, each with a comment naming the commit that fixed it.**~~ — - done, and done again for v0.7.2 / v0.4.9h (2026-08-26). #2, #6, #8, #9 and #10 carry their - comments from the v0.7.1 release, including the pointer on #6 saying #9 superseded it. #3, #14, - #15, #17 and #18 now carry theirs, each naming `2ab25e3` and `e9683cc` and the version each - shipped in — and, where the fix was not what the report implied, the ruling that decided it: - #15 was REVERSED on review (the placement is legal; what was confirmed is that no train crosses - the gap), and #14's comment lists the ten unbuilt cards that are held out, so they do not vanish - along with the issue. - - **Keep doing this.** Auto-closing leaves an issue with no record of which commit or which release - answered it. The token and the API calls are in the workspace's `AGENTS.local.md`. - -31. ~~**Bump the StartOS wrapper to 0.7.2.**~~ — done 2026-08-26 (`1bfea8d`). Submodule pinned to - `v0.7.2`, `current.ts` at `0.7.2:0`, release notes rewritten in all five locales, `README.md` and - `instructions.md` updated. No new version file and no migration. Verified: `npm run check` clean - and `make x86` packs as `v0.7.2:0`. - - **This is the first release that does NOT carry games in progress**, and the release notes lead - with it in every locale. A save is a seed plus the moves played; the deck going from 206 cards to - 121 means a card id recorded under 0.7.1 refers to a different card or to none, so a save stops - replaying at its first `card.play`. There is nothing to migrate — those moves were made against a - deck that no longer exists — and it fails safe: `src/server/index.ts` refuses to resume a save the - rules reject, names the move it stopped at, and leaves the file untouched, so an operator can put - 0.7.1 back on to finish a game that matters. - -32. **Tell the 0.4.9 playtesters their saves are dead, before they find out.** The same deck change - shipped as v0.4.9h, so every save filed before it — including the ones attached to Gitea#15 and - #17 — stops replaying at its first `card.play`, and any game a tester has in progress will be - declined on restart. They fail safe and the files are kept, but nobody has been told. Worth a - line wherever the tester build is announced, and worth knowing when the next bug report arrives - with a save that will not load. - -Queued 2026-08-29, from building Gitea#11 and #16 (both shipped in v0.7.3, main only): - -33. **The second pass on the results screen — badges, and the brainstorm Gitea#16 asks for.** The - first pass is in and reports everything the Frame and the event tally know. What it deliberately - does not have is the interesting half: "maybe create badges for anything interesting that - happened… there should be a whole conversation brainstorming session on what are the things that - might be interesting for people to be aware of at the end of the game." The raw material is - already being kept — `tally.trainsCompletedWithWork` is the switching-master join, `longestStand` - is the engine that sat on a siding — and because the statistics are DERIVED from the event stream - rather than recorded, a second pass can add any of them retroactively to games already played and - saved. Needs Jesse and a conversation, not code, to start. - -34. ~~**`replay.ts` prints a raw outcome enum, exactly as the results screen used to.**~~ — done - 2026-08-30 in v0.7.9. `panels.ts`'s `reasonSentence` is exported and shared, fed the last - recorded frame and stripped of markup; the drift test now maps `win`/`loss` to `won`/`lost` so - it still checks agreement rather than spelling. Original note below. - - **`replay.ts` printed a raw outcome enum, exactly as the results screen used to.** Its summary - line is `` `${o.result} — ${o.reason}` ``, which renders "loss — revenueFloor" — the same defect - Gitea#16 was filed about, in the dev-side replay viewer rather than the playable page. The - sentences now exist (`panels.ts`'s `reasonSentence`), but they are written against a `Frame` and - the replay recorder has a `GameState`, so it is a small refactor rather than a one-line swap. Not - done in v0.7.3 because nothing about it is player-facing and the change earns its own look. - -35. **Extended play has never been played at a real table** — but it now works on a real server. - **Verified live on phoenix.local, 2026-08-29**, against the installed v0.7.3:0 rather than in - tests: a two-seat competitive game (one human client, one bot) was dealt over the HTTP API with - `days: 1`, played to the end of its timetable, and reached `awaitingExtension` on Day 2 with - `official = { win, winner 0, daysElapsed }` frozen at Day 1 (`config.days`) and votes - `[null, null]`. Voting yes as seat 0 was accepted, the bot followed as designed, and the game - returned to `active` with `extraDays: 1` and the official outcome **unchanged**. The tally rode - the Frame to the client. Both test games were deleted afterwards; the box is back to Jesse's own - `WHISTLE-4086` and the `FREIGHT-3230` lobby. - - **The save carry-over claim was also checked rather than asserted**: phoenix held five saves - before the update, of which `WHISTLE-4086` resumed and three were already refused by the 0.7.2 - deck change. After updating to 0.7.3 the log is identical — same game resumed with the same 7 - intents, same three refusals at the same move with the same code. - - **What is still untested is the part the item is named for: humans, at a table.** Nobody has sat - down and played a game off the end of its timetable, and the multiplayer vote has never been - driven through two browsers — what a second player sees while waiting on a first, and whether - "waiting on Carol" is legible once Carol has closed her laptop, are still unanswered. Worth being - the first thing the next play session does. + **What is still untested is what the item is named for: humans, at a table.** The multiplayer + vote has never been driven through two browsers — what a second player sees while waiting on a + first, and whether "waiting on Carol" is legible once Carol has closed her laptop, are still + unanswered. **A HUMAN DID REACH IT ON 2026-08-30, AND IT WAS UNUSABLE — fixed in v0.7.9.** Jesse played a solitaire game to the end and was never offered the extra Day. Nothing was wrong with the engine @@ -259,64 +71,111 @@ Queued 2026-08-29, from building Gitea#11 and #16 (both shipped in v0.7.3, main which is **modal**, so the question sat underneath a dialog whose only control was Close. The dialog asks it now. **The verification recorded above is exactly why this survived** — it was driven over the HTTP API, which renders no dialog, so what was proven was that the SERVER - supports extended play, not that a player can reach it. Read that distinction into every "verified - on phoenix.local" line in this file. + supports extended play, not that a player can reach it. Read that distinction into every + "verified on phoenix.local" line in this file. + +42a. **Nobody has clicked through the solitaire setup screen's own fields** and confirmed the dealt + game matches what was chosen. The screen took three attempts to become reachable at all (item 42 + below); reachable is not the same as correct. Early item for the next play session, with #39. + +40. **A save from before v0.7.4 may not replay, and nobody has been told.** The Red Flags intent + changed shape, a make-up that was legal may now be refused, and a Yard Office arrival asks a + question no older history has an answer for. It fails safe — the server declines the save, names + the move and leaves the file untouched — and `WHISTLE-4086` did survive on `phoenix.local`, so it + is "may not" rather than "will not". Worth a line wherever the build is announced, and worth + knowing when a bug report arrives with a save that will not load. + +32. **Tell the 0.4.9 playtesters their saves are dead, before they find out.** The same shape as #40 + but for the playtest line: the deck change shipped as v0.4.9h, so every save filed before it — + including the ones attached to Gitea#15 and #17 — stops replaying at its first `card.play`. They + fail safe and the files are kept, but nobody has been told. `PLAYTEST-0.7.4.md` (untracked, at + the repo root) is the note drafted for this and leads with it. + +### The screen — v0.8.0 candidates + +All six were raised by Jesse from play and all six live in **Display** below, where the reasoning is. +Listed here in rough increasing cost. Two of them entangle with Gitea#20; see the note under each. + +28. **The game's settings belong in a card, not along the top line** — and show ALL of them, not the + four that fit. **Cheapest of the six**: nothing new has to be sent (`Frame` gained `mode` and + `optionalRules` in v0.7.0) and the renderer already exists — `rulesListHtml` (`settings-form.ts`) + draws exactly this list for the join preview and the seating screen. + +16. **Three explicit display options for the Office map** — always hidden, always on, auto-hide. All + three modes already exist (`districtMode`, persisted); only the BUTTON is a cycle, and it cannot + reach every one of them. + +17. **The same three options for the Division map**, which today cannot be hidden at all. `auto` + there means something different and useful — hide it now, bring it back at the end of the phase — + so it is a one-shot with an expiry rather than a standing rule, and probably should not share an + implementation with #16. + +23. **History: newest at the top?** Reversing is nearly free; the cap (`slice(-60)`) and the forward- + reading phase headings are not. Carries the timestamps question from #14. **Weaker if #15 lands.** + +15. **INVESTIGATE: a "most recent action" line under the status block.** The text already exists and + is already correct — this is placement, not content. **Overlaps #13 and should be decided with + it**: in solitaire "most recent" is the right unit, but in multiplayer what you missed is + everything that happened while you were WAITING, and one line showing only the last of them may + be the least useful line on the page. + +18. **Give every phase a visible beat.** The automatic phases are not too fast — they are never drawn + at all, because `pump` runs them all before the page renders once. **So this is not a `sleep`: it + needs an async pump that steps `advance()` one call at a time and renders between — which is the + same machinery Gitea#20 step 4 specifies.** Whichever is built first should be built for both. + +### Waiting on something outside the code + +7. **Make "Games in Progress" readable** — **ON HOLD, 2026-08-29 (Jesse).** StartOS 0.4.0.2 is + expected to improve how action results are displayed, which is most of what makes this unreadable + — so wait and see what the platform fixes before rewriting the action around a limitation that may + be gone. Re-open it against 0.4.0.2 and re-read the output before designing anything. + +8. **Give a player a way back into a game after losing their browser** — a fresh browser is still + locked out of a RUNNING game, even though the server knows who they are. **Needs Jesse's call on + whether a token in a URL is acceptable.** The lobby half was fixed 2026-08-23: a reload while + seated no longer orphans the chair, and a seat can be given up rather than wedging the table. + +13. **Watch the other players and the bots actually make their moves.** Settled 2026-08-29 as the + harder of the two readings — Jesse: "It's not fun to do my turn and have magic happen in the + background and then have to figure out what others did." So it is not the log-legibility fix. It + is the ordered, per-action presentation of everyone else's turns — **the same mechanism Gitea#20 + step 4 specifies for the common board**, routed to a player's own screen as well. Jesse: "this + relates to issue #20 and will require a lot more thinking." **Do not start it standalone.** + +33. **The second pass on the results screen — badges, and the brainstorm Gitea#16 asks for.** + **Needs Jesse and a conversation, not code, to start.** The first pass is in and reports + everything the Frame and the event tally know; what it deliberately does not have is the + interesting half. The raw material is already kept — `tally.trainsCompletedWithWork` is the + switching-master join, `longestStand` is the engine that sat on a siding — and because the + statistics are DERIVED from the event stream rather than recorded, a second pass can add any of + them retroactively to games already played and saved. 36. **There is no per-Stage "this train did not move" signal, so "longest an engine sat on a siding" - cannot be answered.** Gitea#16 asks for it and the comment on that issue said `trainStoodStill` - would supply it, "emitted per Stage, so a run of them is exactly the streak you describe". That - is wrong, and was found only by reading `advance.ts` while building the tally: the event fires - for a train whose profile sets `stopEarnsPoint` — the X18 Circus and nothing else — and - `tray.stopPointClaimed` guarantees it fires at most once per train per game. A streak folded from - it reads "1 Stage" for ever, which is what v0.7.3 built and then removed. - **What it would take:** either a new event emitted per Stage per stationary tray (cheap to emit, - but it is a lot of events for a statistic nothing scores), or sampling live state on the Stage - boundary the way `stats.ts`'s funnel probe does — which the tally cannot do today, because it - folds a batch of events AFTER `advance` has already mutated past the moment they describe. Worth - settling with the badge pass (#33) rather than on its own, since that is the only consumer. + cannot be answered.** The comment on Gitea#16 said `trainStoodStill` would supply it. That is + wrong, and was found by reading `advance.ts`: the event fires only for a train whose profile sets + `stopEarnsPoint` — the X18 Circus and nothing else — and `tray.stopPointClaimed` guarantees it + fires at most once per train per game, so a streak folded from it reads "1 Stage" for ever. + **What it would take:** a new event per Stage per stationary tray (cheap to emit, a lot of events + for a statistic nothing scores), or sampling live state on the Stage boundary the way `stats.ts`'s + funnel probe does — which the tally cannot do today, because it folds a batch of events AFTER + `advance` has already mutated past the moment they describe. **Settle with #33**, its only + consumer. -37. ~~**Bump the StartOS wrapper to 0.7.3, then 0.7.4.**~~ — 0.7.3 done 2026-08-29 (`74aea24` in - `station-master-startos`). Submodule pinned to `v0.7.3` (`45580d8`), `current.ts` at `0.7.3:0`, - release notes in all five locales, `README.md` and `instructions.md` updated. No new version file - and no migration — the outgoing `0.7.2:0`'s `up` was empty, `versions.md`'s common case, so - `current.ts` bumped in place. Verified: `npm run check` clean, prettier clean, `make x86` packs - as `v0.7.3:0`. **NOT installed on a box and not played** — see #35. +### Unscheduled - **Unlike 0.7.2, games in progress survive this one**, and both docs lead with it. No card data - changed and the engine changes are additive, so every intent in a 0.7.2 save is still legal; - proven rather than assumed by `test/harness.test.ts`, which replays the three files in - `public/replays` (all recorded under an older ruleset) and asserts every intent still applies. +12. **PARTLY REPRODUCED: cars left behind when backing up over them.** Half of it turned out to be + the second half of Gitea#17 and is fixed (2026-08-26): a train pulling out through a 45° leg left + its own cut standing. **The "I can later drive right through them" half is still unexplained and + still needs a board from whoever filed it.** See Rules Questions below. - **Bumped again to `0.7.4:0` the same day** (`085b88b`), pinned to `v0.7.4`, and installed on - `phoenix.local` — verified there, not merely packed: the resume log shows `WHISTLE-4086` coming - back with its 7 intents and all five new engine code paths present in the served build. Both tags - are signed and pushed. +14. **INVESTIGATE: stamp the history with wall-clock time** — even if nothing displays it yet. + Reasoning in Replay / Save Games below. **Half of it already exists server-side and is read by + nothing**, so the first job is to look at a real game's timings file rather than to build. - **Keep doing the whole sequence.** Tag the app, fetch the tag into the wrapper's submodule, - bump `current.ts` in place (the outgoing `up` has been empty every time, `versions.md`'s common - case), rewrite the notes in all five locales, update `README.md` and `instructions.md`, then - `npm run check` / prettier / `make x86` / `make install`. `UPDATING.md` in the wrapper is the - authority and has not needed changing. - -38. ~~**Gitea#13, #5 and #19 — three rules corrections.**~~ — done 2026-08-29 in v0.7.4 - (`5e34c73`, `2280276`, `19a6a47`), wrapper `085b88b` as `0.7.4:0`, installed on `phoenix.local`. - Each issue carries a comment naming its commit and what was ruled, per #30. Reasoning is in - `CHANGELOG.md`; what matters here is what they left behind, below. - -39. **NONE OF v0.7.4 HAS BEEN PLAYED BY A HUMAN.** The Yard Office offer, the Red Flag hold and its - out-of-phase prompt, and the loaded-Extra make-up rules are all tested end to end, packed, and - running on `phoenix.local` — and no person has met any of them at a board. Two are interruptions - that stop the Mainline Phase and put a question in front of somebody mid-thought, which is - exactly the kind of thing only play reveals. Together with #35 this is now the biggest gap in the - project: four features shipped without a table. - -40. **A save from before v0.7.4 may not replay, and nobody has been told.** The same shape as #32 but - for the main line: the Red Flags intent changed shape, a make-up that was legal may now be - refused, and a Yard Office arrival asks a question no older history has an answer for. It fails - safe — the server declines the save, names the move and leaves the file untouched — and - `WHISTLE-4086` did survive on `phoenix.local`, so it is "may not" rather than "will not". Worth a - line wherever the build is announced, and worth knowing when a bug report arrives with a save - that will not load. +15a. **Build documentation FROM the implementation, starting with a card reference.** Raised by Jesse + 2026-08-22; reasoning in Other below. The prompt was finding train card data spread across five + documents of three different vintages, one of them superseded. 41. **The bot cannot use the half of Red Flags a human would.** It takes the danger prompt unconditionally and still plays zero flags in 200 games, because the prompt needs a colliding @@ -324,57 +183,124 @@ Queued 2026-08-29, from building Gitea#11 and #16 (both shipped in v0.7.3, main Stage for switching, which needs it to know it wants time — a notion it does not have. Reasoning and the measurement are under Bot Performance. -42. ~~**Solitaire must ask before it deals, the same way multiplayer's lobby already does.**~~ — done - 2026-08-29 in v0.7.5. Jesse: "let the user choose their options like the start of a multiplayer - game"; "asking first is the only path." A new `#solitairesetup` screen in `play.html` asks the - full shared block — game type, starting hand, Extra start, revenue, victory conditions, optional - rules — before a genuinely fresh visit deals anything; a saved game, an explicit `?seed=`, or a - URL a Deal already wrote all skip past it. The in-game dialog, the lobby and this screen now - share one `wireGameTypeBlock()`/`commitNewGame()` pair instead of the dialog carrying its own - copy. Reasoning in `CHANGELOG.md`. +### Standing practice — not items, rules for every release - **Played in a browser on `phoenix.local` 2026-08-29, and it found a real bug — fixed same day in - v0.7.6.** The splash's "Play solitaire" door landed straight in a leftover Co-op four-seat lobby - instead of the new setup screen: `start()` checked a browser-remembered multiplayer session - before ever looking at solitaire's own state, and a bare `./play.html` load could not tell "I - clicked Play solitaire" apart from "I reloaded mid multiplayer game" — the same class of problem - `?lobby` already solved for the door on the other side (D11), just never applied to this one. The - door now marks its intent (`?solitaire`), checked ahead of the remembered-session lookup. +30. **Close each Gitea issue by hand, with a comment naming the commit that fixed it** and, where the + fix was not what the report implied, the ruling that decided it. Auto-closing leaves an issue with + no record of which commit or which release answered it. Done for every issue closed so far: #2, + #6, #8, #9 and #10 from v0.7.1; #3, #14, #15, #17 and #18 from v0.7.2 — including #15's REVERSAL + on review (the placement is legal; what was confirmed is that no train crosses the gap) and #14's + list of the ten unbuilt cards, so they do not vanish with the issue. The token and the API calls + are in the workspace's `AGENTS.local.md`. - **And it happened AGAIN on v0.7.6, which is what found the real cause — fixed in v0.7.7.** Every - packaged build published the same cache-bust key (`?v=nogit`, because the `.s9pk` build has no - `.git` for `git rev-parse`), and the server sent no `Cache-Control` at all, so neither release - ever reached the browser that asked for it. Both earlier fixes were correct and both were - verified by reading what the SERVER served — which was true and was never the thing in doubt. - **The lesson worth keeping: when a fix appears to have had no effect, check that it arrived - before re-diagnosing it.** A hard-reload would have answered it on the first report. +37. **The wrapper release sequence, unchanged since 0.7.2 and worth following exactly.** Tag the app, + fetch the tag into the wrapper's submodule, bump `current.ts` **in place** (the outgoing `up` has + been empty every time, `versions.md`'s common case — no new version file, no migration), rewrite + the release notes in all five locales, update `README.md` and `instructions.md`, then + `npm run check` / prettier / `make x86` / `make install`. `UPDATING.md` in the wrapper is the + authority and has not needed changing. - **And a THIRD report, 2026-08-30, with the build confirmed current on screen — which is what - finally found it. Fixed in v0.7.8.** v0.7.5 skipped the setup screen whenever a save existed - ("a saved game is a game to resume"), so any browser that had ever played solitaire could never - reach it again; the private window that seemed to vindicate v0.7.7 simply had no save. The door - outranks a save now, a bare reload still resumes, and `#ss-resume` keeps the game in progress - one button away since Deal clears it. + **Say which way games in progress go, in every locale, every time.** 0.7.2 broke them (206 cards + to 121, so a card id recorded under 0.7.1 refers to a different card or to none) and led with it; + 0.7.3 and 0.7.4 carried them and led with that. It fails safe either way — `src/server/index.ts` + refuses a save the rules reject, names the move it stopped at, and leaves the file untouched, so + an operator can put the old version back to finish a game that matters. - **Three attempts, two of them fixing something real that was not the reported fault.** Each was - reported as verified, and each verification read what the SERVER served rather than exercising - the path with the state a returning player actually has. The thing that worked was a failing - test written before the fix. Worth remembering next time a report repeats: reproduce the user's - state first, and treat "I verified it" as unearned until something failed the way they described. +### Settled, kept for the reasoning - Still not verified past that: nobody has clicked all the way through the setup screen's own - fields and confirmed the dealt game matches what was chosen. Worth being an early item in the - next play session, alongside #39's four unplayed v0.7.4 features. +Closed items, newest first. Kept because several of them are the only record of a ruling or a lesson; +the numbers stay so cross-references above and below still resolve. 43. ~~**"Waiting on" said nobody while the game was stopped on the Superintendent.**~~ — done - 2026-08-30 in v0.7.9. Reported by Jesse from play. `Frame.actor` carried `clock.currentActor`, - null for the whole Mainline Phase, so all three interruptions (clearance, Yard Office, Red Flag) - reported that the Division was running itself. It carries `actingPlayer` now and a new - `awaiting` field says what the question is and which train it is about. + 2026-08-30 in v0.7.9. `Frame.actor` carried `clock.currentActor`, null for the whole Mainline + Phase, so all three interruptions (clearance, Yard Office, Red Flag) reported that the Division + was running itself. It carries `actingPlayer` now and a new `awaiting` field says what the + question is and which train it is about. **Worth knowing for the next Frame field:** the engine + had the right answer in `actingPlayer` since the Gitea#5 refactor, and the Frame simply did not + carry it. A view that reads one field of `clock` directly is a place this can happen again. - **Worth knowing for the next Frame field:** the engine had the right answer in `actingPlayer` - since the Gitea#5 refactor, and the Frame simply did not carry it. A view that reads one field - of `clock` directly is a place this can happen again. +34. ~~**`replay.ts` printed a raw outcome enum, exactly as the results screen used to.**~~ — done + 2026-08-30 in v0.7.9. Its summary line rendered "loss — revenueFloor", the same defect Gitea#16 + was filed about, alive in the dev-side viewer a release after the playable page was fixed. + `panels.ts`'s `reasonSentence` is exported and shared rather than reimplemented, so the replay and + the results screen cannot explain one ending two different ways; the drift test maps `win`/`loss` + to `won`/`lost` so it still checks agreement rather than spelling. + +29. ~~**Put the Fedora at the right-hand end of the phase row.**~~ — done 2026-08-30 in v0.7.9. + Details, and why the name does NOT go inside the Supervisor Shift pill, in Display below. + +42. ~~**Solitaire must ask before it deals, the same way multiplayer's lobby already does.**~~ — done + 2026-08-29 in v0.7.5, and then fixed three more times. Jesse: "let the user choose their options + like the start of a multiplayer game"; "asking first is the only path." A `#solitairesetup` screen + asks the full shared block before a genuinely fresh visit deals anything; a saved game, an + explicit `?seed=`, or a URL a Deal already wrote all skip past it. The in-game dialog, the lobby + and this screen share one `wireGameTypeBlock()`/`commitNewGame()` pair. + + **THE LESSON, AND IT IS THE MOST EXPENSIVE ONE IN THIS FILE.** The same report came back three + times, and each of the first two "fixes" corrected something real that was not the reported fault. + + - **v0.7.6** — the splash's "Play solitaire" door landed in a leftover Co-op lobby: `start()` + checked a remembered multiplayer session before looking at solitaire's own state. Real bug. + Not the one reported. + - **v0.7.7** — every packaged build published the same cache-bust key (`?v=nogit`, because the + `.s9pk` build has no `.git` for `git rev-parse`) and the server sent no `Cache-Control`, so + neither release ever reached the browser that asked for it. Real bug. Still not the one + reported. **When a fix appears to have had no effect, check that it ARRIVED before + re-diagnosing it** — a hard reload would have answered it on the first report. + - **v0.7.8** — the actual fault: v0.7.5 skipped the setup screen whenever a save existed ("a + saved game is a game to resume"), so any browser that had ever played solitaire could never + reach it again. The private window that seemed to vindicate v0.7.7 simply had no save. + + **Each of the three was reported as verified, and each verification read what the SERVER served + rather than exercising the path with the state a returning player actually has.** The thing that + worked was a failing test written before the fix. Next time a report repeats: reproduce the + user's state first, and treat "I verified it" as unearned until something failed the way they + described. + +38. ~~**Gitea#13, #5 and #19 — three rules corrections.**~~ — done 2026-08-29 in v0.7.4 (`5e34c73`, + `2280276`, `19a6a47`), wrapper `085b88b` as `0.7.4:0`, installed on `phoenix.local`. Each issue + carries a comment naming its commit and what was ruled. What they left behind is #39, #40 and #41 + above. + +31. ~~**Bump the StartOS wrapper to 0.7.2.**~~ — done 2026-08-26 (`1bfea8d`). Folded into the + standing practice at #37 above, which is where the sequence now lives. + +25-27. ~~**Three Division-map drawing reports from the v0.7.0 build**~~ — no track geometry, the + buffer stop pointing the wrong way at two players, and east not always being to the right. All + answered by **Gitea#18**; the last was FIXED OUTRIGHT by it and is the reason a single row won. + Collapsed into one entry in Display below. + +19-22, 24. ~~**Five drawing items against the wrapped Division map**~~ — vertical track art, the + inter-row connector, overflowing captions, filling the dead centre, and seating the viewer at the + bottom. All **SUPERSEDED by Gitea#18**, which replaced the layout rather than fixing the drawing. + Two ideas survive them and are recorded in Display below: "SHARED in the middle, YOURS on the + right" (now Gitea#20's territory), and why rotating the map to seat a viewer was refused. + +9-11, 12a-12g. ~~**The v0.4.9d and v0.4.9e gameplay-testing reports**~~ — twelve bugs, all shipped in + v0.4.9e / v0.7.1. Two trains in one station answering to one button; a Freight House unloading the + boxcar it just loaded; the Grocer's Warehouse shipping and the Refinery receiving; Gitea#4 extra + train starts; #7 coach counts on four train cards; #6 then #9 on discarding train cards (#9 + superseded #6 three days later — a Timetabled train may be tossed face-up to a Department slot, + an Extra may not); #8 the per-diem train and the caboose; and #10 the Day-rollover dialog. + Reasoning for each is in `docs/rules/implications.md` and `CHANGELOG.md`. + + **Gitea#2 carries a RULING worth keeping.** Four porters, two passengers on the platform, and + only one may be worked — RULED AND FIXED, but not the way the report implied. The engine is + faithful to the written rules at every step; what bites is that BOTH directions of porter work + move coaches one-way into a Classification Yard that comes back only when the Division Yard is + bare of all ~60 cars. Sixteen coaches in the game, and the reported save runs dry on Day 5 with + eight stranded. **Jesse's ruling: the shortage stays** — "it is possible to run out, that's part + of the strategy" — so the three balance options in Play Balance below are DECLINED, not deferred. + What was actually wrong is that the game said NOTHING: a Porter action that cannot be taken is + simply absent from the menu, and the "why is nothing moving?" panel covered freight facilities + only. That half is fixed. + +1-6. ~~**The 2026-08-20 multiplayer planning queue and the first StartOS play session**~~ — the New + Train phase car-placement round, unified victory conditions, Phase 2 server core, the lobby + offering every game parameter, deciding what the four `optionalRules` are (`sisterTrains` + deleted, `employeeRotation` implemented, the other two already live), and stopping every release + from destroying every game in progress — done by replaying the save rather than comparing version + strings. All shipped in v0.6.0; reasoning in Multiplayer below. --- @@ -1449,11 +1375,19 @@ Deferred while planning the server; decisions and reasoning are in `docs/archite ## Display -- [ ] **THE BOARD STILL DOES NOT SAY WHICH WAY A HEAVY GRADE CLIMBS.** RAR, twice: "grade should +- [x] **~~THE BOARD STILL DOES NOT SAY WHICH WAY A HEAVY GRADE CLIMBS.~~** — done 2026-08-30 in + v0.7.9 (`cf018b4`). A brown wedge in the card's lower right rising toward the climb, with an + arrow lying along its slope. **It is Gitea#18 that made this drawable**: east is always to the + right on a single-row map, so a wedge can be read without a compass — under the wrapped layout + the same wedge would have pointed a different way at every seat. Orientation was measured + rather than assumed (400 seeds x 4 player counts, 535 grades, 0 without one, 276 east / 259 + west). Original note below. + + **The board did not say which way a Heavy Grade climbs.** RAR, twice: "grade should tell you which way is up." `gradeUp` is dealt at setup and drives which of Helpers or Brakeman/Airbrakes can ever pay, and Gitea#3 made it matter more — the modifiers now move a train's STARTING REGION, so playing the wrong one is three Stages of climb instead of two. The - tooltip says it (`mainlineDescription`); the map does not. Untouched by Gitea#3, which was + tooltip says it (`mainlineDescription`); the map did not. Untouched by Gitea#3, which was about the rules rather than the drawing. @@ -1586,202 +1520,59 @@ What is on the screen and where. Split out of Other 2026-08-22; the rules are el **The open questions.** Which of the six stay on the top line — Revenue and the objective are glanced at constantly and clearly belong there, the seed and the code almost never are. Whether the card folds like `#district` does or is always open. Whether the collision counts belong in - it or beside the objective, since they are a live score rather than a setting. And it interacts - with the Display items about the right column and the middle of the Division map (item 22): if - the shared board moves to the centre, the right column is "yours", and a settings card is not - yours — it is the table's. + it or beside the objective, since they are a live score rather than a setting. And it inherits + the one principle that outlived the wrapped-map items (settled below): **SHARED in the middle, + YOURS on the right.** A settings card is not yours — it is the table's — so if the common board + of Gitea#20 ever comes back onto this page, the settings card belongs beside it rather than in + the right-hand column. -- [ ] **INVESTIGATE: the Fedora belongs at the right-hand end of the phase row.** Raised by Jesse - 2026-08-23, the day after it was added: "on the next row down, we recently added Superintendent - and saying who's got the Fedora. That information should be all the way on the right, where - currently it says 'Supervisor Shift'. Would it be possible to say 'Supervisor Shift — and then - the player name, who's the current supervisor'? If not, just moving the Superintendent and the - Fedora graphic to the right-hand side of Supervisor Shift is probably a better place for that." +- [x] **~~INVESTIGATE: the Fedora belongs at the right-hand end of the phase row.~~** — done + 2026-08-30 in v0.7.9 (`Next` #29). Jesse's fallback was the one taken: `.tc-super` moved to the + end of the row, after the pills, and wrapping under them rather than squeezing the chips on a + narrow screen. - **Where it is now.** `turnChartHtml` (`sim/turnchart.ts`, shared by the play screen and both - replay viewers) lays out four blocks in a row: the Day/Stage/clock, the phase, "waiting on - ", then the Fedora chip (`.tc-super`), and last the `
    ` of five phase - pills. So the Fedora sits in the middle of the row, immediately before the pills. + **The appealing version was tried and rejected.** Putting the name INSIDE the Supervisor Shift + pill reads as "this phase belongs to that player", which is not what the Fedora means — the + office holds the clearance ruling and starts every round, in every phase. Worth knowing if + anyone proposes it again. - **Putting the name IN the Supervisor Shift pill is the appealing version and needs thought.** - The pills are a WHERE-ARE-WE indicator — each lights violet while its phase is running and dims - once it is done — so a name inside one may read as "this phase belongs to that player", which is - not what the Fedora means (the office holds the clearance ruling and starts every round, in - every phase). The pill is also the one whose tooltip already explains the hat passing every - third Stage, which is why it is the natural home. Worth trying both and looking at them. + **The row was looked at as a whole**, per the note left here at the time, because item #15's + "most recent action" line still wants space in the same strip. Nothing was taken; the row has + room for one more block at the widths tested. - **The fallback Jesse names** — move `.tc-super` to the end of the row, after the pills — is a - two-line change and safe. Neither should be done without looking at the row as a whole: it is - the most-glanced-at strip on the page, and item 15's "most recent action" line wants space in - the same place. -- [ ] **THE DIVISION MAP DRAWS NO TRACK GEOMETRY — a turnout is indistinguishable from a straight.** - Reported by Jesse 2026-08-23, playing the v0.7.0 build on StartOS: he upgraded a straight on his - Running Track to a turnout and "did not see the division map on my side updated. And in the - opponent's screen, they did not see any update in their division map either." +- [x] **~~Five drawing items against the wrapped Division map~~ — ALL ANSWERED BY Gitea#18** + (closed 2026-08-26), which replaced the layout rather than fixing the drawing. They were: + the map drawing no track geometry; vertical track art on a card laid down a side lane; the + inter-row connector routed round the outside with angled corners; the Division Point captions + overflowing and the buffer stops pointing the wrong way once the route wrapped; and seating the + viewer at the bottom with the table wrapped around them. All five were the same pass, and + Jesse's instruction was to do them together — which is what made replacing the layout the + cheaper answer than drawing it five ways. - **It did update. The whole visible change is one word.** Measured rather than reasoned about — - a real two-player game played out ~260 turns, then a running-track straight upgraded to a - turnout, diffing the drawn SVG on both seats: + **What the single row settled.** The route is one line, west on the left and east on the right, + at every seat count (`board-svg.ts`). Both buffer stops face outward, so the two "wrong way" + reports cannot recur. The captions sit under their own Division Point rather than hanging off + the ends, so nothing clips. And **east is always to the right** — the report that decided it, + and the reason the horseshoe lost to a row that is 1,580px wide at four players against 842. + It is also what made the Heavy Grade wedge drawable at all (item above). - ``` - own division view : CHANGED - other's view : CHANGED - drawn SVG : CHANGED - removed: straight - added : turnout - ``` + **The Division map no longer draws office-area detail**, so there is no Running Track on it to + draw turnout geometry for. That belongs to the Office map, which reads `cell.links` and already + draws it properly. - **Why.** `divisionSvg` draws the SAME generic rail on every Running Track cell — - `rail(c.x + 6, c.y + 32, c.x + c.w - 6)`, unconditionally — and prints the card's name under it. - So a straight, a curve and a turnout are pixel-identical on the map and only the caption - differs. `officeSvg` does it properly for the district grid: it reads `cell.links` and draws the - real geometry, 45° legs included. + **Two things survive their items and are NOT closed with them:** - **The data is already there and unused.** `RunningCardView` carries - `links: connectionsFor(card)` — the same field `officeSvg` draws from — and `divisionSvg` never - reads it. So this is a rendering change with no plumbing behind it. + - **"SHARED in the middle, YOURS on the right"** — Jesse's principle from the dead-centre item, + and worth keeping as the rule that decides where anything new on the screen belongs, even + though a row has no centre to fill. It is now **Gitea#20**'s territory: the common board is + the shared thing, and it got its own display rather than a hole in the map. + - **Why seating the viewer at the bottom was refused**, since it will be proposed again: the + Division is a LINE, and the open gap between the two buffer stops is the only thing that says + so. Rotate the map to seat a viewer and that gap moves with them — sometimes behind them, + out of the eye's path, which is exactly where the one feature saying "this is not a circle" + must not go. Jesse called it definitively superseded, 2026-08-26. - **What is NOT wrong, checked at the same time:** a card laid anywhere BELOW the Running Track - correctly changes nothing on the map — the map is the through route between the Limits, and - district interiors live in the Office Area grid. Of 4,000 bot steps in a two-player game, all - 15 running-row plays changed both players' maps and none of the 19 below-the-row plays did. The - delta is not dropping anything: `deltaFrame` JSON-compares `division` and sends it whenever it - differs. - - **Do it with items 19, 20, 21 and 24** — Jesse's instruction, 2026-08-23. Drawing real geometry - is the same pass as turning that geometry through 90° down a side lane, and both are wasted - work if the map is later rotated to seat the viewer at the bottom. - -- [ ] **INVESTIGATE: turn the track art vertical on a Division card laid vertically.** Raised by Jesse - 2026-08-22: a side lane "looks like a bunch of disconnected left-right tracks stacked one on top - of another instead of looking like a continuous track." - - **It does, and the cause is one line.** `divisionSvg` draws every cell's rail with - `rail(c.x + 6, c.y + 32, c.x + c.w - 6)` — horizontal, unconditionally, whatever side of the - table the cell was laid on. **`railV` already exists**, is already used for the connector - between stacked cells, and takes the same shape turned ninety degrees. So the cell needs to know - which lane it is in (`dir[i]` is `'top' | 'right' | 'bottom' | 'left'`, known at layout time and - currently not stored on the cell) and pick the one that matches. - - Everything else inside a side cell — the name, the capacity line, the train chips — is laid out - horizontally too, so this is bigger than swapping one call: it is deciding whether a side cell is - a rotated card or a differently-arranged one. Worth a sketch before any code. - -- [ ] **INVESTIGATE: run the inter-row connector round the outside, as rail, with angled corners.** - Raised by Jesse 2026-08-22. **CONFIRMED IN PLAY 2026-08-23**, and it is worse than a drawing - complaint: "as a train traverses the board in a multiplayer game, east is not always to the - right. Sometimes, for train direction, east might be south, west, or north as it traverses the - different players." The route wrapping through the lanes is exactly this, and a player reading - direction off the screen is being told the wrong thing — not merely an ugly corner. - - **Where it comes out is wrong.** The turn between two sides of the table is drawn from - `a.y + CH` — the BOTTOM edge of the last cell in the top row — across to `b.y`, the TOP edge of - the first cell in the next. So the route leaves the top row's rightmost Limits downward out of - its underside, when it should leave from its right-hand side; and on the bottom row it enters - through the top edge when it should come in from the right. Jesse: "it should be on the outside - circumference of the display." - - **What it is drawn WITH is wrong too.** That connector is a single `` - — one plain polyline — while every other join on the map is `rail()` or `railV()`, two rails - with cross ties. The connector between two stacked cells down a side already uses `railV`, which - is why the vertical run looks like track and the corners do not. - - **And the corners want an angled piece**, Jesse's wish-list item: a 45° rail between the - horizontal and vertical runs rather than a right-angled elbow, matching how the district's own - track geometry works (everything leaving a card's north or south edge does so at 45°). That - would want a `railD` beside `rail` and `railV`. - - All three are the same drawing pass and should be done together. Note the current elbow routes - through `(ay + by) / 2` — the middle of the board — which is the space the item below wants to - fill, so these two interact. - -- [ ] **INVESTIGATE: the Division Point captions overflow, and the buffer stops point the wrong way - once the route wraps.** Raised by Jesse 2026-08-22. **CONFIRMED IN PLAY 2026-08-23** at two - players: "the eastern division point has the end marker to the right, so it overlays where the - track is into the person's area, instead of off the left at the actual end of the track." The - stop is drawn past the cell's right-hand edge whatever lane the cell ended up in, so at a - wrapped route it points back into the board — over a district, not away from the line. - - **The captions.** "west end · in and out" and "east end · in and out" are centred under their own - cell at `x = c.x + c.w / 2`. That was a deliberate fix — they used to hang off the outside of - the board and print clipped mid-word — and it works for a single row. It does not survive a - wrap: a Running Track cell is 78px wide and the caption is not, and `boardW`/`boardH` are - computed from CELL extents plus `PAD = 22`, never from the text. **The viewBox does not know the - captions exist**, so any caption wider than its cell plus the padding is outside it. This is the - same class of bug as the one the comment above the code says was already fixed once; the fix - solved the one-row case. - - **The buffer stops.** Both are drawn on the HORIZONTAL ends of their cell — `first.x - 9` and - `last.x + last.w + 9` — regardless of which side of the table the cell was laid on. With one row - that is right: the line ends to the left and to the right. Wrapped, the East Division Point can - end up on the bottom row or down a side, and its stop still points right, into the board rather - than away from the route. Jesse's read is that it belongs at the top there. The correct rule is - probably "away from the neighbour it joins", which is derivable rather than a special case — and - it depends on the vertical-card question above, so do that one first. - -- [ ] **INVESTIGATE: fill the dead centre of the Division map with the common board.** Raised by - Jesse 2026-08-22: with three or four seats the map is a ring with a large empty middle, while - the timetable sits in a column on the far right. - - **The proposal, in his words:** move the timetable into the middle; maybe the yards, "because - that is too applicable to everybody"; maybe the Department and Salvage decks as well, since - those apply to all players. "Then the Division map becomes the common board area. The - right-hand side — your move, your cards, your facilities, what's blocked — is your side of - things." - - **That is a genuinely good division of the screen** and it names a principle the layout does not - currently have: SHARED in the middle, YOURS on the right. Worth writing down as the rule even if - the move itself is deferred, because it decides where anything new belongs. - - **What makes it awkward.** The middle is only empty at three and four seats — at one seat there - is no middle at all, and at two the rows face each other across a gap the width of `SIDE_GAP`. - So whatever goes there needs somewhere else to live at low seat counts, which is close to - building both layouts. And the map is an SVG built by `divisionSvg` while the timetable, yards - and decks are HTML panels (`panels.ts`), so "put them in the middle" means either - foreign-objecting HTML into the SVG or positioning HTML over it — neither free, and the map is - embedded into the replay by `toString()`, which constrains what it may reach for. - -- [ ] **INVESTIGATE: seat the viewer at the bottom of the Division map and wrap the table around - them.** Raised by Jesse 2026-08-22: "consider that the player being displayed is always at the - bottom of the display, and the rest of the table is wrapped around. This would give something - of a feel of sitting at an actual table with my cards in front of me and the other players in - front of me as well." His own note: not a decision, something to think about. - - **It needs no new data.** `divisionSvg` already takes `roster.viewer` — "the player this map is - being drawn for" — and `main.ts` already passes it. The layout is a fixed lane order, - `top → right → bottom → left`, filled in ROUTE order west to east; rotating means offsetting - which lane the first seat lands in so the viewer's own district comes out on `bottom`. That is - an index shift in one array, not a new layout engine. - - **The strongest argument for it is already in the code.** The map marks your district with a - colour AND spells out "(you)", and the comment says why: "a colour alone cannot say which of - four railroads is the reader's, and that is the first thing anybody wants to know at a table - they just sat down at." A fixed position answers that structurally — you would know before - reading anything. The marker stays as reinforcement rather than being the only signal. - - **What it costs is the other thing the map says.** The section is headed "The Division — west - to east" and the route is a LINE, not a loop: it starts at the West Division Point and ends at - the East one, with buffer stops at both and a deliberately open gap between them. Today that - line starts top-left, where a reader starts reading. Rotate it and west starts wherever your - seat put it — and the open gap, which is the thing that stops the ring being read as a loop, - moves with it. Sometimes it would land behind you, out of the eye's path, which is exactly - where the one feature that says "this is not a circle" should not be. - - **So the question is which of the two the map is FOR**, and it may not have the same answer at - every seat count. At one seat there is nothing to rotate. At two it is a swap, and free. At - three, rotating changes which way the horseshoe opens. At four it moves the break in the - square. A rule like "rotate at three and four, leave one and two alone" is defensible but has - to be decided rather than fallen into. - - **One caller has no viewer at all**: the site's replay viewer calls `divisionSvg(f.division)` - with no roster (`web/replays.ts`). A replay watches every seat and belongs to none, so it has - nothing to rotate around — the feature has to degrade cleanly to today's layout there, which is - an argument for building it as an optional rotation rather than as the layout. - - **Same drawing pass as items 19, 20 and 22** — vertical track art, the corner connectors and - the dead centre. All four move cells around the ring or change what is drawn inside them, and - doing them one at a time means laying the map out four times. - [ ] **INVESTIGATE: history newest-at-the-top, and timestamps on it.** Raised by Jesse 2026-08-22. diff --git a/src/sim/board-svg.ts b/src/sim/board-svg.ts index 2d04de4..f226261 100644 --- a/src/sim/board-svg.ts +++ b/src/sim/board-svg.ts @@ -55,11 +55,12 @@ export function divisionSvg(nodes: DivisionView[], roster?: DivisionRoster | nul * * West DP · Mainline · [Limits … Office … Limits] · Mainline · [ … ] · Mainline · East DP * - * SEATING. Players sit around a table, so the route is laid out the way they do: one row alone, - * two rows facing, a horseshoe of three, a square of four. The Division is a LINE and not a loop - * — trains enter at one Division Point and leave at the other — so the shape is deliberately left - * open, with the two ends drawn as buffer stops facing each other across a marked gap. Closing it - * into a ring would promise a connection the rules do not have. + * ONE ROW AT EVERY SEAT COUNT (Gitea#18). It used to be laid out the way players sit — two rows + * facing, a horseshoe of three, a square of four — and the reasoning for dropping that is at the + * layout itself below. The Division is a LINE and not a loop — trains enter at one Division Point + * and leave at the other — so the row is deliberately left open, with the two ends drawn as + * buffer stops facing outward. Closing it into a ring would promise a connection the rules do + * not have. * * Self-contained on purpose: the replay embeds this by `toString()`, so it may not reach for * anything outside its own body. @@ -108,7 +109,6 @@ export function divisionSvg(nodes: DivisionView[], roster?: DivisionRoster | nul * this is back to what a buffer stop actually needs. */ const PAD = 22; - const SIDE_GAP = 34; const esc = (t: string): string => String(t).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] ?? c);