v0.7.0 — four game types, a lobby you can read and leave, and a multiplayer game that makes a sound

The multiplayer set-up, the lobby, the start of a game, and four signals a remote client had never
been sent. Reasoning, the preset table and what was verified how: CHANGELOG.md.

- Co-op, Competitive, Cutthroat, Solitaire and Custom, on both screens, from one shared block —
  they had drifted, and each was missing a question the other asked.
- A player reads the whole rule set before taking a seat, may leave a lobby or a running game, and
  keeps a seat across a reload. The host may clear a chair. The browser remembers every game it is
  in, not just the last one.
- The start of a game is drawn: a handoff beat, an announcement, the code and type in the header.
- Sound, the timetable flash, announcements and the just-drawn badge now reach a remote client;
  justDrawn goes to the seat that drew it and nobody else.
- Played on StartOS, which found the rest: an Extra belongs to the player who played it, the board
  never named the Superintendent, bot seats were reported as absent players, and rule section
  numbers are out of every string a player reads.

Also carries the previous session's Heavy Grade documentation work — asked again, answer unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016JczK5i33ZNSf2PtzZqdhS
This commit is contained in:
Jesse.Markowitz
2026-08-23 05:46:42 -04:00
co-authored by Claude Opus 5
parent 42adfda390
commit 06db36e5b5
41 changed files with 4387 additions and 588 deletions
+213 -5
View File
@@ -37,9 +37,11 @@ Queued 2026-08-22, from playing on StartOS:
7. **Make "Games in Progress" readable** — nested groups rather than one run-on line per game, plus
sorting by name, start time or last move. Reasoning in Multiplayer below. Small, and it is the
action most used for actual administration.
8. **Give a player a way back into a game after losing their browser** — today a fresh browser is
locked out permanently, even though the server still knows who they are. Reasoning in
Multiplayer below; needs Jesse's call on whether a token in a URL is acceptable.
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.
Queued 2026-08-22, from a v0.4.9d gameplay-testing report (six bugs, forwarded by Jesse):
@@ -110,6 +112,24 @@ Queued 2026-08-22, from a session looking at the screen rather than the rules. A
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.
Queued 2026-08-23, from Jesse playing the v0.7.0 build on StartOS. **All three are the same drawing
pass as 19-21 and 24 above, and he asked for them to be discussed together rather than picked off:**
25. **The Division map does not draw track geometry at all** — a turnout laid on the Running Track
looks exactly like the straight it replaced, because the only thing that changes is a caption.
Reasoning and the measurement in Display below.
26. **CONFIRMED IN PLAY: the buffer stop points the wrong way** at two players — see item 21, now
reported from a real game rather than read off the code.
27. **CONFIRMED IN PLAY: east is not always to the right.** As a train crosses the Division the route
wraps through the lanes, so a player's east can be drawn south, west or north. See item 20; it is
the same wrap that puts the buffer stop in the wrong place.
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**, with (or in) the Supervisor Shift
pill. Reasoning in Display below.
---
## Replay / Save Games
@@ -565,6 +585,19 @@ Deferred while planning the server; decisions and reasoning are in `docs/archite
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?"
**PARTLY FIXED 2026-08-23, and the fixed half was the more common one.** A browser that
reloaded while SEATED IN A LOBBY used to orphan its chair outright — the token lived in a
closure and was only written to `localStorage` at `Lobby.Start`, so the player could not
return and nobody could free the seat, on a table that cannot start until every chair is
taken. The record is written at create/join now, carries `stage`, and `start()` walks the two
probes (`/api/session`, then the lobby stream) to land the browser wherever its seat actually
is. **A player may also LEAVE now** (`/api/lobby/leave`), and the host may clear a chair, so a
stranded seat is no longer permanent for the rest of the table either.
**What is left is exactly the case Jesse asked about**: a genuinely fresh browser, on a
RUNNING game. Everything below still stands, and still needs his call on whether a token in a
URL is acceptable.
**A new tab or window of the SAME browser is fine** — `localStorage` is per-origin and shared
across a profile, so `start()` finds the token and rejoins automatically. **A genuinely fresh
browser is not**: another browser, a private window, another device, or cleared site data.
@@ -634,6 +667,58 @@ Deferred while planning the server; decisions and reasoning are in `docs/archite
almost nothing — and sorting by last move ascending is how you find the game nobody has
touched, which is the main reason to open this action at all.
- [x] **~~The lobby, the setup form and the start of a game~~ — done 2026-08-23 (Jesse's cleanup
pass).** Kept for the reasoning, since several of these were decisions rather than fixes.
**The game types.** Co-op, Competitive, Cutthroat, Solitaire and Custom (`src/web/presets.ts`),
replacing the old two-mode radio. A type is a set of DEFAULTS, not a ruleset: every rule stays
editable, and editing one selects Custom, which keeps the scoring of the type it came from.
Jesse's numbers — Co-op pays 1 per transit and asks 3 combined Revenue per player per Day;
Competitive asks 2 and pays nothing for transits; Cutthroat asks nothing at all and lifts the
whole-game collision cap, leaving three-in-one-Day as the only shared way to lose; every type
deals six cards. **The Revenue floor is a formula**, so table size and Day count re-derive it
rather than making a game Custom — which is why those two, and the seed, sit above the type
radios as parameters. The type is DERIVED from the numbers, never stored, so no saved game
carries a label that can disagree with itself (`test/presets.test.ts`).
**Where an Extra may start was missing from the lobby entirely**, so every multiplayer game
ever played used the most permissive setting (`anyOffice` — an Extra may be planted in another
player's district) and no host was ever asked. It is a Cutthroat-only default now. The reverse
hole existed too: the solitaire dialog had none of the three optional rules. Both screens ask
the same eleven questions through one shared module (`settings-form.ts`), and a test asserts
the markup carries every field on both — the drift is what motivated the shared block.
**The dead PvP checkbox is gone from both screens.** `buildDeck` ANDs `pvpCardsAllowed` with a
hard-coded `cardsImplemented = false`, so the control could not do anything whatever it was
set to. The 22 opponent-directed cards (and the 7 defences held out with them) are now a
property of the game type, and the fact is stated in words where the checkbox was.
**The lobby itself:** joining is a door of its own rather than a heading below fifteen fields
the joiner has no use for; a stored join secret collapses to one line and re-opens on a 403; a
display name is remembered and may not duplicate another at the same table (`NAME_TAKEN`); the
seating list numbers bots as the game will; the code copies as a code AND as an invite link;
server codes are translated into sentences in a red block instead of `.dim` grey; the lobby
stream has an `onerror` that tells a blip from a dead lobby (and finds a game that started
while the connection was down); and **a player may read the whole rule set before taking a
seat** (`/api/lobby/preview`, which never carries the seed).
**The start of a game**, which nobody had ever drawn: a handoff curtain with a deliberate beat
instead of a "connecting" line written into the DISCONNECT banner, an announcement naming the
game and its type, a marker at the top of the log so the bots' opening turns are visibly after
the start, the game code and the type in the header for the rest of the game, and a Start
button that cannot be pressed twice.
**The four transient signals reach a remote client at last.** `createRemoteSession` answered
all four with empty values, so multiplayer had no sound, no timetable flash, no announcement
when a completed run paid the table, and no badge on the card you had just drawn. `justDrawn`
is the redaction-sensitive one — `game.justDrawn` is one field for the whole game and does not
say whose card it is — so the server remembers who drew and sends it to that seat alone.
**Two things found by RUNNING it rather than reading it.** A bot seat was being reported as a
disconnected player, which would have put "waiting on Bot 1 — not here yet" on every screen
for a whole game. And the six-card opening survives a table with bots in it: a three-seat game
(two humans, one bot) played through Day 1 without stalling on the discard round.
- [ ] **Let the game join a call and talk to the table.** Long-term. If the game could join a Zoom,
Teams or Jitsi call and post into its chat, it could carry the whole table's shared state
without anyone alt-tabbing: the history of actions as they happen, and a prompt when someone
@@ -1036,6 +1121,13 @@ Deferred while planning the server; decisions and reasoning are in `docs/archite
What is on the screen and where. Split out of Other 2026-08-22; the rules are elsewhere.
- [ ] **The log's start marker only works while the whole log fits.** Added 2026-08-23: a multiplayer
game marks the top of the history with "— the game began —", which is honest only while the
panel is showing every line there is. The panel caps at `slice(-60)`, so past sixty lines the
marker is suppressed rather than lying about where the top is — and "what happened while I was
waiting" (item 13) still has no marker at all. Both want the same mechanism, and item 23's
newest-at-the-top question decides what that mechanism draws.
- [ ] **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
@@ -1127,6 +1219,102 @@ What is on the screen and where. Split out of Other 2026-08-22; the rules are el
will want it off. Whatever this becomes probably needs a speed control, or to scale with whether
anything actually happened in the phase.
- [ ] **INVESTIGATE: the game's settings belong in a card, not along the top line.** Raised by Jesse
2026-08-23, playing the v0.7.0 build: "the game-specific information in the very top line should
probably be a card like Facilities, timetable or blocked. Off on the side, we can give complete
information about all the game options and not take up valuable real estate at the top of the
screen."
**What the top line carries today**, in order: Revenue, the objective (`#objective`), the seed
or seat (`#seed` — the seed in solitaire, `Seat 2` in a multiplayer game), the game code
(`#gamecode`, added 2026-08-23), the game type (`#gametype`, e.g. "Custom — scored as
Competitive"), and an abbreviation of the house rules (`#houserules`, "3 cards · 1/1/0"). The
last four were each added because the information was missing entirely, and the header is now
carrying them because it was the only place they had ever been put.
**What a card could say that the header cannot.** Jesse's list, plus what the Frame already
carries: seed and seat, the game code, the game type and what it is scored as, the opening hand,
all three revenue rates, the Day count, the combined-Revenue floor, BOTH collision limits (with
the running counts, which the Frame has as `collisionsToday`/`collisionsTotal`), where an Extra
may start, and every optional rule that is on. **Nothing new has to be sent** — `Frame` gained
`mode` and `optionalRules` in v0.7.0, and everything else on that list was already on it. The
read-only renderer already exists too: `rulesListHtml` (`settings-form.ts`) draws exactly this
list for the join preview and the seating screen, so the card is largely a matter of calling it.
**His own framing of the value**, worth keeping because it names when it is read: "To go, 'Oh
wait, what did we set that to?' They should be able to look that up, but it does not need to be
at the top every moment because it is not something that they're likely to need all the time."
**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.
- [ ] **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."
**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
<name>", then the Fedora chip (`.tc-super`), and last the `<ol class="tc-phases">` of five phase
pills. So the Fedora sits in the middle of the row, immediately before the pills.
**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 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."
**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:
```
own division view : CHANGED
other's view : CHANGED
drawn SVG : CHANGED
removed: <text class="bs-name" ...>straight</text>
added : <text class="bs-name" ...>turnout</text>
```
**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.
**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.
**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."
@@ -1143,7 +1331,11 @@ What is on the screen and where. Split out of Other 2026-08-22; the rules are el
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.
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
@@ -1167,7 +1359,11 @@ What is on the screen and where. Split out of Other 2026-08-22; the rules are el
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.
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
@@ -1698,6 +1894,18 @@ Doesn't fit the above.
Shed and Power Plant may be similarly stale — flagged as a new item above rather than assumed.
- [x] **Poling — closed, v0.5.0.** Confirmed already at 0 copies, the same treatment as Sharp Curves,
pinned by `mainline-cards.test.ts`. No code change; removed from Rules Questions.
- [x] **Heavy Grade orientation — asked again 2026-08-23, and the answer did not change.** Raised as
"did we ever fix Heavy Grade to allow user placement of direction?", with the option of giving
the choice to the **Superintendent** considered and rejected. Jesse's call: v0.5.0 stands.
Three things came out of the re-examination and are recorded in `implications.md` §10 Q11 so it
is not asked a third time — the advantage is permanent while the office rotates, so a rotating
chooser moves the fairness problem rather than solving it; there is no setup phase to ask in
(`createGame` is pure, and `pendingDecision` is typed for clearance alone across 19 readers);
and since v0.6.2 deals the Mainline deck without replacement, only **20%** of solitaire games
contain a Heavy Grade at all. **The docs were the real defect** — `README.md` listed it among
three open rules questions, all three of which v0.5.0 had closed, and the Mainline deck
reference said the implementation "needs a player-selection step". Both corrected; no code
change, and none wanted.
- [x] **Heavy Grade orientation stays rolled, permanently — v0.5.0, Jesse's call.** The card prints
"Player sets orientation", but a Heavy Grade sits on the shared Division chain between two
players (or beyond an end Division Point, next to one) — never inside one player's own district