v0.6.1 — five of six playtest bugs: one button per train, and a load that has to go somewhere
Gameplay testing on 0.4.9d returned six reports. Five are fixed; the sixth could not be reproduced and is written up in TODO.md with the two questions that would pin it down. TWO TRAINS AT ONE PLATFORM ANSWERED TO ONE BUTTON. `porter.board` and `porter.detrain` carried no tray, so there was one button per platform however many trains stood at it and the reducer filled the first empty coach on the A/D tracks. `check` and the reducer were not even asking the same question: `check` skipped a train whose card refuses passenger work and the reducer did not. Both intents now carry an optional `trayId`, one function resolves the train and the coach for check/execute/reduce alike, `legal.ts` offers one candidate per train, and the label names it. A LOAD COULD BE MADE AND BROKEN WITHOUT GOING ANYWHERE. A Freight House could unload the boxcar it had just loaded; a platform could detrain the passengers it had just boarded. Full Revenue at both ends for a movement that never happened. Jesse's rule: a load made anywhere in an Office Area may not be broken anywhere in that Office Area, ever — it has to be carried to another district. The load carries the seat that made it (`RollingStock.origin`), stripped by `pooled` at every yard push. Measured at -0.60 +/- 0.10 Revenue a game (t = -6.1) over 400 paired deals: 78 worse, 3 better, 319 unchanged — free Revenue coming off the board, not a nerf. THE GROCER'S WAREHOUSE SHIPPED AND THE REFINERY RECEIVED. Both were `flow: 'both'` on the reading that "Freight House" was a collective term for exactly those two, and therefore what §9.3 described. The engine has dealt a Freight House CARD since before v0.4.9, so §9.3 names it and the argument goes. The card set agrees: all three Refinery modifiers grant +1 outbound. Refinery outbound-only, Grocer's inbound-only, Freight House the one two-way industry — which leaves exactly the one same-district pairing the rule above refuses. NOT REPRODUCED: cars left behind when backing up over them. Five layouts tried, including cars spotted at an industry; every one couples the lot. Three are pinned in `apply.test.ts`. One way to create such cars was closed anyway — `flyingSwitch` wrote its cut past `carsOn`. Both published replays that had gone dead were re-recorded; a rules change retires a save, and `harness.test.ts` is what catches it. The same change ships as v0.4.9e on the 0.4.9 line, branched from the v0.4.9d commit — the engine files these fixes touch are identical across the two lines, so the patch applied cleanly both ways. Also carries the two "Queued 2026-08-22, from playing on StartOS" TODO items that were staged before this work started (Games in Progress readability, and getting back into a game after losing a browser). They are notes, and items 9-12 below them are numbered against them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011nbvwWMef8CuEP6t5cgkTv
This commit is contained in:
co-authored by
Claude Opus 5
parent
40f07b0710
commit
83a5450866
@@ -30,7 +30,24 @@ Queued 2026-08-21, from playing the StartOS build:
|
||||
6. ~~**Stop every release destroying every game in progress**~~ — done in v0.6.0, by replaying the
|
||||
save rather than comparing version strings.
|
||||
|
||||
Nothing else queued at the moment.
|
||||
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.
|
||||
|
||||
Queued 2026-08-22, from a v0.4.9d gameplay-testing report (six bugs, forwarded by Jesse):
|
||||
|
||||
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. **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.
|
||||
|
||||
---
|
||||
|
||||
@@ -374,6 +391,79 @@ number until the rules stop moving.
|
||||
|
||||
Deferred while planning the server; decisions and reasoning are in `docs/architecture/multiplayer.md`.
|
||||
|
||||
- [ ] **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?"
|
||||
|
||||
**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.
|
||||
The token lives only in that one browser, and nothing else will accept an identity claim.
|
||||
`/api/lobby/join` resolves a code against `lobbies`, and a started game is removed from
|
||||
`lobbies` at `Lobby.Start`, so typing the game code answers `no open lobby with that code` —
|
||||
the same answer a typo gets.
|
||||
|
||||
**The server knows exactly who you are and cannot be told.** Each game's `sessions.json`
|
||||
holds `{ token, gameId, player, displayName }` and survives restarts — read off the box:
|
||||
`HOPPER-4607: player 0 = Jesse | token df9e04c7…`. Everything needed is on disk; there is no
|
||||
door. `lobby-and-sessions.md` §1 says "presenting the token IS the rejoin", which was a fair
|
||||
assumption when a game lasted an afternoon and is a much worse one now that a game survives
|
||||
an update (v0.6.0) and can sit for weeks.
|
||||
|
||||
**A second, nearer limit: `REMOTE_KEY` is a single `localStorage` key**, so a browser
|
||||
remembers exactly one multiplayer game. Join a second and the first token is overwritten and
|
||||
gone, with the same lockout. D13 says one game at a time is expected but "deliberately not
|
||||
enforced" — the client enforces it by forgetting.
|
||||
|
||||
Three ways out, and the third is the one that fits what is already built:
|
||||
|
||||
1. **Show the player their own rejoin link** — a URL carrying the token in the fragment, to
|
||||
copy and keep. No new server state, and the fragment never reaches the server. It is still
|
||||
a credential in a link, so it lands in history and in whatever they paste it into.
|
||||
2. **Rejoin by game code + display name + join secret — do not do this.** Every player holds
|
||||
the join secret, so any of them could claim another's seat by typing their name.
|
||||
3. **An administrator action, "Get Rejoin Link"** — pick a game and a player, get a URL to
|
||||
send them. Gated by the admin secret, so only whoever runs the box can issue one, and no
|
||||
player can impersonate another. Fits the existing admin-action pattern exactly.
|
||||
|
||||
**(1) and (3) together**, most likely: the player keeps their own link, and the administrator
|
||||
can reissue one when they did not. Keying remembered sessions by `gameId` — with a picker
|
||||
when the browser holds more than one — fixes the single-key limit at the same time. Jesse has
|
||||
not yet decided whether a token in a URL is acceptable; the alternative is a bare token
|
||||
pasted into a field, which is uglier and stays out of history.
|
||||
|
||||
- [ ] **The StartOS "Games in Progress" action is one long unreadable run-on per game.** Raised by
|
||||
Jesse 2026-08-22 after using it against four games. Lives in the WRAPPER repo
|
||||
(`station-master-startos`, `startos/actions/gamesInProgress.ts`), whose `AGENTS.md` says work
|
||||
belongs in issues on that repo rather than a `TODO.md` — recorded here because this is where
|
||||
the project's list actually is; move it if that policy is meant to bind.
|
||||
|
||||
**What he asked for**, taking the current output field by field: a separator between the
|
||||
players and the Day/Stage line; the phase in parentheses rather than after an em dash
|
||||
(`Day 1, Stage 1 (Local Ops)`); a separator before "Waiting on"; one after the waiting-on
|
||||
player and seat, before the start time; and one between the start time and the last-move
|
||||
time.
|
||||
|
||||
**Why they are all missing at once, most likely.** `describe()` joins its lines with `\n`,
|
||||
so the intent was one field per line. Every separator Jesse is missing is exactly where a
|
||||
newline is — which says the StartOS action-result view does not render newlines in a
|
||||
`single`'s value, and collapses the lot into one line. Worth confirming in the UI before
|
||||
designing around it, since the whole diagnosis rests on it.
|
||||
|
||||
**The structural fix, better than adding separators.** `ActionResultMember` can itself be a
|
||||
`group` (`osBindings/ActionResultMember.d.ts` — "a new group of nested values, experienced by
|
||||
the user as an accordion dropdown"), so groups nest. Each game can be a collapsible group
|
||||
whose members are individual `single` rows — Players, Position, Waiting on, Started, Last
|
||||
move — instead of one string. That gives every field its own labelled row, makes the
|
||||
separator question disappear rather than answering it, and collapses cleanly when there are
|
||||
many games. Do this rather than punctuating the run-on.
|
||||
|
||||
**Sorting, also asked for**, and worth having once a box holds more than a handful: by game
|
||||
name, by start time, or by last-move time, ascending or descending. An action's input spec is
|
||||
built at open time, so a `Value.select` for the field and another for the direction costs
|
||||
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.
|
||||
|
||||
- [ ] **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
|
||||
@@ -772,6 +862,45 @@ Deferred while planning the server; decisions and reasoning are in `docs/archite
|
||||
|
||||
---
|
||||
|
||||
## Rules Questions
|
||||
|
||||
- [ ] **NOT REPRODUCED: "when I back up to collect standing cars and, further down the tracks, the
|
||||
caboose, I get the caboose but the cars remain. I can later drive right through them."**
|
||||
Reported against v0.4.9d by a playtester (not Jesse, who forwarded it and could not add detail;
|
||||
his guess was that the cars were spotted at an industry).
|
||||
|
||||
**What was tried, all of which works.** Cars on plain track on the way to the caboose; cars
|
||||
SPOTTED AT AN INDUSTRY on the way; the train's own cut standing on the square it is pulling out
|
||||
of; a stale `standingWest` on the intermediate card; the industry locked by MEN AT WORK (which
|
||||
correctly blocks the whole route rather than letting the crew past). Every one couples the lot.
|
||||
The first three are pinned in `apply.test.ts` — "backing up over a cut to something beyond it
|
||||
takes both" — so if the case is found later it is somewhere none of them cover.
|
||||
|
||||
**Why it is hard to make happen.** Coupling is mandatory (§A.4) and `exploreMoves` accumulates
|
||||
what it meets card by card, so a route that reaches the caboose has already met everything
|
||||
between. `carsOn` (`state.ts`) is the SINGLE answer to "what is standing here", and the movement
|
||||
walk, the sweep in `carsCoupled` and every renderer all ask it — so cars a train can drive
|
||||
through would have to be cars that are on screen and not in `carsOn`, and there is no such
|
||||
place. (One route to one was closed anyway: `flyingSwitch`'s reducer wrote the cut straight into
|
||||
`industryTrack`, which for a Passenger Facility is not where `carsOn` looks. `check` refuses a
|
||||
non-freight target, so it never fired.)
|
||||
|
||||
**The two questions that would settle it**, for whoever has the board: was there a SECOND route
|
||||
to the caboose — a parallel row, or a turnout — so the move could have gone round the cars? And
|
||||
what did the move button say it would couple? The label names every car (`describeIntent`), so a
|
||||
button that read "couples caboose" and one that read "couples 2 boxcars, caboose" are different
|
||||
bugs: the first is route selection, the second is the sweep.
|
||||
|
||||
- [ ] **An unload does not check the facility's commodity.** `laborer.beginUnload` gates on
|
||||
`allows.inbound`, a loaded car, an empty of that type in the Division Yard and room in the red
|
||||
box — but never on `facilityCarTypes(f)`, which `freightAgent.stockOutbound` does check. So a
|
||||
Freight House (boxcars) will unload a hopper. Found reading the code for the v0.4.9e district
|
||||
rule, not from play. Low impact today because the district rule now refuses the only same-Office
|
||||
pairing that made it easy to hit, and because the bot spots matching cars — but it is a rule the
|
||||
engine states in one direction and not the other.
|
||||
|
||||
---
|
||||
|
||||
## Other
|
||||
|
||||
Doesn't fit the above.
|
||||
@@ -828,6 +957,13 @@ Doesn't fit the above.
|
||||
made for the other three, not an assumption that the same correction applies, since raising or
|
||||
lowering Laborer counts is also a balance question, not only a docs one.
|
||||
|
||||
**v0.4.9e narrows it.** The Direction column for the Grocer's Warehouse and the Oil Refinery was
|
||||
the wrong half of that v0.5.0 pass and has been put back to one-way each, from gameplay testing
|
||||
and Jesse's confirmation. The *numbers* in those two rows are still the card reference's own
|
||||
(1 Laborer, 3–4 track) and still unverified against the engine, so this entry stands as written
|
||||
for all five industries — what changed is only that the two rows the v0.5.0 pass claimed to have
|
||||
re-verified turn out to have been re-verified against a premise rather than against a card.
|
||||
|
||||
---
|
||||
|
||||
## Done, kept for the reasoning
|
||||
@@ -1019,7 +1155,14 @@ Doesn't fit the above.
|
||||
tooltip briefly told players four working cards did nothing, which is worse than the bare label
|
||||
it replaced — `enhancements.test.ts` had passing tests for all four the whole time.
|
||||
- [x] **A modifier's grant can land on a direction its host cannot use, and nothing says so —
|
||||
corrected in v0.4.7.** The earlier "no bug" verdict below was wrong. The reasoning had been that
|
||||
corrected in v0.4.7, and the correction was itself half wrong.** *(v0.4.9e: the ORIGINAL verdict
|
||||
was right about the Grocer's.* A Grocer's Warehouse **is** inbound-only — gameplay testing said
|
||||
so and Jesse confirmed it — so the Ice House's outbound grant beside one is genuinely dead, the
|
||||
way the Truck Dock's inbound grant beside the outbound-only Packing Sheds is. What survives from
|
||||
v0.4.7 is the machinery and the decision behind it: an industry's printed flow is absolute, the
|
||||
grant is dropped rather than the direction opened, and `suppressedGrants` says so on the page.
|
||||
What does not survive is opening the two facilities up. Original v0.4.7 note follows.)*
|
||||
The earlier "no bug" verdict below was wrong. The reasoning had been that
|
||||
a Grocer's Warehouse is inbound-only. It is not — the card reference says "Both" — so the grant
|
||||
was being dropped on a direction the facility should have had. Reported again in play as
|
||||
"grocer's warehouse didn't get extra outbound slot for truck dock". The suppression machinery
|
||||
|
||||
Reference in New Issue
Block a user