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:
co-authored by
Claude Opus 5
parent
42adfda390
commit
06db36e5b5
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user