v0.6.2 — an Extra starts where you put it, a train card is never discarded
Three more from the v0.4.9e gameplay-testing round, filed as Gitea issues, plus two bugs found underneath them. Gitea#2 is diagnosed but NOT fixed: it needs a ruling, and the reasoning is in TODO.md under Play Balance. GITEA#4 — AN EXTRA STARTS WHERE THE PLAYER PUTS IT. Only one Division Point was ever offered, chosen by number parity. The number no longer decides an Extra's direction — the start does, which supersedes the recorded ruling that "the number decides, like everything else on the timetable". The two cannot both hold: an odd, westbound Extra placed at the WEST end would leave the Division on its first move having crossed nothing, and be paid for the run. Either end now runs the train away from itself; at an Interchange or a Control Point the player picks the direction. The Interchange start is a YARD, off the running line, which is what makes the Superintendent clause work: placing it can never force a collision, a guaranteed one holds it there for another Stage, and a potential one is the Superintendent's to rule on — exactly evaluateClearance's `blocked` and `ask`, so nothing new decides collisions. Where an Extra may start is now a house rule (divisionPointsOnly / ownOffice / anyOffice, defaulting to what the engine already did). The legacy `atSeat` intent field still replays as it always meant. FOUND UNDERNEATH IT: an Extra started away from a Division Point ran empty. isBeingMadeUp tested position alone, so the Control Point start has been shipping since it was added with a train that could never be given a consist. Found by playing it, not by the tests, which had only asserted where the tray landed. FOUND UNDERNEATH IT: collide left the wrecks on the card. Destroyed trains kept their Transit entries, and evaluateClearance counts every transit as an occupant, so one rear-end collision permanently poisoned that Mainline card for every later train. THE MAINLINE CARDS WERE ROLLED, NOT DEALT — drawn from the nine types with replacement, so a Division could hold two Interchanges and Plains carried the weight of a card printed once. "An Extra may start at the Interchange if one is on the board" only reads as a rule if the board holds at most one. Now dealt from the printed deck without replacement, verified over 1600 deals. This re-deals every seed: the published replays were re-recorded, and the saved games in docs/ are retired too — two of those were already dead before this release and nobody had noticed. GITEA#6 — A TRAIN CARD IS NEVER DISCARDED, Timetabled and Extra alike. The forced play needed no mechanism: nothing discardable plus a hand over the limit leaves exactly one legal way to end the turn, and playing a train is unconditionally legal, so the corner cannot trap anyone. The bot needed no rule either. 400/400 games finished, revenue unmoved, trains scheduled 1.2 -> 1.3. The player is told on the card and on the button. GITEA#7 — COACH COUNTS. 1/2 Crack Limited 3 -> 2, 5/6 The Sparrow 2 -> 3. A change to the cards, so Trains3.pdf and the transcription keep the original numbers with a footnote while content.ts and the Home Deck reference carry what the game plays. CONTENT.TS COMMENT PASS — no data changed, only comments. Four were factually wrong, including an office table naming counts doubled long ago and a pointer to a DEALT_DECK_SIZE that has never existed. Every Enhancement row cited its implementation by line number and every citation had rotted; they name functions now. Card counts came out of the comments, since they move with play balance; source-sheet figures and dated measurements stayed. TODO.md gains an item for a card reference generated from content.ts, in six sections, so the documentation cannot disagree with the game. 715 tests pass, tsc clean, site builds.
This commit is contained in:
@@ -4,7 +4,9 @@ Things worth coming back to. Anything noted here should either get done or get a
|
||||
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, Rules Questions, Other — and ordered within each by how much it is currently costing us.
|
||||
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
|
||||
@@ -49,12 +51,111 @@ Queued 2026-08-22, from a v0.4.9d gameplay-testing report (six bugs, forwarded b
|
||||
12. **NOT REPRODUCED: cars left behind when backing up over them** — see Rules Questions below. The
|
||||
one report of the six that is still open, and it 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.**
|
||||
DIAGNOSED, AWAITING JESSE'S RULING — see Play Balance below. 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.
|
||||
|
||||
13. **Show me the other players' moves, bots included** — raised by Jesse 2026-08-22 from playing a
|
||||
multiplayer game. 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** — a side lane currently reads
|
||||
as stacked left-right segments rather than one continuous run.
|
||||
20. **Run the inter-row connector round the OUTSIDE**, draw it as rail rather than a plain line, and
|
||||
give the corners an angled piece.
|
||||
21. **The Division Point captions overflow the map**, and the buffer stops point the wrong way once
|
||||
the route wraps.
|
||||
22. **Fill the dead centre of the Division map with the common board** — timetable, yards, the
|
||||
Department and Salvage decks. Then the map is what everyone shares and the right column is yours.
|
||||
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, so the screen sits you at the table. Needs no new data; the cost is that "west to east"
|
||||
stops starting where you start reading.
|
||||
|
||||
---
|
||||
|
||||
## Replay / Save Games
|
||||
|
||||
The replay viewer, the save format, and how a game gets shared.
|
||||
|
||||
- [ ] **INVESTIGATE: stamp the history with wall-clock time.** Raised by Jesse 2026-08-22: "store a
|
||||
date/time stamp with the history information. Even if it's not displayed immediately, someone
|
||||
could tell afterwards, or later if we decide to display it — how long things took between
|
||||
different turns and actions."
|
||||
|
||||
**Half of this is already built and nothing reads it.** `server/session.ts` closes a
|
||||
`TurnTiming { player, phase, day, stage, startedAt, endedAt }` every time the acting player,
|
||||
phase, Day or Stage changes, and `persistence.appendTiming` writes each one to the game's
|
||||
timings file. So every multiplayer game on the box already has per-turn wall-clock on disk.
|
||||
Nothing displays it, and nothing has ever read it back. **First job is to look at that file
|
||||
from a real game** — the answer to "how long do turns take" may be sitting there already.
|
||||
|
||||
**Solitaire has none of it.** `game.log` is `{ text, tone }[]` and a save is
|
||||
`{ seed, config, history }`. There is no clock anywhere on that path.
|
||||
|
||||
**The constraint, and it is deliberate rather than an oversight** (stated twice in
|
||||
`server/session.ts`): wall-clock is kept OUT of `history` because a replay must reproduce a game
|
||||
from decisions alone. Do not put a timestamp on an `Intent`. It would ride into every save,
|
||||
change the save format, and make two recordings of the same game unequal for no gain — the
|
||||
engine has no clock and must stay deterministic.
|
||||
|
||||
**And a timestamp on a log LINE does not survive.** `game.log` is rebuilt by `fromSave` on
|
||||
undo, on restore and in the replay viewer, so a time recorded on a line is gone the first time
|
||||
the player takes a move back. Whatever is built has to be a SIDECAR — times keyed by intent
|
||||
index, written only on the live path — and every consumer has to handle its absence, because a
|
||||
replayed or imported game legitimately has no clock at all. That absence is the honest answer,
|
||||
not a hole to fill with `createdAt` (the same call `lastMoveAt` already makes, and for the same
|
||||
reason).
|
||||
|
||||
**The open question is granularity**, and it is Jesse's to settle: per INTENT (every draw,
|
||||
every Move — finest, biggest sidecar, and the only thing that answers "how long between
|
||||
actions") or per TURN SPAN (what already exists, one row per player-phase, and enough for "how
|
||||
long between turns"). Per span is free today; per intent is new work on both the solitaire and
|
||||
the multiplayer paths.
|
||||
|
||||
**What it would feed if built:** the "Games in Progress" admin view (item 7 above, which already
|
||||
shows `lastMoveAt`), a post-game "that Day took 40 minutes" summary, and any future pacing
|
||||
question about whether a 12-Stage Day is too long at a real table — which is the sort of thing
|
||||
only a table can tell us and only a clock can record.
|
||||
|
||||
- [ ] **INVESTIGATE: how would a player publish a replay so other people can watch it?** Today
|
||||
"Save replay" downloads a JSON file to the player's own machine, and the only way it reaches
|
||||
the site is by sending it to Jesse to drop into `public/replays/` and redeploy. The question is
|
||||
@@ -330,6 +431,41 @@ number until the rules stop moving.
|
||||
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.
|
||||
- [ ] **Gitea#2 — passenger operations starve themselves of coaches, and the game says nothing.**
|
||||
Reported from v0.4.9e play: the Sparrow pulls into the Terminal with two loaded coaches, two
|
||||
passengers wait on the platform, four porters are unused, and only ONE of the four intended
|
||||
actions can be taken. Reproduced from the save (`docs/station-master-seed947338225-day5(1).json`,
|
||||
Day 5 Stage 11): the Division Yard holds **1 empty coach and 0 loaded**, while the
|
||||
Classification Yard holds **4 loaded and 4 empty** that cannot come back.
|
||||
|
||||
**The engine is not deviating from the rules.** Checked step by step: §9.2 discards the white
|
||||
coach into the Classification Yard on boarding, draws the white coach from the Division Yard on
|
||||
de-training, and §2.2 returns the Classification Yard only when the Division Yard is empty. All
|
||||
three are implemented exactly. The problem is the interaction — a single global refill
|
||||
condition over a pile holding six commodities with very different demand, where coaches (16 of
|
||||
~60 cars) are consumed by both halves of every passenger cycle. Traced over the reported game
|
||||
the coach pool goes 8+/8− to 0+/1− by Day 5.
|
||||
|
||||
**Three ways out, and it is Jesse's call which:** (a) refill when the Division Yard is dry of
|
||||
the type-and-state being asked for rather than dry of everything — the reading a player
|
||||
rummaging a table-top pile actually uses, and the biggest balance change; (b) the same trigger
|
||||
but return only the cars of that type; (c) leave the rules alone and raise the coach count in
|
||||
`ROLLING_STOCK_SUPPLY`, which the item below already sanctions — lowest risk, but it delays the
|
||||
wall rather than removing it. Measure (a) or (b) over 400 paired seeds before shipping.
|
||||
|
||||
- [ ] **A blocked PASSENGER facility produces no impediment at all.** `impediments()`
|
||||
(`src/sim/narrate.ts`) opens with `if (!f || f.kind !== 'freight') continue`, so the "why
|
||||
nothing is moving" panel has never had anything to say about a platform. That is the second
|
||||
half of Gitea#2 and the half that is unambiguously a bug: the player above was not merely
|
||||
blocked, he was given no reason — the button simply was not there. Worth fixing whichever way
|
||||
the supply question is settled.
|
||||
|
||||
- [ ] **The log lowercases the first letter of every narration it attributes to a player**, so
|
||||
`EXTRA X18 started…` renders as `Player Solitaire eXTRA X18 started…` (`src/web/game.ts`:1065,
|
||||
`n.text.charAt(0).toLowerCase()`). Harmless-looking and it hits every line that opens with an
|
||||
all-caps keyword — `TRAIN 1 MADE UP`, `COLLISION`, `EXTRA`. Found while playing the Interchange
|
||||
start; predates it. Wants a rule that leaves an already-capitalised word alone.
|
||||
|
||||
- [ ] **The rolling stock supply is a guess.** `ROLLING_STOCK_SUPPLY` (coach 8+8, boxcar 10+10,
|
||||
hopper 8+8, reefer 5+5, tank 6+6, caboose 6) is marked provisional in `content.ts` and was
|
||||
scaled alongside the Gap 12 industry increase. Now that the Classification Yard returns stock
|
||||
@@ -391,6 +527,40 @@ number until the rules stop moving.
|
||||
|
||||
Deferred while planning the server; decisions and reasoning are in `docs/architecture/multiplayer.md`.
|
||||
|
||||
- [ ] **I CANNOT SEE WHAT THE OTHER PLAYERS DID — BOTS INCLUDED.** Raised by Jesse 2026-08-22 from
|
||||
playing a multiplayer game on StartOS: "on my display I need to see other players' moves, even
|
||||
if they are a bot."
|
||||
|
||||
**What the code already does**, checked rather than assumed: `game.log` is ONE shared log and
|
||||
`linesSince(seat)` (`server/session.ts`) sends every seat everything in it, so a bot's turn is
|
||||
not silently dropped — `driveBots` plays through `submit()`, which calls `record(game, events,
|
||||
actor)`, and `record` prefixes any event carrying a `player` with "Player <name>". So the moves
|
||||
*are* arriving, attributed, in the history panel. Whatever is wrong is not that they were never
|
||||
sent, and that is worth knowing before anything is built.
|
||||
|
||||
**What is genuinely missing is the BOARD.** `snapshot(s, …, viewer)` builds `cells` from
|
||||
`areaOf(s, viewer)` alone, so a Frame contains the viewer's own Office Area and nobody else's.
|
||||
Another player can move a train the length of their district and the only trace on your screen
|
||||
is a line of text. The Division map is the one shared picture, and it shows trains on the
|
||||
Mainline, not switching inside a district.
|
||||
|
||||
**Not yet established: which of the two Jesse means.** "I need to see other players' moves" fits
|
||||
both "the history panel is not telling me" (a legibility problem — the panel scrolls, a bot can
|
||||
take a dozen actions between your turns, and nothing marks where your last turn ended) and "I
|
||||
want to watch their railroad" (a Frame problem). Ask before building: the first is an afternoon,
|
||||
the second is a new view.
|
||||
|
||||
**The constraint on the second**, and it is the one that must not be got wrong: a district's
|
||||
BOARD is public — cards on the table, cars standing on them, trains — and a player's HAND,
|
||||
Revenue detail and drawn cards are not. `test/redaction.test.ts` exists precisely to catch a
|
||||
Frame that leaks the wrong half, and it works by serialising a seat's whole Frame and asserting
|
||||
no other seat's secrets appear anywhere in it. Any "show me their district" feature has to
|
||||
extend that test in the same commit, not after it.
|
||||
|
||||
**A cheap first move that is right either way:** mark the log where the viewer's own last turn
|
||||
ended, so "what happened while I was waiting" is a readable block rather than a scroll. That
|
||||
needs no new data on the Frame — `sentLines` already knows the boundary.
|
||||
|
||||
- [ ] **A LOST SESSION TOKEN LOCKS A PLAYER OUT OF A RUNNING GAME PERMANENTLY.** Raised by Jesse
|
||||
2026-08-22: "if I opened a fresh browser window and wanted to resume HOPPER-4607, how would
|
||||
the server know which player I am and which game I'm trying to get to?"
|
||||
@@ -862,6 +1032,246 @@ Deferred while planning the server; decisions and reasoning are in `docs/archite
|
||||
|
||||
---
|
||||
|
||||
## Display
|
||||
|
||||
What is on the screen and where. Split out of Other 2026-08-22; the rules are elsewhere.
|
||||
|
||||
- [ ] **INVESTIGATE: a "most recent action" line under the status block.** Raised by Jesse
|
||||
2026-08-22: a line below the status block ("Day, Stage, phase, waiting on") and above the
|
||||
Division map, carrying the same kind of text the history does — *"Jesse drew from the Home
|
||||
Office deck"*, *"played right-hand turnout at (−3, 0)"*. His own note: "I'm not sure that's
|
||||
what's going to make the most sense, but I think it's something that should be investigated."
|
||||
|
||||
**The text already exists and is already correct.** `describeIntent` and `narrate` produce
|
||||
exactly those sentences, and `record()` attributes them with the player's name. Nothing new has
|
||||
to be written to say what happened — this is placement, not content.
|
||||
|
||||
**The slot is real but crowded.** Between `#turnchart` and `<main>` in `play.html` there are
|
||||
already three transient banners: `#phasenote` (a phase CHANGED, auto-hides), `#announce` (a
|
||||
one-shot announcement — a train completing its run pays everyone), and `#presence` (someone is
|
||||
disconnected). A permanent fourth line has to not compete with them, and the palette is already
|
||||
spoken for: violet reports where you are, amber means clickable, green and red mean good and
|
||||
bad (`turnchart.ts`). A "what just happened" line is none of those.
|
||||
|
||||
**Two placements, and they are different features.** Put it in the page and it is the play
|
||||
screen's. Put it in `turnChartHtml` and it appears in **both replay viewers** too, which is
|
||||
probably a feature — a replay stepping frame by frame has exactly this question — but it makes
|
||||
the change three screens wide.
|
||||
|
||||
**The question behind it is the unit, and it is why this should be decided with item 13.** In
|
||||
solitaire "most recent action" is right: you took it, you are looking straight at it. In
|
||||
multiplayer the thing you actually missed is everything that happened while you were WAITING,
|
||||
which is many actions and possibly a whole bot turn — and one line showing only the last of
|
||||
them may be the least useful line on the page. Item 13's cheap first move (mark the log where
|
||||
your own last turn ended) answers that better. They may both be right, and one may make the
|
||||
other pointless; deciding them separately risks building both and needing neither.
|
||||
|
||||
- [ ] **INVESTIGATE: three explicit display options for the Office map — always hidden, always on,
|
||||
auto-hide.** Raised by Jesse 2026-08-22.
|
||||
|
||||
**All three modes already exist.** `districtMode` is `'auto' | 'open' | 'closed'`, persisted to
|
||||
`localStorage` with the sound and zoom settings (`main.ts`). Nothing needs adding to the model.
|
||||
|
||||
**What is wrong is that the button is a CYCLE, and it cannot reach every state.** The handler is
|
||||
`districtMode = districtMode === 'auto' ? (open ? 'closed' : 'open') : 'auto'` — so from `auto`
|
||||
you land on whichever pin is the OPPOSITE of what auto is doing right now, which depends on the
|
||||
phase, and every second press goes back to `auto`. You can never get from `open` to `closed`
|
||||
without passing through `auto`, and which of the two you can reach at all changes as the game
|
||||
moves between phases. That is why it does not feel like a setting.
|
||||
|
||||
**Likely three buttons or a three-way segmented control**, one per mode, showing which is
|
||||
current — the label work is already done and is worth keeping: it says what pressing it DOES
|
||||
("always showing — click for auto-hide") rather than what the panel is currently doing, which
|
||||
was a deliberate fix and should survive whatever replaces the cycle.
|
||||
|
||||
- [ ] **INVESTIGATE: the same three options for the Division map, where `auto` means something
|
||||
different.** Raised by Jesse 2026-08-22.
|
||||
|
||||
**Today it cannot be hidden at all.** `#division` is a plain `<div>` in an unnamed `<section>`
|
||||
in `play.html` with no toggle and no fold rule — `#district` has `.folded` styling and a button,
|
||||
the Division has neither.
|
||||
|
||||
**`auto` here is not the Office's `auto`, and that is the point.** The Office folds by PHASE
|
||||
(`FOCUS_PHASES` — open during Local Operations and Cargo, folded otherwise). Jesse's Division
|
||||
rule is "hide it NOW, and bring it back at the end of this phase": you fold the map away to get
|
||||
room while switching or working cargo, and it returns of its own accord when you are done. So
|
||||
it is a one-shot with an expiry, not a standing rule — the state has to remember WHICH phase it
|
||||
was hidden during, and clear itself when `f.phaseKey` moves off that one. Different enough from
|
||||
`districtMode` that sharing an implementation with it would probably be a mistake.
|
||||
|
||||
**Hidden and shown stay put** until pressed again, exactly as the Office's pins do.
|
||||
|
||||
- [ ] **INVESTIGATE: give every phase a visible beat — perhaps one second.** Raised by Jesse
|
||||
2026-08-22, watching a game play: New Train, Mainline and the shift change "look like they are
|
||||
being skipped entirely". His suggestion: move to the phase, take a visible beat so the second
|
||||
row shows it changed, then move on.
|
||||
|
||||
**They are not too fast. They are never drawn.** `pump()` (`advance.ts`) loops `advance()` until
|
||||
something needs input, and `drain()` renders ONCE after the whole batch. So every automatic
|
||||
phase between one click and the next resolves without the page ever painting it. A minimum dwell
|
||||
time on its own therefore fixes nothing — the page has to step `advance()` one call at a time
|
||||
and render between, which makes this an async pump with a queue rather than a `sleep`.
|
||||
|
||||
**`#phasenote` already exists for exactly this feeling** — it announces that the phase CHANGED,
|
||||
because "the page can change out from under a player between one click and the next" — but with
|
||||
only the final phase ever drawn it can only ever announce the last transition of the batch.
|
||||
Stepping the pump is what would let it announce each one.
|
||||
|
||||
**This is where it meets item 15.** Jesse's own example: during the New Train beat the "most
|
||||
recent action" line would read *"no new trains to build out"* — which is a sentence nothing
|
||||
currently produces, because a phase that does nothing emits no event to narrate. Some of these
|
||||
beats would need a line written for them, and deciding which is part of the same investigation.
|
||||
|
||||
**The obvious risk, worth stating before anyone builds it:** a second per phase is four seconds
|
||||
of enforced waiting per Stage, forty-eight per Day, and a player who has seen it a hundred times
|
||||
will want it off. Whatever this becomes probably needs a speed control, or to scale with whether
|
||||
anything actually happened in the phase.
|
||||
|
||||
- [ ] **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.
|
||||
|
||||
**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 `<path class="bs-turn">`
|
||||
— 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.
|
||||
|
||||
**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.
|
||||
|
||||
**Timestamps** are item 14 above — the same question, and it should be answered once. Whether
|
||||
the history DISPLAYS a time is downstream of whether one is recorded at all, and of the sidecar
|
||||
constraint written up there.
|
||||
|
||||
**Order.** Today `main.ts` renders `session.lines().slice(-60)` oldest-first and then sets
|
||||
`log.scrollTop = log.scrollHeight`, so the newest line is at the bottom and the panel scrolls
|
||||
itself down to it. Reversing is nearly free — reverse the slice, drop the auto-scroll — and it
|
||||
does what Jesse wants: a glance at the top is always the most recent thing.
|
||||
|
||||
**Two things that are not free.** The cap is `slice(-60)`, so "scroll back through the history"
|
||||
reaches sixty lines and stops however it is ordered; a real scrollback means raising or removing
|
||||
that cap and deciding what the panel does with a thousand lines. And the log carries PHASE
|
||||
HEADINGS (`t-phase`) that read forwards — a heading introduces the lines under it — so reversing
|
||||
the list puts each heading below the lines it announces. That has to be handled or the panel
|
||||
reads as nonsense at exactly the boundaries it exists to mark.
|
||||
|
||||
**Worth deciding together with item 15**, which proposes pulling the single most recent line out
|
||||
of this panel entirely. If that lands, the argument for reversing the panel is weaker.
|
||||
|
||||
---
|
||||
|
||||
## Rules Questions
|
||||
|
||||
- [ ] **NOT REPRODUCED: "when I back up to collect standing cars and, further down the tracks, the
|
||||
@@ -905,6 +1315,43 @@ Deferred while planning the server; decisions and reasoning are in `docs/archite
|
||||
|
||||
Doesn't fit the above.
|
||||
|
||||
- [ ] **Documentation generated from the implementation, not written alongside it.** Raised by Jesse
|
||||
2026-08-22, immediately after Gitea#7 changed the coach counts on four train cards and the
|
||||
answer to "where do we keep track of that?" turned out to be **five places of three different
|
||||
vintages**: `src/engine/content.ts` (the truth), `docs/StationMaster-Home-Deck-v0.4.5.md` (a
|
||||
readable per-card table, a version-stamped snapshot), `docs/rules/implications.md` §5 (the
|
||||
transcription of `Trains3.pdf`, deliberately frozen at what the design SAYS),
|
||||
`docs/Trains3.pdf` (the artwork), and `docs/rules/card-reference.md` (an invented placeholder
|
||||
catalogue, banner-marked SUPERSEDED, whose train table still looks authoritative if you land in
|
||||
the middle of the file). Every hand-maintained one of those drifts the moment a card changes,
|
||||
and this release proved it.
|
||||
|
||||
**The deliverable, at minimum: a reference document for every card that can be played**,
|
||||
generated from `content.ts` so it cannot disagree with the game. Sections, in order:
|
||||
|
||||
1. **Mainline cards** — the Division's own deck, dealt at setup rather than held in hand.
|
||||
2. **Home Deck Cards — Trains**
|
||||
3. **Home Deck Cards — Track**
|
||||
4. **Home Deck Cards — Industry**
|
||||
5. **Home Deck Cards — Modifiers**
|
||||
6. **Home Deck Cards — PVP**
|
||||
|
||||
Each card wants its name, what it does, where it may be placed, and — the part only the
|
||||
implementation knows — **whether its printed effect actually resolves yet**. `content.ts`
|
||||
already carries that last one for Enhancements (`EnhancementRule.effect`, live / dormantSolo /
|
||||
unbuilt, each row citing the file that reads it); the same honesty is what makes a generated
|
||||
reference worth more than a transcription. `enhancementText()` and `mainlineDescription()` are
|
||||
the model: prose composed from the data, so a tooltip cannot drift from the rule it describes.
|
||||
|
||||
**Deliberately NOT including card counts per category.** Jesse's call in the same breath: the
|
||||
counts move with play balance, so a document that prints them is stale on the next retune. The
|
||||
same rule was applied to `content.ts`'s own comments on 2026-08-22 — see the pass recorded in
|
||||
CHANGELOG for what came out and what was kept.
|
||||
|
||||
Not started. Worth deciding first whether this is a build step writing Markdown into `docs/`,
|
||||
or a page on the site beside the replay viewer — the site can render it from the same view-model
|
||||
the game uses, which argues for the second.
|
||||
|
||||
- [ ] **Real audio, as committed assets.** Everything the game plays is synthesised from oscillators
|
||||
(`src/web/sound.ts`), which was the honest choice for a site that fetches nothing — but it is a
|
||||
placeholder, not the finished sound. Sound therefore defaults to OFF.
|
||||
|
||||
Reference in New Issue
Block a user