4adf149ba5595ef53e2b8217af026ee34d12c9ac
101
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4adf149ba5 |
v0.8.0.10 — playtest fixes: clearance rulings, the log, the map, and a save file
From the first two multiplayer playtests of v0.8.0.9, each traced before fixing. The engine: - A train on a card BEHIND the one departing no longer triggers a clearance ruling or an opposite-direction bar (#26). Reproduced from the exported save: X15 was held over X18 behind it, and X18 then collided into the full Whistle Post. Games in progress holding a ruling the engine no longer asks for will not resume (28 of 40 recorded four-seat games); shipped as is at Jesse's call. - `mainlineModified` carries the card's previous kind, so the log can say what a Realignment converted (#27). The screen: - The turn chart and the Division map name the player whose move is on screen while bot turns replay, not the live actor (#25). - The owning player's name is no longer outlined by the turn arrow's stroke, which made it unreadable (#24). - A Mainline card flashes on the map when a Realignment changes it (#28). - The history is held back with the board and revealed step by step, instead of arriving whole while the board is still catching up (#29). - A ruling made by holding the office reads "Superintendent Player X" (#30), and no line names a player twice (#31). - A seated player can download their own game as a save file: the play page's Save replay button, fed by GET /api/save?token=… (#32). The StartOS action cannot do this — an action result is text only. Closes #24 Closes #25 Closes #26 Closes #27 Closes #28 Closes #29 Closes #30 Closes #31 Closes #32 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nnuCv8UodHucFfx3LWEoXv0.8.0.10 |
||
|
|
76c6e103b3 |
v0.8.0.9 — the bot plans its switching turn, stops wasting its draws, and the engine walks each route once
The developer bot, re-measured decision by decision against the bot before it, goes from about -0.3 revenue a game to about 4.8: - plans the whole switching turn before its first Move (sim/switch-planner.ts), +2.89 over 1600 paired seeds; closes TODO #53 - takes a face-up card only if it could play it, +1.52 over 1600 seeds - stops running Second Sections by accident in the New Train phase, +0.32 - lays track by what the district can do afterwards, +0.12 over 6400 seeds, run-arounds in 22 of 60 districts against 9 The engine is 2.8x faster with play proven identical: a route cache scoped to one unchanged position, applyIntent split into prepareIntent + commitEvents, and less allocation in exploreMoves. npm test now leaves out the bot simulations, which run as npm run test:sim. No rule changed; games in progress resume. Rejected candidates and the Second Section card question are in CHANGELOG.md and TODO.md (#104-#106). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nnuCv8UodHucFfx3LWEoXv0.8.0.9 |
||
|
|
072029b1f7 |
v0.8.0.8 — a played train does not come back; a discarded one does
Jesse's ruling on the question v0.8.0.7 filed: once a regularly scheduled train has been played its number is on the timetable, so putting it back into a reshuffled deck to be played again makes no sense. The same card sitting in a discard pile was never played and its slot is still open, so it should come back. An Extra is a single run rather than a standing slot, so a played one is free to run again. The test is therefore WHERE the card is, not only what it is — which is worth writing down, because it is exactly the rule a later tidy-up would simplify into filtering by kind everywhere. And the duplicate that started it: trainScheduled was pushing a synthetic train-<number> into the Salvage Yard beside the real card cardPlayed had already put there — four scheduled trains left eight entries in a pile holding four cards. Nothing read it, it inflated the depth, it displayed as "a card", and it would have been swept into the draw deck to be drawn as an id with nothing behind it. Removed, which retires the phantom-id class rather than papering over it, so v0.8.0.7's cardName resolver for it goes too. Games in progress resume: no predicate changed its answer, and a draw is a draw whatever is on top. What differs is the Yard's depth, which was double-counting, and what a reshuffle recovers — and reshuffles are effectively unreachable, with zero seen across eight games driven to 4000 moves. Closes #23. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6cF1iYvJ1kNmzYBzu4QX6v0.8.0.8 |
||
|
|
d0e5091824 |
v0.8.0.7 — the Salvage Yard had nothing to say, and phases too little time to read
The Salvage Yard was face up all along; its tile just read "a card". apply.ts pushes a synthetic train-<n> id on trainScheduled, nothing in s.cards matches it, and cardName fell through to its default — and since a train is scheduled several times a Day that id is on top most of the time. Measured before touching anything: the tile read "a card" from the opening frame through 60 pushes while its depth climbed from 2 to 8. cardName resolves it now, in sim/view.ts, because this is a name. The engine half is filed as Gitea#23 rather than fixed here. reshuffleIfDepleted sweeps the Salvage Yard back into the draw deck, so that synthetic id can be shuffled in and drawn into a hand as an id with no card behind it. Eight games driven to 4000 moves across eight seeds produced zero reshuffles, so it is latent; there are two defensible fixes and the choice turns on what the synthetic id is for, which is not a call to make while fixing a label. And phases scale with the speed control again, damped to a third of the rate. They were pinned in v0.8.0.3 because scaling them walled off a player's own turn; pinned turns out to be too short to read at 10x. Damped satisfies both: 1x unchanged, 10x lands exactly on the four-times guess. Bounded because phase beats cluster rather than accumulate — 1.0 per push on average, 4 at worst, so the wait after a move is ~2.4s typical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6cF1iYvJ1kNmzYBzu4QX6v0.8.0.7 |
||
|
|
64e8ce584f |
v0.8.0.6 — your move waits its turn, the lit pile keeps asking to be looked at
Your actions are put away while the board is catching up. The board on screen is behind the game, so a move offered there is a move against a position that has already moved on — and the screen had grown to four things competing at once: the district, the history, the catching-up row, and a lit pile. Skip is one click away, so the wait stays voluntary. That could have locked a player out of their own game. Hiding actions behind busy() makes that flag the thing standing between a player and their turn, and without requestAnimationFrame nothing ever advances the queue — so busy() would never clear. Caught by the DOM-stub test that has been proving this page still starts since long before any of this existed. No rAF now means draw everything at once, which is what pace 0 does deliberately, and a queue that throws empties itself rather than stranding anyone. The lit pile was never brief: measured, it stays lit for 6997ms at 10x. It was a single flash over a dark fill, easy to miss while watching the district — a state that settles stops asking to be looked at. It pulses now for as long as the move is up. And the pace ceiling was not theoretical. 10x was the top of the ladder and was reported still a bit fast; it runs to 20 now. A control whose limit is reached in ordinary use has the wrong limit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6cF1iYvJ1kNmzYBzu4QX6v0.8.0.6 |
||
|
|
3fca325699 |
v0.8.0.5 — the Home Office deck, and lighting the pile a move touched
"Many operations still occurred too fast for me to see", at 10x — where an action already holds the screen for seven seconds. So it was never duration: a bot drawing a card changes one number in a panel nobody is watching, and the board sits unchanged. Raising the dwell was the wrong lever and it had been pulled three times. f.deck has carried the face-down count since the Frame existed and nothing drew it — the display gap test/display-gaps.test.ts sweeps for, surviving in the one panel that draws every other pile. It is a tile now, first in the row, face down, because that is the order a card travels and not knowing what is on top is the point. And the piles a move touched are lit for as long as that move is on screen. Derived from the frames either side of a step rather than sent, so nothing joins the protocol and the 0.8.1 board gets it free. What lights follows what is public, and was measured across four seeds rather than reasoned about: a Home Office draw lights the deck and never names the card; a Department draw lights that pile, and the deck too when it refills; a discard lights the Department it lands on; a played card lights the Salvage Yard. Switching and new trains light nothing here — they move the board, which the district panel already follows. A state rather than a flash: the timetable's fixed 1.5s animation would be over long before a seven-second pause. Not for your own moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6cF1iYvJ1kNmzYBzu4QX6v0.8.0.5 |
||
|
|
fc40fc39ed |
v0.8.0.4 — take the test server's name back out of the tracked files
Both repositories allow anonymous clone — checked rather than assumed: info/refs for git-upload-pack answers 200 for each, git-receive-pack answers 401. So everything committed here is public, and tracked files are supposed to carry placeholders rather than real hosts. Ten mentions added while building v0.8.0 are now "the test server" or "the target hardware", across CHANGELOG.md, the common-board plan's three deferral banners, sim/pacing.ts, and two test files. Prose and comments only, no behaviour; the quotes are untouched, because what was said about bot pacing is the part worth keeping. Left alone deliberately: nineteen older mentions in entries about v0.7.5, v0.7.6 and v0.7.8 and in TODO.md, since rewriting a changelog after the fact makes the record less true; and scripts/deploy-web.ts, where the host is the functional default for FB_URL rather than prose — turning that into a required variable changes how deploying works and wants deciding on its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6cF1iYvJ1kNmzYBzu4QX6v0.8.0.4 |
||
|
|
ff629c0708 |
v0.8.0.3 — Skip on the left, a caption that says who, and a clock that stops
stretching Three things from playing v0.8.0.2, all about the row rather than the mechanism. Skip was on the far right and a player's eye is on the countdown. Moved to the left, in front of the count. The caption said what but never who. Measured over 40 turns of a real 3-seat game, half the waiting is automatic phases — 21.0s of phases against 21.7s of other players — and a phase narrates as "Mainline", which is accurate and no answer at all to "who am I waiting on". A phase introduces itself now: "The Division: Mainline phase". A player's move already carries its name from record(), so it is left alone. The row was also hiding one step early, because it showed only while behind > 0 — which goes false exactly when the last step of a burst goes up, so the step most likely to be read lost its caption. And the speed control was stretching the clock along with the players. It was not his own move being replayed — own moves have cost nothing since v0.8.0.1 — it was the phases behind it, which put 105 seconds of clock-ticking into a 5x game. pace now scales a player's move and leaves a phase at its tabled beat, which is what the control has always claimed to do. Off still means off for both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6cF1iYvJ1kNmzYBzu4QX6v0.8.0.3 |
||
|
|
c10f52791e |
v0.8.0.2 — the speed control that was only ever a URL parameter, and a Day-end
contradiction Two things found by playing v0.8.0.1, neither in the mechanism itself. ?pace= never worked. index.html's doors are play.html?lobby and play.html?solitaire, so arriving through the splash replaces the query string and the play page only ever saw ?lobby — a whole game was played at 1x while believing it was at 7x. v0.8.0 shipped that parameter as the only way to change speed and the game's own front door destroyed it. There is a control on the play screen now, beside zoom, persisted per viewer; the doors carry pace through as well, so the URL lever is honest for handing two playtesters different speeds. PACE_LEVELS moved to sim/pacing.ts with DWELL and MAX_PACE — the tuning surface in one file, and testable. The committed default is unchanged: what it should be is a question for a game played at a speed that took effect. And the Day-end dialog said "0 today, 2 in all". advance.ts increments the Day and then zeroes collisionsToday, and noteDayEnd() fires when the Day goes up — so the dialog reporting the Day that just finished was drawn from the very frame in which that Day's count was reset. Reproduced on four of five seeds before changing anything. The count is captured at the rollover now; it is not derivable on the client, because in multiplayer the push announcing the new Day is the same push that carries the reset. And "today" was the wrong word regardless: it names the Day instead — "Collisions: 2 on Day 1, 2 in all". Unrelated to v0.8.0 — that one has been wrong since the dialog was built for Gitea#10, and needed somebody to play a Day with a collision in it and then read the summary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6cF1iYvJ1kNmzYBzu4QX6v0.8.0.2 |
||
|
|
0cfeb4c496 |
v0.8.0.1 — bot play was way too fast, and the last step never got its moment
Two things from the first real play on phoenix.local. One bug: busy() was pending.length > 0, so the final step of a burst reported the queue idle the instant it was shown — the district panel snapped back to the viewer's own board and the countdown row vanished before either could be read. And calibration. "Start at 1s and tune down" was applied to switching, while a 250ms action tier was invented beside it — fine for a switching burst, wrong for the common case, since switching is not legal until there is track down. A real early-game bot turn measured 750ms end to end. Actions are 700ms now, and localOps.choose moved out of bookkeeping: it is the line announcing what a bot is about to do, and at zero dwell nobody ever saw it. The viewer's own moves now cost nothing — their board comes from their own Frame, so holding their click only delayed the thing they wanted to watch. And pace supports 2 and 3 as asked, bounded by MAX_PACE so a typo cannot look like a frozen board; every tier scales together, so the weighting survives any speed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6cF1iYvJ1kNmzYBzu4QX6v0.8.0.1 |
||
|
|
02289e94b8 |
v0.8.0 — the board replays what everyone else did, instead of arriving rearranged
TODO #13, #15 and #18 — Gitea#20 steps 2-4 pointed at a seated player's own screen. Every accepted intent, and every automatic phase that does anything, becomes an ordered presentation step. A bot's whole switching turn used to land in one push; now it arrives as a run of steps, the district panel follows whoever is acting, and a [N behind] … [Skip] row says how far the board is from the game. Solitaire runs the same path — one collector inside submit(), which both session kinds already funnel through — which is where its automatic phases finally get a visible beat. Dwell is assigned by kind: switching holds the screen, turn bookkeeping costs nothing, and the clock turning over earns the beat. Tunable per viewer without a rebuild, and off entirely at pace 0. Also: switching was the one class of action logging unattributed, and now names its train. Reasoning, measurements and the three things that turned out wrong are in CHANGELOG.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6cF1iYvJ1kNmzYBzu4QX6v0.8.0 |
||
|
|
312e0301e0 |
v0.7.9.8 — the test command did not typecheck, and the plan had gone stale
Housekeeping before v0.8.0: the answer to "anything else that should be looked at first". One real hole, one stale document, and my own leavings. #102 — `npm test` passed green on a type error. `pretest` ran `scripts/build-web.ts`, which invokes `tsc --ignoreConfig` against three web entry points, so it saw only what those three transitively import and under a WEAKER configuration than tsconfig.json — no `noUncheckedIndexedAccess`, no `exactOptionalPropertyTypes`, `--types ''`. It never saw `src/server/` or a single file under `test/`. Demonstrated rather than argued: a planted `const DELIBERATE_TYPE_ERROR: number = 'not a number';` in src/server/session.ts gives `npm run typecheck` a TS2322 and `npm test` a clean `# fail 0`. `pretest` is `tsc --noEmit && node scripts/build-web.ts` now, and the same error exits 1 with the tests never running. This mattered THIS week rather than generally: v0.8.0 is steps 2-7 of the common board — display stream, credentials, persistence, Chromium supervisor — which is almost entirely src/server/, exactly the half the test command could not see. #103 — the plan had drifted from the code it is the source for. docs/plans/jitsi-common-board.md was written 2026-08-27, still said "No implementation has been performed", and is what steps 2-7 get built from. Step 1 shipped across four releases since, so every "current code finding" under it described a fault that is now fixed — a document reading as present tense and nine days stale sends the next reader to fix things twice. Measured: its PublicFrame sketch lists four properties never built (protocolVersion, config, scoring, deckCounts) and omits 28 that exist, and the shape is the real difference — the implementation is FLAT where the plan grouped things into objects, so a renderer written from the sketch would not compile. The plan now says so at the top and at step 1, names src/sim/view.ts and the redaction allow-list as the authority, keeps the original sketch for its reasoning, and calls out `protocolVersion` as unbuilt rather than dropping it quietly — step 2 is the reconnecting display stream and is the first thing that would want one. One step-1 item is STRUCK OFF rather than built: "add the Red Flag holder to the public player projection". The premise does not hold here. `decks.redFlags` is written once, in setup.ts, from `optionalRules.emergencyToolbox`, and never again — `redFlag.play` emits `phaseEnded` and does not spend it — so every player holds one or none does, decided before the deal. A per-player `redFlagHeld` would be one already-public option copied N times, while telling every reader of the common board that it varies by player and might change mid-game. Worse than the absence. Pinned by test so it is not re-raised from the plan. Four dead imports removed, all mine: `HAND_LIMIT` left unused in apply.ts, view.ts and web/game.ts when 0.7.9.6 consolidated the three copies of the §6.2 test, and `actingPlayer` in web/game.ts, dead since 0.7.9.5 made `currentActor` delegate. Finding them re-measured #46: `tsc --noUnusedLocals` now reports 40, up from 29 on 2026-08-30. That entry's "without the flag this list simply regrows" is a measurement rather than a forecast now. The other 36 and the flag stay open. 946 tests pass, up from 943. No behaviour changes: three new tests pin an invariant, and the rest is a build command, dead imports and a document. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y5boPxP6JHRYMm8adXaF5Rv0.7.9.8 |
||
|
|
88a42ae9e2 |
v0.7.9.7 — a device that said it was available all day after it was gone
The last item off 0.7.9.6's sweep, parked there as the one genuine maybe. It had a second half worth more than the first. #101 — Telegraph (+4), Telephone (+8) and Radio (+12) are "once a day, when dispatching facing trains, add +N to the other train's number". `enhancementText(key)` takes only the KEY, so the tooltip could not vary with anything: a spent Radio read "Once a day, add +12..." for the rest of the Day, advertising a bonus that was not there. That is `trainRules` before #100, in another corner of the same view. THE HALF THAT ACTUALLY SURPRISES. `spendDispatchBonus` reads `areaOf(s, s.clock.superintendent)` — the SUPERINTENDENT's own devices, not the train owner's — and the Fedora moves every STAGES_PER_SHIFT (3) Stages, four times a Day. So a player's Radio does nothing at all for three-quarters of the Day, and is spent automatically, without its owner being asked, during the quarter it is theirs. Neither half was anywhere on the board. The card now reads as one of three states — available and dispatching, unspent but idle while somebody else holds the Fedora, or spent until the next Day — and names the shift length, because "not now" without "for how long" is half an answer. A spent device is struck through on the board. Shown on EVERY district, not only the viewer's (Jesse's call): it is public, and a rival's spent Radio is what you want to know before forcing a meet. What counts as a device is `enhancementRule(key)?.dispatchBonus` rather than three keys written out in the view — the ladder lives in ENHANCEMENT_RULES and a fourth rung would otherwise be silently exempt. A STALE COMMENT CORRECTED, AND PINNED. `advance.ts` warned that indexing a SEAT-keyed area with the PLAYER holding the Fedora "is right only while seating is the identity map". It read as a live Employee Rotation bug and was not one: `areaOf(s, p)` IS `areaAtSeat(s, seatOf(s, p))`. A comment that sends the next reader chasing a bug that does not exist costs about what the bug would. Rewritten, and the claim is now a test — seating set to a real permutation, and the Superintendent's own district rather than the seat with the same index is the one that reads as dispatching. TWO THINGS MUTATION CAUGHT THAT PASSING DID NOT. A test asserted the ABSENCE of /spent|Fedora/ with the Fedora held, and a mutant with the `dispatchBonus` guard deleted PASSED it — the leaked text in that case says "Available today, and this district is dispatching", which contains neither word. A test that something was left alone has to compare it against what it should be, so it asserts equality with `enhancementText` now, in both Fedora states. And the replay wire format needed the field: cells pack positionally, so the flag is index 9 and reads `?? []`, the same tolerance `standingWest` uses — older recordings report no device spent, which is what they drew at the time, so every published replay is unchanged. 943 tests pass, up from 934. NOT VERIFIED AT A TABLE, like 0.7.9.6. #39 and #35 still stand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y5boPxP6JHRYMm8adXaF5R |
||
|
|
7f4e027258 |
v0.7.9.6 — three things the engine knew and the screen did not
Found by looking rather than by being told. Gitea#21, #22, #94 and #96 were four instances of one fault in a row — the engine gains something that changes what a train may do, and nothing draws it — and every one was found by a player hitting it. So instead of waiting for the fifth, every field of GameState and its nested types was enumerated, checked for a reader in sim/view.ts, src/web/ and sim/narrate.ts, and the survivors verified BY RUNNING THE ENGINE rather than by trusting the grep. Four fields had no reader. `movedThisPhase` lives and dies inside one `advance` call and is nobody's business. The other three are below. What was ruled out matters as much: `freightWorked`, `drawnThisTurn`, `freightAgentUsed`, `switchedSince` and `movesUsed` are invisible on purpose, their effect already showing as legality or as a complement already on the Frame. A field is not a display gap merely because nothing renders it. #98 — the Crew Tray pool. §7 scarcity is called an explicit mechanic and was explicit only in the engine. The blocked panel had one tray rule, keyed off the train due out this Stage, so a player who spent a card on an Extra or ordered a second section got an EMPTY panel while their train sat behind an exhausted pool — both having been announced once in the log in a line promising a future event that nothing then confirmed. The shared table carries the pool and the queue now, so the common board gets it too, and the panel reports all three with the count beside them. #99 — a train held at the Limits vanished off the board, and this one had shipped. The Interlocking stops an inbound train on the Limit Track rather than colliding with a full Office. `arriveAtOffice` removes the tray from the Mainline node's `transits` and the Interlocking branch pushes it onto `heldAtLimits` without assigning `tray.position` — and the map draws mainline nodes from `transits` and squares from `position.at === 'grid'`, so between the two it was drawn in NEITHER. It disappeared on arrival and reappeared in the Office some Stages later. Fixed in the view: the engine is right, and `position` is left alone deliberately so nothing treats the train as standing somewhere it could be switched from. #100 — the Campaign Train's speeches change its rules, and the card said the same thing before and after. Worse, the "EXPEDITED ... costs 1 Revenue" warning prints only under `rules.expedite`, so X17 became subject to a fault whose warning the game shows to every other expedited train and never to it. `trainRules` reads `speechMade` now and borrows `isExpedited` from advance.ts rather than restating the test. #45 — the 0.7.9 dead-field audit, finished, and the answer was different for each. `overHandLimit` is WIRED: its consumer existed all along and was inferring the hand limit from the ABSENCE of `draw.end` in the menu, which is sound only while `check` keeps refusing for exactly three reasons. `viewerSeat` is DOCUMENTED, with a condition — Gitea#20's board keys districts by seat, and the note says to delete it if step 2 ships without using it. The audit had missed a third limb. `game.mustPlayCard` was assigned on every submit and read by nothing: deleted. Chasing it turned up the thing worth fixing — the §6.2 hand-limit test existed in THREE places, all agreeing, which is the state #96's disagreement started from. One `overHandLimit(state, player)` in state.ts now, and the other two ask it. `Session.overHandLimit()` is deleted rather than kept: the Frame already carries the fact, so the method was a second path to it. 934 tests pass, up from 917. The 17 new ones were written red, and each fix checked by mutation: reverting `speechMade` fails 2, dropping the held-train projection fails 4, forgetting the tray queues fails 2. The empty blocked panel is reported beside its positive control, since an empty result from a broken function proves nothing. NOT VERIFIED AT A TABLE. Engine and view work, checked by tests and by running the engine. #39 and #35 still stand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y5boPxP6JHRYMm8adXaF5R |
||
|
|
d5445badcc |
v0.7.9.5 — two answers to one question, and the copy nobody read
Both faults are in what 0.7.9.4 had just built, and both are the same shape: a second copy of an answer that agreed with the first until it didn't. #96 — the §3.3 vote has no actor, and the screen named one anyway. The vote is PARALLEL: every un-voted seat may vote at any moment, in any order, one refusal ends it, and `apply.ts` says where it accepts one that there is no actor to be. The turn chart named the last seat to move before the timetable ran out — no more claim on the vote than anybody else — directly above a tally correctly showing three seats outstanding. The cause is worth more than the symptom. `currentActor(game)` (`web/game.ts`) guarded on `status !== 'active'`; `currentActorOfState` (`sim/view.ts`), added the same day in #95 and the one the frame calls, did not, so it handed back whatever `clock.currentActor` was left holding. The view now carries the guard and `currentActor` delegates to it. That matters more than the tidiness: `currentActor` is what REFUSES an intent, so a screen answering differently tells the table to wait on a player the server would turn away. The fourth of this class after Gitea#21, #22 and #94 — but the first found by asking a view helper its question in a state the game is not `active` in, which is the generalisation and is cheaper than finding the fifth the same way. #97 — narration reaches a seat once, by one path. `Frame.lines` carried the whole log on every push to every seat, and nothing read it: `RemoteSession` accumulates from `push.lines` alone and its `lines()` returns that accumulator, so the log was serialised into every frame, grew all game, and was discarded on arrival while `linesSince` sent the same text correctly beside it. The duplicate was masking a bug rather than merely wasting bandwidth. `connect()` cleared `lastFrame` but not `sentLines`, so a reconnecting seat was told "nothing new since your last push" while the browser it answered had just reloaded from an EMPTY accumulator — the history panel came back blank, mid-game, with the server holding the whole log. So the two halves are one change, and the plan's instruction taken alone ("stop passing the full game log into `frameFor()`") would have deleted a real behaviour rather than a duplicate. Every remaining reader of `Frame.lines` was checked before the field was emptied: all of them are the solitaire and replay path, which builds Frames through `snapshot()` directly and never goes near a session. One test was wrong before the code was. The first draft of the reconnect test connected inside its own fixture, so both sides of the comparison were the empty array and it passed against the broken server. Each test now asserts its premise is non-empty before comparing. Also: `docs/plans/jitsi-common-board.md` is committed. It was never added — not ignored, just missed — while TODO.md cites it twice as the plan for all of v0.8.0 and the last two releases were built from it, so a clone got a TODO pointing at a file that did not exist. 917 tests pass, up from 909. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y5boPxP6JHRYMm8adXaF5R |
||
|
|
ebd16983e2 |
v0.7.9.4 — Gitea#20 step 1, and a Red Flag you can see
Step 1 of the common board done as its own release rather than as the first hour of 0.8.0, since both halves of it are worth having whether or not anything is ever published to a call. #95 — the public projection helpers. `projectDistrict(state, seat)`, `projectDivision(state)`, `projectSharedTable(state)`, `publicSnapshot(state)` and `currentActorOfState(state)`, with `snapshot()` REBUILT to compose from the same helpers rather than keeping a second copy of the shared table, so a player's frame and a spectator's cannot come to disagree about the clock, the phase, whose turn it is or the score. Behaviour-neutral; the 897 existing tests passing unchanged is the proof. The public view is composed UPWARD, never by calling `snapshot()` once per seat. That shortcut is the trap the plan names: `snapshot` assembles one player's view, so a public view made of player views builds every private field and then has to remember to strip it — and it defaults its viewer to player zero, so a careless spectator call would have served seat 0's hand. Districts are keyed by SEAT with the player resolved through `playerAtSeat`, because Employee Rotation moves players between districts and a board that treated seat and player index as interchangeable would relabel every district the first time anybody rotated. One plan finding is struck off rather than fixed: it warns a display reading `clock.currentActor` could highlight the wrong district during a decision. Measured over six seeds and 3,600 decision points, that field and `actingPlayer` never disagreed. `currentActorOfState` exists anyway, as one place for the next reader to ask. #91 — the redaction net, systematically. v0.7.9.2's two leaks were found by reading a plan, not by a test, which is the whole argument for this: a suite made of the leaks somebody happened to notice proves nothing about the next one. Serialise a seat's Frame, the PublicFrame a spectator gets and the narration they receive, then search all three for every opponent card id, every card name unique to one opponent's hand, the seed and any private decision or menu data — across a fresh game, a blind draw, mid-game, a pending decision, Employee Rotation before and after the seating moves, a reconnect push (a full Frame, and its own opportunity to leak) and a played-out game. And the allow-list, which is the plan's stated acceptance bar rather than the tests: every property of `publicSnapshot` is written down with its reason and compared on every run, so adding a field fails the suite until somebody has said out loud that a spectator may see it. Both v0.7.9.2 leaks were fields nobody had ever asked that question about. Proved by mutation rather than by passing: restoring the seed line fails 6 tests, restoring the blind-draw card name fails 1, adding a private field to the public projection fails 7, and making `players[]` carry hand contents instead of a count fails 5. Two false failures were worth the lesson. A card NAME is a type, not an identity — "right-hand curve" names a dozen cards and one is legitimately a cell label the moment anybody lays track, so searching for it fails on correct code, which is worse than not searching; a name is evidence only when every card bearing it is in the one hand. And a one-digit seed makes the seed check meaningless: seed 7 matched "Train 7". One item on the plan's list has no test because it has no referent — there is no secret objective in this game, `objectiveOf` deriving from `config.minCombinedRevenue` and the player's own Revenue, both public. #94 — a Red Flag standing at an Office's Limits is on the map. It is a token set out ON the board that holds the next train arriving from that side, and it was announced once in the log and drawn nowhere, so a train stops short three Stages later with its only explanation scrolled out of the panel. `DivisionView`'s office node carries `redFlag` and the map draws a staff and pennant AT THE END IT GUARDS — west on the left, east on the right — because which approach it covers is the whole of the information; a flag in the middle of the cell would say one is out and leave the reader to hover for the half that decides whether to run a train. The tooltip leads with it, ahead of everything that merely describes the cell. The third of these in a row after Gitea#21 and #22: when the engine gains something that changes what a train may do, the question to ask is where it is drawn, not whether it works. 909 tests pass, up from 897. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ss2y7FyhxkHjGj7xnUPCgY |
||
|
|
e734481d65 |
v0.7.9.3 — the reference says what only the code knows, and saves get a rule
The generated card reference now carries the half TODO #15a said was the point of generating it: every Enhancement's `live` / `dormantSolo` / `unbuilt` status — whether its printed effect actually resolves yet — and the opponent-directed Action and Space-use cards, none of which is dealt in any deck. A transcription cannot carry either fact. Card counts are removed throughout, as ruled: they move with play balance so a document printing them is stale on the next retune. Where a count matters it is a yes/no "is this dealt at all", which is a fact about the design rather than the current tuning. #88 closes with it — it asked whether `card-reference.md`'s industry rows were stale, deliberately without rewriting them since Laborer counts are a balance decision. They are; nothing in the engine changed; that file is simply no longer where anyone looks. The balance question it guarded is #70. Save compatibility becomes a general rule in `README.md` § Design notes rather than a fact restated per version: a save is a list of moves and reopens by being re-played through the CURRENT rules, so any change that makes a once-legal move illegal stops an older one there — a deck change being the likeliest breaker. It fails safe every time. #40 generalised, #32's version-specific note dropped, #52 carries the ruling that versioned replays are a post-1.0 question. #94 opened: a Red Flag set out at an Office's Limits holds the next train from that side and is drawn nowhere. `DivisionView`'s office node has no `redFlag` field and `board-svg.ts` never mentions one, so after the single log line announcing it there is nothing on screen. Same class as Gitea#21 and #22, and already on the common board's step 1 list. No engine change. 897 tests pass, unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3Qk7uresKCHksdZajXCLg |
||
|
|
819996faa2 |
v0.7.9.2 — two things the table could hear that only one seat should
Both leaks were found while planning the common board (Gitea#20 step 1), and both are live multiplayer bugs with or without that display, so they are fixed now rather than with 0.8.0. `game.log` is one shared list and `linesSince(seat)` slices it with no per-seat filter, so every line reaches every player. It carried the SEED in the opening line of each multiplayer game — the whole future of the deal — and the NAME OF A CARD DRAWN BLIND from the face-down Home Office deck. Solitaire deliberately keeps both: a one-seat table has nobody to leak to, the seed is what a bug report quotes, and a player's own history naming their own draw is the record. A Department slot is face up and stays named. The drawer still learns their card through `justDrawn`, which already goes to that seat alone. Neither was found by a test. Every test in `redaction.test.ts` passes an empty log, so the whole of narration has sat outside the redaction net since the net was built. Both now have tests there; TODO #91 carries what is still owed and supersedes #78, which described a gap that had already been closed and never mentioned this one. `docs/rules/` had no current description of the game, and `content.ts` named `card-reference.md` as the file that carries what the cards say — a file whose own banner says not to use its numbers, describing the v0.4.5 deck where 3/4 is a Mail-Express with three coaches. Every file in that directory is a deliberate historical record, so none of them is rewritten. `as-built.md` is new and GENERATED from the same catalogues the engine instantiates from, with a test that re-runs the generator and fails when the checked-in file disagrees. A hand-written replacement would have drifted the same way, for the same reason. TODO.md: #32 closed — the playtest migration note did its job and the jump is made; the durable fact it carried is kept. #78 retired in favour of #91. The "play it at a table" section now records that 0.7.4-0.7.9 were test-run without change requests, and that more testing comes at the end of the 0.7.9 series. 897 tests pass, up from 891. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3Qk7uresKCHksdZajXCLg |
||
|
|
7ade60e21f |
v0.7.9.1 — the engine was right twice; the screen was not
Gitea#22: the Division map drew every westbound train in the wrong half of its Mainline card. `regionOfTransit` counts from the end a train entered, which is what the collision rules ask; the map wanted "which printed box, left to right" and used the same number, so an eastbound train came out right by luck and a westbound one came out mirrored. It cost a collision — Train 3 was cleared to follow T5 and ran into TX17, which the picture had drawn ahead of T5 rather than behind it. One mirror in `view.ts`, at the boundary the map is drawn from; the collision rules are untouched. Gitea#21: a second tank car would not come off at a refinery, and "Blocked — why nothing is moving" answered by describing the refinery's green box. The real answer was Train 3's printed rule — the Express works one freight car per location — so the refusal was correct and the panel sent the player to spend a Freight Agent action that could not have helped. No rule changed. The panel now names the budget, asking the reducer's own predicate so its words cannot drift from the rule. Both were replayed from the saves attached to the issues and verified in the exact position each report names. The map fix is proved by mutation: reverting the mirror fails two tests, and making the renderer ignore the region fails a third. Every existing region test ran eastbound, where the mirror is the identity, which is why the bug survived them. 891 tests pass, up from 884. Closes #21 Closes #22 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3Qk7uresKCHksdZajXCLg |
||
|
|
6058f6c17e |
TODO.md: work sections first, the argument moved to the back
The file had reached 2,441 lines and had to be read end to end to find out what was live. Restructured with Jesse. WHAT WAS WRONG was not the ordering. Eight subject sections sat under a 43-entry chronological queue that had come to duplicate them: the queue was where the priority lived, the sections were where the reasoning lived, so every live item existed in both and drifted between them. That is what let eight items be closed in one place and left open in the other last week, which reads as live work and is worse than no entry at all. One item per place now. THE SHAPE. A short overview; then "Every time", the process this project runs on — the version bump being Jesse's call, signed commits and tags, deploying from the right line, the wrapper release sequence, and the lesson that only a test failing before the fix proves a fix; then an index; then ten work sections in priority order, one per area, each holding every open item for that area. Play it at a table first, because the file itself calls it the largest gap in the project. Gitea#20 second, as v0.8.0. Then multiplayer operations, the screen, replays, rules, balance, the bot, code health, documentation. THE ARGUMENT MOVED TO THE BACK, NOT AWAY. An item in a work section is a few lines: what it is, what it blocks, what it would cost. The measurements, rulings and rejected approaches behind it are in a Reference section keyed by the same number, moved verbatim. All 61 open items have one. Then Done, which keeps every closed item because several are the only record of a ruling or a lesson. Checked mechanically rather than trusted: every lead and every body line over 40 characters from all 120 items of the old file was asserted present in the new one. Zero fragments lost. Two rendering bugs the move introduced were caught the same way — bodies that were list continuations indented 4 or 6 spaces render as CODE BLOCKS once they are no longer inside a list, and a wrapped lead splits mid sentence if a blank line is inserted before its continuation. THE NUMBERS ARE PERMANENT IDS. Items keep the number they were raised under for life, wherever they later move, because commit messages, Gitea comments and other items refer to them by number. Nothing was renumbered; previously unnumbered items took the next free numbers. Items that existed only as queue entries — 32, 33, 35, 36, 39, 40, 42a — are proper items with checkboxes for the first time, which is why the open count reads 61 against last week's 54 without anything new being raised. Also carries the two items Jesse asked for while finishing 0.7.9. #44: how much history the panel holds should be configurable, with his three candidates left deliberately open — a StartOS action reaches only multiplayer, since solitaire has no server; per game puts a display preference into the ruleset and every save; per browser is where every other display preference already lives. The cap has no recorded reason anywhere, and the replay viewer already answers the same question in a different unit. #46: 29 unused declarations and no flag to catch them, with the argument that the flag matters more than the 29. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YTaNBL1jVxNqgFdjHkHoo3v0.7.9 |
||
|
|
e62ea54259 |
Four things the game counted and never said, and two it said wrong
Stays in the unshipped v0.7.9. Prompted by Jesse asking the general question after two v0.7.9 fixes turned out to be the same shape: actingPlayer existed and the Frame threw it away, and collisionsToday / collisionsTotal rode the Frame for three releases with nothing drawing them. So what else is computed, serialised and sent to nobody? THE AUDIT, done rather than guessed. Every one of Frame's 59 top-level fields grepped for a read across the seven renderers, then the same for Tally's 26 members. 55 of 59 are read. Four are not. tally.unloadsBegun was visible rather than merely unused. §9.1 makes loading and unloading the same shape — begun, then carried through — and the results screen printed "Loads still in the pipeline" for one side and nothing for the other, reporting half of a symmetric mechanism. tally.cardsDiscarded was counted by the engine and listed beside "Cards drawn" and "Cards played" without it, though Gitea#9 made throwing a Timetabled train away a deliberate move — a player CHOICE the game counted and never reported. Both are reported now. viewerSeat and overHandLimit are deferred by Jesse. The second is the fullest version of the shape: engine computes it, view.ts puts it on the Frame, session.ts declares it on the Session interface AND implements it twice, and the only caller in the repo is its own test. Four layers of plumbing, no consumer. The decision when it comes is delete-or-document, not a patch. Fixing the two turned up a third thing: resultsHtml draws tallyHtml(report?.tally ?? f.tally), and report is f.official, so a finished game reports the tally frozen at the official ending rather than the live one. The first attempt at a test overrode f.tally alone, changed nothing on screen, and failed for a reason unrelated to the fix. A SHOUTED KEYWORD IS NOT A SENTENCE. `EXTRA X18 started…` attributed to a player rendered as `Player Solitaire eXTRA X18 started…`, and the same happened to TRAIN 1 MADE UP and COLLISION. `record` folds a narration's opening word into the middle of a sentence and did it with a flat charAt(0).toLowerCase(). It now folds only a sentence-cased word — ^[A-Z][a-z], a capital followed by a lower-case letter — which also leaves X22 Pee-Dee alone, where a naive uppercase test gets it wrong because '2'.toUpperCase() is '2'. It had been filed under Play Balance, where it has no business being, which is how it survived a session that had ruled balance work out of scope. A REPLAYED SAVE NOW NARRATES WHAT THE LIVE GAME NARRATED. fromSave's loop called record(game, result.events) with no actor, so every restored save, every undo (which rebuilds through fromSave) and the replay viewer stripped the "Player X" prefix off every attributed line. submit attributes and fromMultiplayerSave attributes; this was the one path of three that did not. One argument, with actor already computed on the line above. Why it survived: nothing ever compared a fromSave-built log against a LIVE-played one. The single log-comparing test compares undo's rebuilt log against another fromSave-built log — and undo itself rebuilds through fromSave — so the gap cancelled out on both sides. The suite was green with the bug in and green with it out. The new test plays a game, saves it, restores it and asserts the two logs are identical: the missing direction, not a new requirement. All three fixes were confirmed to go RED with the fix reverted before being called done. 884 tests pass, seventeen new. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YTaNBL1jVxNqgFdjHkHoo3 |
||
|
|
d267f89a82 |
The screen does what you tell it — three Display items, and a fourth declined
Reviewed with Jesse out of TODO.md's Display section. Stays in the unshipped v0.7.9. #17 (hiding the Division map) was DECLINED, and the reason is that its premise had already died. Gitea#18 replaced the wrapped layout with a single row, and the reason to fold the map away was that it GREW — a horseshoe of three or a square of four pushed the board off the screen. One row is boardH = PAD * 2 + CH + 30: 150px, fixed, at every seat count. That is not worth a control, three states and a persisted preference. It was a sixth member of the drawing pass that got closed with Gitea#18 and stayed open only because it reads as a control question rather than a drawing one — recorded in TODO.md as an explicit decision, with the design that had already been worked out kept, and with the one thing that would justify reopening it: the map growing again. #16 THE OFFICE AREA'S AUTO-HIDE COULD NOT REACH EVERY STATE. One button cycling auto -> pinned -> auto, where the pin was `open ? 'closed' : 'open'` and `open` is what auto is doing AT THAT MOMENT. So the pin a press offered depended on the phase, and going from always-show to always-hide meant clicking back to auto, waiting for the phase to turn over, and clicking again. Three controls now, one per mode. The labels still say what pressing DOES, which was an earlier deliberate fix; what the cycle could not do was report the state it was in, and aria-pressed carries that now. They are addressed by id rather than queried off the container, and that is testability rather than style: the stub DOM the web suite runs against only models markup the page WROTE, so a child query finds nothing and the control would have shipped green and unexercised. The test presses always-show to always-hide directly — the transition the cycle could not make. #23 THE HISTORY READS NEWEST FIRST. Jesse: "the top line is the most recent and the further down you go, the older the entry." The phase headings now trail the lines they announce, ruled acceptable rather than overlooked: "stage changes will be beneath (prior to / older than) the following events. That is OK." Reading down is reading backwards. Grouping by phase and reversing the groups was offered and declined as more machinery than the complaint needs. replays.ts keeps its oldest-first log deliberately — it is paired with a frame stepper, where newest-first would fight the stepping. The slice(-60) cap is untouched and stays open. #28 THE SETTINGS MOVED INTO A CARD. The top line carried six things and now carries four: Revenue, the objective, the collision counts and the game code. The rest is a This Game card at the foot of the right-hand column, folded by default. Nothing new travels for it — configFromFrame already existed and main.ts already called it three times, so rulesListHtml(configFromFrame(f), ...) needed no refactor, and the card draws from the same renderer as the lobby's join preview so the two cannot drift. THE COLLISION COUNTS ARE NEW ON THE BOARD, NOT MOVED. The Frame has carried collisionsToday and collisionsTotal since v0.7.0 and nothing drew them, so the one victory condition that ends a game EARLY ran invisibly — the second time this release that the Frame had the answer and the view never asked (see #43's actingPlayer). They stay on the top line while the limits go in the card: a limit is agreed to once, "2 of 3 today" changes how you play the next Stage. One stub gap closed to get here: none of the five element factories in test/web.test.ts had setAttribute, so the first render threw and any control reporting state through ARIA was untestable. WHAT IS NOT VERIFIED: the layout. There is no browser on this box, so nothing has confirmed the segmented control, the card or the reversed panel look right on screen. The logic is tested; the appearance is not, and wants the next play session. 881 tests pass, fourteen new. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YTaNBL1jVxNqgFdjHkHoo3 |
||
|
|
31b942cc38 |
TODO.md had stopped tracking its own closures
Nothing was wrong with the ordering. What had gone wrong is that eight
items were done or superseded in one place and still open in another,
which reads as live work — worse than no entry at all. Found by
reviewing the file against the Gitea tracker and the git log, not by
reading it.
NEXT WAS 43 CHRONOLOGICAL ENTRIES, 26 OF THEM STRUCK THROUGH, and the
live work had stopped being visible in it. It is grouped by what each
item is WAITING ON now — blocked on a table, the screen, waiting on
something outside the code, unscheduled, standing practice — with the
closed entries moved to a Settled block at the end of the section.
EVERY NUMBER IS KEPT AS A STABLE ID. The rest of the file refers to
items by number ("decide with item 15", "same drawing pass as 19-21"),
so renumbering would have silently broken every cross-reference.
Numbers are ids, not positions, and the file now says so. The one
reference that pointed at a settled item (item 22, the dead centre of
the Division map) is rewritten to name the principle that outlived it
rather than the item.
EIGHT STALE ITEMS CLOSED. The Heavy Grade orientation was fixed in
|
||
|
|
bb1b661211 |
A game you come back to has not begun
Reported by Jesse, 2026-08-30: "when you are continuing the saved game out of that screen, do not post a message that says 'The game has begun.' … it needs to say 'The game has resumed.'" A restored game draws exactly like a dealt one — mid-Day, mid-phase, with a log already several turns deep — and solitaire said nothing at all to tell the two apart. It flashes "The game has resumed — Day 3, Stage 7" now, on both ways back in: the setup screen's Continue saved game, and a bare reload that restores the save. THE SAME LINE WAS WRONG ON THE MULTIPLAYER SIDE, IN THE OTHER DIRECTION. noteFirstFrame guards on firstFrameSeen, which is per page-load — so re-entering a game this browser already held a seat in, by reloading mid-game or picking it out of the lobby's list, announced that the game had BEGUN to somebody who had been playing it for an hour. beginRemote carries whether this is a rejoin now, and the line reads "resumed" when it is. Both halves are pinned, including that a brand-new game does not claim to be a resume — an announcement that fires either way says nothing. Stays in the unshipped v0.7.9. 878 tests pass, eleven new. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX |
||
|
|
788e5f2eec |
The save warning, the buttons, and a claim that was simply false
- The save warning is a warning: 15px, weight 500, bright amber on a deeper ground with a 5px rule down the side. (What Jesse was looking at is v0.7.8, where this line is still the small grey .ng-note — none of 0.7.9 has been deployed.) - The buttons read the same on both screens: "Continue saved game" and "Create new game". Solitaire said "Continue Existing Saved Game" and "Deal New Game", the lobby said "Create game" — three phrasings for two actions. - "Off in every game type" is deleted from the Optional rules note because it was not true. Checked against the presets rather than taken on trust: discardTimetabled (§6.2, a Timetabled train may be discarded) ships ON in all four types, not just Co-op. The note now says only what holds for all of them. Stays in the unshipped v0.7.9. 877 tests pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX |
||
|
|
a70b7f88f3 |
Say who the game is waiting on, and move the Fedora; replay in words
Three items folded into the unshipped v0.7.9. WAITING ON (reported by Jesse from play). The status line said "nobody — the Division is running itself" while the game was stopped on the Superintendent. Frame.actor carried clock.currentActor, which is null for the whole Mainline Phase, so all three interruptions — §8.1's clearance ruling, Gitea#5's Yard Office offer, Gitea#19's Red Flag prompt — reported that nobody was holding it up. actingPlayer had the answer since the Gitea#5 refactor; the Frame threw it away. It carries actingPlayer now, plus a new `awaiting` field naming the question and the train: "waiting on Bob · a clearance ruling · Train 4". Naming the person alone is not enough when three different things can be pending. THE FEDORA (TODO #29) rides at the right-hand end of the phase row instead of a line of its own, and wraps under rather than squeezing the chips. THE DEVELOPER REPLAY (TODO #34) printed "loss — revenueFloor", the same defect Gitea#16 was filed about, still alive because nothing player-facing pointed at it. panels.ts's reasonSentence is exported and shared rather than reimplemented, fed the last recorded frame and stripped of markup. The drift test maps win/loss to won/lost so it still checks the two AGREE rather than that they are spelled alike. Also carries the previous, unsigned commit's work: the two setup screens worded the same section by section. 877 tests pass, ten new. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX |
||
|
|
131538dc7c |
Two setup screens, not three — the in-game dialog is deleted
Jesse: "it should not go to a separate screen. We should reuse the Solitaire New Game Screen… in general we should reuse what we already have." #newgamedlg was a third copy of the same questions and the one that drifted: shown only to a solitaire player, it asked "Everyone loses if COMBINED Revenue…" and explained Employee Rotation in full multiplayer terms beside a control it had disabled. Both were on the list to re-word; deleting the screen removes the drift instead of restating it. New game opens the setup screen IN PLACE rather than navigating, so the live session stays in memory: the fields open on the rules actually being played (what the dialog was good for), and Continue Existing Saved Game puts the board back with no reload. render() calls save() every frame, so nothing is at risk either way. The two remaining screens now match below their headers — same three parameters in the same order, same seed note, same chair note. Solitaire shows Players at the table locked at 1 rather than omitting it: a fixed control says "same form, table of one", a missing one made it a different form sharing a rules block. Drift guard drops to two prefixes and now fails if any ng- id returns. Stays in the unshipped v0.7.9. 874 tests pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX |
||
|
|
cf018b4a5f |
A Heavy Grade shows which way it climbs
Jesse: "heavy grade mainline card tooltip states climbs east, but card doesn't show it." The Frame has carried gradeUp all along and the tip has said it; the card drew nothing, so the one Mainline card whose orientation is set per game was the one you had to hover to read. A brown wedge in the lower right rising toward the climb, with a bone arrow lying along its slope, centred on the triangle's centroid. East is right on this map (Gitea#18), which is what lets a wedge be read without a compass. Four passes. Up the hypotenuse the arrow began at the wedge's thin corner, where there is no height, so its head read as clipped. Level, it was contained but did not read as climbing. At 30° — steeper than the wedge's own 22.5° — it had to be tucked into the fat half. Parallel to the slope is the shape that fits: the gap to the hypotenuse is then constant, so the arrow can sit centred. The wedge grew to 58x24 to pay for it, since a centred arrow has less room than an off-centre one. Sizes are a search result, clearing every edge by 2.88px. The first containment test bounded the arrow against the CARD, which it never left, while the wedge clipped it — a green check on a visibly broken glyph. It checks the TRIANGLE now with a 2px floor, plus parallelism derived from the wedge and centroid placement. Orientation is always set, measured not assumed: 400 seeds x 4 player counts, 535 grades placed, 0 without one, 276 east / 259 west. Stays in the unshipped v0.7.9. 874 tests pass, one new. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX |
||
|
|
45cf521a40 |
configWith let the day count and the Revenue floor disagree
minCombinedRevenue fell back to SOLO_CONFIG's constant — the floor for a
FIVE-Day game — whatever days said. configWith({ days: 1 }) asked a
one-Day game to clear 15, which a full five-Day game averages barely
half of; configWith({ days: 10 }) asked for that same 15. It derives
from the days it was given now.
Not a live fault: createLocalSession is the only caller and the page
always writes the floor itself, so no dealt game was ever wrong. Found
by a throwaway probe that passed only days — which is how the next
caller would reach for it. Unchanged at the default day count, since
SOLO_CONFIG's floor is this same formula at DEFAULT_DAYS.
Stays in the unshipped v0.7.9 per Jesse — no version bump for the next
several fixes. 873 tests pass, three new.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX
|
||
|
|
e035dda5a3 |
The extension question was hidden behind the results screen (Gitea#11)
Jesse, playing v0.7.8: "Solitaire game ended. I did not have an option to extend the game by a day." The engine and the Frame were right — checked before changing anything. A solitaire game at the end of its timetable reaches awaitingExtension with extensionVotes [null], and renderEnding writes "play one more Day" into #actions. It then opens #resultsdlg, which is MODAL, so those buttons were directly underneath a dialog whose only control was Close. The results dialog now carries the question itself, hidden unless a vote is pending: Play One More Day / End the Game Here, casting the same game.extend intent. The #actions buttons stay as the fallback once it is closed. TODO.md #35 recorded extended play as verified on phoenix.local — over the HTTP API, which renders no dialog. What was proven was that the server supports it, not that a player can reach it. Noted there. Rides along in the unshipped v0.7.9. 870 tests pass, one new. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX |
||
|
|
f2c87b6871 |
v0.7.9 — solitaire setup screen feedback, and the dead settings it exposed
Four pieces of feedback from Jesse on the solitaire setup screen. THE COLLISION LIMITS DID NOTHING IN SOLITAIRE. Asked to reword those entries to "the game ends immediately and results in a loss", which was unwriteable: advance.ts gated the §3.4 check on competitive/coop, and a solitaire game's mode is 'solitaire'. Both limits were offered as live settings, rode into the config, and never fired — the existing text was already false. The exclusion was never a stated rule and nothing recorded a reason for it. Jesse's ruling: the settings do what they say, so the gate is gone rather than the controls. Measured, not asserted — 200 standard developer-bot games: loss/collisionFloor 1 in 200, Days played 5.00 -> 4.98 mean with a minimum of 1, collisions per game unchanged at 0.14. Recorded in TODO.md under Play Balance, since full-length figures predate it. Extra start defaults to ownOffice: at one seat it is the same rule as anyOffice (apply.ts only rejects another seat's start), so this is a label fix with no gameplay effect. Also: collision wording on all three screens, Employee Rotation reads "not applicable for solitaire", and the save warning is legible at 14px on an amber panel with buttons that say Continue Existing Saved Game and Deal New Game. 869 tests pass, two new; one asserted the opposite of the ruling and says so where it was reversed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX |
||
|
|
193800a649 |
v0.7.8 — the setup screen was unreachable for anyone who had ever played
Third report of the same symptom, this time with the build confirmed current on screen, which ruled out v0.7.7's caching fault and left the real cause exposed. v0.7.5 skipped the setup screen whenever load() found a save, reasoned as "a saved game is a game to resume". A browser that has ever played solitaire always has one, so the door could never reach the screen again — and the fresh private window that appeared to vindicate v0.7.7 simply had no save. Two real faults were stacked; the caching one is fixed and had been masking this. The door outranks a saved game now: ?solitaire is a request to set one up, while a bare reload still resumes (pinned by its own test). Since Deal clears the save, the screen carries #ss-resume and says what Deal costs, so the door cannot destroy a game in progress. Also, per Jesse, riding along rather than taking its own release: the splash footer now names both ways to play. 868 tests pass, four new. The reproduction was a failing test written before the fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoXv0.7.8 |
||
|
|
af68aac78d |
v0.7.7 — two releases shipped to a browser that never received them
buildStamp()'s no-git fallback was the literal "nogit", and the .s9pk Dockerfile copies the tree in without .git — so git rev-parse fails on every packaged build. That string is also the cache-bust key every module URL carries, so v0.7.4, v0.7.5 and v0.7.6 all published ./web/main.js?v=nogit, byte-identical, and returning browsers refetched nothing. v0.7.5's setup screen and v0.7.6's door fix were both correct and neither arrived. The fallback is now the package version plus the build timestamp, always distinct. And serveStatic sent no Cache-Control at all, which is the other half — a cached play.html pins a player to the whole build it names. A request carrying ?v= is now immutable for a year; everything else is no-cache. ?v= rather than "not HTML" because build-web.ts tags the modules and nothing else. Neither half is sufficient alone. 864 tests pass, two new. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoXv0.7.7 |
||
|
|
b4f09f05cb |
v0.7.6 — the solitaire door could not reach solitaire
Found by Jesse playing v0.7.5 on phoenix.local: a browser that had ever held a multiplayer seat could not reach the new solitaire setup screen at all. start() checked a browser-remembered multiplayer session before ever looking at solitaire's own state, and a bare ./play.html load could not tell "clicked Play solitaire" apart from "reloaded mid multiplayer game" — the same problem ?lobby already solved for the door on the other side, never applied to this one. The door now links to ./play.html?solitaire, and start() treats that, an explicit ?seed=, or the setup screen's own ?hand= (written by every Deal) as proof this navigation means solitaire — checked ahead of the remembered-session lookup rather than only below it. 862 tests pass, three new. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoXv0.7.6 |
||
|
|
3e961496b0 |
v0.7.5 — solitaire asks before it deals, the same way multiplayer already does
A new #solitairesetup screen in play.html asks the full shared game-options block — type, starting hand, Extra start, revenue, victory conditions, optional rules — before a genuinely fresh visit deals a game. A saved game, an explicit ?seed=, or a URL a Deal already wrote all skip past it, same as ?lobby already skips the front doors on an invite link. The in-game dialog, the lobby and this screen now share one wireGameTypeBlock()/commitNewGame() pair instead of the dialog carrying its own copy of the questions. 859 tests pass. Not yet played in a browser. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoXv0.7.5 |
||
|
|
a02d1fcffe |
TODO: v0.7.4 is shipped and installed, and four features are unplayed
Records the close of the 2026-08-29 session. #38, done: Gitea#13, #5 and #19 in v0.7.4, wrapper 085b88b as 0.7.4:0, installed on phoenix.local. Each issue carries a comment naming its commit and what was ruled, per the standing instruction in #30 — auto-closing alone leaves no record of which release answered a report. #37 extended: the wrapper went 0.7.3 then 0.7.4 the same day, and the sequence is written down rather than rediscovered next time. Both tags signed and pushed. Three things the session leaves behind, and the first is the one that matters: #39 — NOTHING FROM v0.7.4 HAS BEEN PLAYED BY A HUMAN, and nor has extended play from v0.7.3 (#35). Four features shipped without a table between them. Two of them are interruptions that stop the Mainline Phase and put a question in front of somebody mid-thought, which is exactly what only play reveals. #40 — a save from before v0.7.4 may not replay, and nobody has been told. Same shape as #32 for the playtest line. It fails safe and WHISTLE-4086 did survive on phoenix, so "may not" rather than "will not". #41 — the bot still cannot use the half of Red Flags a human would: planting a flag on purpose to buy a Stage for switching. It takes the danger prompt unconditionally and still plays zero in 200 games. Also de-duplicates #35, where an earlier edit left the superseded paragraph appended to the rewritten one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EAgJSmeV8zrMh55Mj85ESb |
||
|
|
19a6a47ab6 |
v0.7.4 — Red Flags hold a train out of your Limits (Gitea#19)
"If played, asked FLAG EAST or FLAG WEST. That stops all trains from entering your limits from that direction (i.e. Flag East holds westbound trains). You can do this if you see a problem or wish to complete switching." REPLACES the old rule outright, per Jesse's call. Red Flags used to be played on a stopped train out on the Mainline and protected it from a rear-ender: offered 4,212 times and played 4 across 600 games, a mechanic nobody used, and ABS Signals already does that job better. The flag is now planted on one side of your own district and holds the next train arriving from that side. SPENT ON THE TRAIN IT STOPS. One card, one train, so there is no lifting action to build, nothing to forget, and a flag cannot quietly strangle the Division. The held train loses one Mainline Phase and comes in on the next — it buys a Stage to clear the lead, which is what "wish to complete switching" asks for. PLAYABLE OUT OF PHASE, which is the other half of the issue: when an arrival would certainly collide and the district's owner holds the card, the phase breaks in and asks. Offered ONLY to somebody holding one — a prompt with a single button is not a choice, and it would leak that a collision is coming. The danger is read from §8.3's own two triggers rather than restated, so the prompt cannot offer a flag against a collision that will not happen. Built on the decision union Gitea#5 introduced: this adds a `redFlag` case and nothing else structural. THE BOT STILL NEVER PLAYS IT, AND I MEASURED RATHER THAN ASSUMED. It now takes the out-of-phase prompt unconditionally — the engine has already established the danger, so there is nothing left to judge — and over 200 solitaire games `redFlagsSet` fires ZERO times. The prompt needs an arrival that would collide (0.14 per game, about one game in seven) to coincide with holding the card from a three-card hand out of 121. So the anomaly exemption in sim.test.ts stays, but its comment no longer claims the bot is unwilling: it is measuring deck luck. What is left to fix is the half of the card a human would use, planting a flag on purpose to buy switching time, and TODO.md now says that instead of the old finding. A BUG WORTH RECORDING, because the next interruption will meet it too: the flag was originally taken down in a `reduce` case, which never fires for an event advance.ts emits — the phase driver mutates state and then describes it. The flag stayed up and held every train that came. test/events.test.ts's unreduced-event registry is what makes that class of mistake visible, and `redFlagSpent` is on it deliberately now, with the reasoning. 858 tests pass. Closes #19 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EAgJSmeV8zrMh55Mj85ESbv0.7.4 |
||
|
|
228027637b |
The Yard Office is offered, reachable, and can be run into (Gitea#5)
"Trains that are only freight (cabooses ok, no coaches allowed) that arrive in a
player's area who has the yard office card get an extra ability… the game will
offer that player the option… They can of course still choose to have the train go
to the standard office."
It was implemented, in a stripped form missing all three conditions: a qualifying
train was TELEPORTED onto the Yard Office card. Nobody was asked, no route was
computed — so the card's own printed "that can reach the yard office in one move"
was unenforced — and because nothing was walked, nothing was ever met on the way.
All three now hold:
- OFFERED to whoever sits in the district, interrupting the Mainline Phase on the
turn the train arrives. Declining is an ordinary arrival onto an A/D track.
- REACHABILITY is the engine's own move walk. `exploreMoves` already means what
the card means — any distance without changing direction, finishing on
Operational Rail (§2.4, §A.1) — so using it is what makes code and card agree.
Reversing is a separate Move, so a yard that can only be reached by backing up
is correctly out of reach.
- CARS ON THE LEAD COLLIDE. The walk does not treat standing cars as obstacles;
it COUPLES them, because that is what a switching move does. An arriving train
is not switching, so what it would have coupled is what it is about to hit —
the same reading §8.3 already applies to the Running Track. `destination.couples`
is therefore the fouling signal, and it needed no new machinery.
Per Jesse's ruling (2026-08-29) the two failures his issue names are kept apart: no
route means no offer, with the history saying why ("make sure this is logged in
history — why can't move so user knows why they can't get to yard"); a route that
exists but is fouled IS offered, and taking it crashes. A silent absence is
indistinguishable from a broken feature, which is how the missing check survived.
THE SHARED REFACTOR THIS NEEDED. `pendingDecision` was one question asked of one
player — §8.1's clearance, always the Superintendent — and `currentActor` hardcoded
that. It is a discriminated union now, with `decisionActor` as the single place that
maps a question to whoever must answer it, and `clearanceRuling` generalised to
`decisionAnswer`. Six copies of `pendingDecision !== null ? superintendent :
currentActor` across the engine, the sim, the web client and the tests collapse into
`actingPlayer`; they had already stopped being right the moment a second kind of
question existed. Gitea#19 needs the same machinery and now only has to add a case.
A BUG THE FIRST CUT WALKED INTO, worth recording because it is a trap the next
interruption will meet too: the offer must be put BEFORE the train is taken off its
Mainline card. `needsClearance` unwinds the whole phase and the driver re-enters
from the top, so asking after the `transits` filter cost the train its place on the
card and the answer had nowhere to land. §8.1 gets this right by asking before it
commits, and the Yard Office now does the same.
The developer bot declines: the Yard Office frees an A/D track, but the lead may be
fouled and the bot cannot read its own yard well enough to tell (`TODO.md`, Bot
Performance). Declining is always safe and keeps the harness comparable with every
measurement taken before this rule existed.
851 tests pass.
Closes #5
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAgJSmeV8zrMh55Mj85ESb
|
||
|
|
5e34c73b16 |
Extras that must run loaded, and a circus paid per district (Gitea#13)
"I've redefined some of the extra trains that they have to run full boxcars —
military trains, circus trains, etc. If not loaded, then empty, and if none
available, run without."
MAKE-UP. X17 Campaign, X18 Circus and X19 Military carry `mustRunLoaded`. It is a
preference order rather than a flat requirement, so the rule is asked of the
DIVISION YARD: an empty is refused only while the yard can still supply a loaded
car this train would accept, and once it cannot, the empty is legal and the train
may still depart short. Per category, since that is the slot the car competes for
— a loaded coach is no reason to refuse an empty boxcar.
SCORING, per Jesse's ruling (2026-08-29): "once per stop in an office area. In a
multiplayer game, each player could score if the circus stops in their area." So
`stopPointClaimed` (a boolean, once per game) becomes `stopPointSeats` (the seats
already paid). A Circus touring three districts is paid three times; one parked in
the same district all game is paid once. The other half of the ruling — "if the
circus train gets recycled and played a second time as a second extra, then it
could again score points later too" — needs no code: a train is made up onto a
fresh tray every time, so a re-played Extra starts with an empty list.
The point now requires the train to be FULLY LOADED, meaning every non-caboose car
loaded. A coach counts as loaded when occupied, which is what makes this the right
test for the Campaign Train: X17 carries one coach and no freight, so "fully
loaded" is exactly "the candidate is aboard". X17 also GAINS the per-stop point —
it had `stopThenExpedite` and no scoring rule at all, and Jesse's "credit for a
circus or campaign train (one point per stop)" says it should score.
TWO BUGS FIXED ALONG THE WAY:
- `ConsistSpec.emptiesOnly` was declared on X13 Appleseed, RENDERED to the player
as "(empties only)" by both web/game.ts and sim/view.ts, and enforced by
nothing — `acceptsCar` never read it, so the Appleseed could be made up loaded
while its own card said otherwise. It is the same rule as this issue pointing
the other way, and it would have been perverse to add one and leave the other.
- Setting up out on the Mainline paid a point to PLAYER 0 whoever was playing:
`playerAtSeat` needs a seat, off the grid there is none, and the fallback was
`0`. Scoping the rule to Office Areas is what the ruling says and removes the
misattribution rather than patching it.
Both loading rules exempt the caboose: every caboose in ROLLING_STOCK_SUPPLY is
minted loaded, so an unexempted rule would bar the one car a consist lists by name.
`trainNeedingCars` now asks the full question per car rather than the shape
question. It shares its predicate with `check` precisely to avoid the stall its own
comment describes, and the shape question stopped being the same question: an
empties-only train facing a yard of loaded cars would have been told a car was
available and then refused every one.
The two published replays that stopped replaying under the new rules were retired
and re-recorded with save-replay.ts, which verifies each candidate before writing
it. That is what test/harness.test.ts is for and what its comment prescribes.
846 tests pass.
Closes #13
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAgJSmeV8zrMh55Mj85ESb
|
||
|
|
9ae8e9e09d |
TODO: extended play verified against the running server on phoenix.local
v0.7.3:0 is installed. #35 keeps its heading — nobody has played this at a table with humans — but the engine and server half is no longer merely compiled. Dealt a two-seat competitive game over the HTTP API with days: 1, played it to the end of its timetable, and watched it reach `awaitingExtension` with the official result frozen at config.days. Voted yes; the bot followed; the game returned to active with extraDays: 1 and the official outcome unchanged. Both test games were deleted afterwards. The carry-over claim was checked the same way rather than asserted. phoenix held five saves before the update, one resumable; after it the resume log is identical — same game, same 7 intents, same three refusals at the same move with the same code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EAgJSmeV8zrMh55Mj85ESb |
||
|
|
5865c3a6b7 |
TODO: the wrapper is on 0.7.3, and #13 is the harder reading after all
Three updates from the 2026-08-29 session, none of them code. #37, new and already done: the StartOS wrapper is bumped to 0.7.3 (`74aea24` in station-master-startos). Recorded with what was verified — check, prettier, pack — and what was not: it is not installed on a box and has not been played, which is #35 and still open. #13 is settled, and as the reading that costs more. Jesse: "I want to be able to watch other players and bots make their moves. It's not fun to do my turn and have magic happen in the background and then have to figure out what others did." The entry had explicitly left open which of two things was meant — a log-legibility problem or a new view — and it is the second. It is Gitea#20 step 4 pointed at a player's screen rather than the common board, and `docs/plans/jitsi-common-board.md` already has the mechanism; what it does not have is the seated-player half, which that plan deliberately excludes. Jesse: "this relates to issue #20 and will require a lot more thinking." Marked to be designed with #20, not started alone. #7 is on hold: StartOS 0.4.0.2 is expected to improve action displays, and the diagnosis behind that item is that the action-result view collapses newlines — exactly the sort of thing a platform release fixes. Re-read the real output before designing around a limitation that may have been lifted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EAgJSmeV8zrMh55Mj85ESb |
||
|
|
45580d8b61 |
v0.7.3 — a game that asks before it ends, and a results screen worth reading
Two issues off the tracker, and they are halves of one thing: the end of a game. Neither ships on the 0.4.9 line — Jesse's call, that line may be complete and these are not fixes people mid-playtest need. EXTENDED PLAY (#11). The official result is settled at the original game length and never changes: in a five-Day game extended to eight, the winner is whoever led at the end of Day 5. Extending grants exactly one Day and the question is put again at the end of it — solitaire the player decides alone, multiplayer it is unanimous and one refusal ends it there. Only days-based endings offer it; a §3.4 collision breach is final, during an extended Day exactly as during the scheduled game. It could not be a client-side change. `check` refused every intent once `status` left `active`; the server never loads a `finished` game back into memory; and a save is `{ seed, config, history }` replayed through the engine, so a "continue" the history does not record did not happen. Hence a fourth status, `awaitingExtension`, and a `game.extend` intent. `config.days` never moves — `extraDays` counts the borrowed Days and `official` freezes the outcome, the standings and the statistics at the first ending. THE RESULTS SCREEN (#16). `GAME OVER — revenueFloor` was `outcome.reason`, an internal enum interpolated into the page at the one moment the game has the player's whole attention. Every reason now has a sentence with the game's own numbers in it. Around it: the result and winner, standings, the rules the game was dealt under, a per-player breakdown, and the railroad — trains through the Division and how many worked en route, loads made up and broken, passengers, cars switched, trains destroyed. It shares the Day-end dialog's blocks rather than reimplementing them, and stays reopenable so continuing does not cost you the results. Statistics are folded, not recorded: `state.tally` counts what the event stream says happened, hooked at `applyIntent` and `advance` because `reduce` never sees the phase driver's events — and those are the interesting ones. Nothing in the rules reads it, and it rides the Frame, so multiplayer gets the same numbers as solitaire from one implementation. THREE BUGS FOUND IN TESTING, all of which would have shipped: - a saved game containing a vote could not be resumed (NO_ACTOR). A history is a flat Intent[] with no seat recorded; the replay derives who acted from the turn order, which cannot work for an intent every seat may send in any order. `game.extend` carries its voter, checked against the authenticated seat. - an all-bot game hung on the question for ever. `driveBots` loops on `currentActor`, null the moment the game stops, so it cannot cast a vote, and the bot-vote driver returned early with no humans to follow. - the balance harness became unbounded — `test/sim.test.ts` went from under a second to never finishing. `randomBot` took another Day about half the time, so every seeded game ran to playGame's 50,000-turn cap. Fixed in the driver, not in a policy, so it holds for bots not yet written. All three have regression tests. 832 tests pass, against 793 before this change. NOT BUILT, and a correction. #16's own comment said `trainStoodStill` "is emitted per Stage, so a run of them is exactly the sat-on-a-siding streak". It is not: reading advance.ts, it fires once per game and only for a train whose profile sets `stopEarnsPoint` — the X18 Circus — with `stopPointClaimed` preventing a second. The streak was built, rendered "1 Stage at (0,0)", and was taken out again. There is no per-Stage "this train did not move" signal in the engine, so "longest an engine sat on a siding" needs one first; TODO.md #36 records what it would take, and the Circus set-up is reported instead. Badges remain the second pass #16 asks for (TODO.md #33), and because the statistics are derived rather than recorded, that pass can add any of them retroactively to games already played and saved. Extended play has not yet been played at a real table (TODO.md #35): the multiplayer vote has only been driven through `session.intent`, never through two browsers. Closes #11 Closes #16 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EAgJSmeV8zrMh55Mj85ESbv0.7.3 |
||
|
|
510e33bac7 | TODO: the wrapper is on 0.7.2, and the playtesters need telling their saves are dead | ||
|
|
c4a08433ba | TODO: the release follow-ups, and the wrapper bump is waiting on a v0.7.2 tag | ||
|
|
2ab25e320c |
v0.7.2 — a leg that is part of the row, the deck the sheet prints, regions not miles per hour, and a Division you read left to right
Gitea#17 — a 45° leg is an end of the west-to-east row, so backing into a cut through a curve's south leg no longer couples it back to front. The same assumption left a crew's own cut standing when it pulled out through a leg, which is the "cars left behind" report we had failed to reproduce. Gitea#14 — every count is docs/Deck cards5.xlsx. Track halved, and the Q12 office doubling and Gap 12 industry tripling both come out with it: they were measured against a deck with twice the track, and keeping them at the sheet's track count wipes out the reefer chain entirely. 84 rows now match card for card; the ten Safety, Event and Inspection cards it adds are not built and are held out. Cards the sheet no longer lists are dealt zero copies rather than deleted, so their rules stay implemented. Gitea#15 — RAR reversed it: a rail may stop dead against its neighbour and the placement is legal. What must hold is that no train crosses the gap, which was already true and is now pinned against the reported board. Gitea#3 — the printed speeds are scenery. A card costs one Stage per printed region and where a train STARTS is what varies; Fast/Slow is read on Hilly alone. Entering a one-region card behind another is a collision now, which is what ABS exists to prevent, and ABS no longer holds trains silently. Gitea#18 — the Division draws as one row, west to east, with no office-area detail. East is finally always to the right. Closes #3 Closes #14 Closes #15 Closes #17 Closes #18v0.7.2 |
||
|
|
441447648d |
v0.7.1 — a caboose is not a load, a Day that says it ended, and a train you may throw away
Four issues off the Gitea tracker, all of them things a player saw at the board. Reasoning for every item, and what was verified how: CHANGELOG.md. - Gitea#8: X22 Pee-Dee refused every caboose, including the one it was made up with, so setting it out stranded the train. All six cabooses are minted loaded because §2.2's "coloured is loaded, white is empty" doubles as a piece count in the supply table; one read of the flag took that literally. A caboose carries the crew, not freight, so it is never a load. - Gitea#10: a Day turns over inside the phases that run themselves, so it passes between one click and the next — and both transient signals fade before a player reading the board notices. A modal stops and waits, carrying the standings, the Days left and the combined target. Suppressed on the first frame, on Undo stepping back across a rollover, and on the Day the game ends. - Gitea#9, which SUPERSEDES Gitea#6 from three days ago: a Timetabled train may be tossed face-up to a Department slot, where a rival may pick it up — the second half of the ruling needed no code, since that is where every discard already goes. An Extra still may not. A New Game setting on this line (discardTimetabled, on by default), the plain rule on the 0.4.9 line. - Gitea#2 is not an engine bug: the rules are implemented exactly, and running the coach pool dry is Jesse's ruling to keep — "part of the strategy". What was wrong is that the game said nothing. A blocked platform now gives its reason, from the engine's own predicate, including how many coaches are stranded in Classification and what brings them back. The same four ship as v0.4.9g on the playtest line. Closes #2 Closes #8 Closes #9 Closes #10 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLnYR4XtXQNamYJXGYT8oCv0.7.1 |
||
|
|
603d38602c |
TODO: record the 0.4.9 divergence on the Local's coach
Checked while answering whether v0.7.0 needed porting to the playtest line: almost none of it does, but §A.4's coach ruling from v0.5.0 is a code difference that line never received — `!atOffice` on the coachStaysOnStationTrack check. Jesse's call is not to port it mid-playtest; written down so the divergence is a decision rather than a surprise, and so that line's README is not "corrected" into describing behaviour its build does not have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016JczK5i33ZNSf2PtzZqdhS |
||
|
|
06db36e5b5 |
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_016JczK5i33ZNSf2PtzZqdhSv0.7.0 |
||
|
|
42adfda390 |
v0.6.3 — the deploy script follows the host to FileBrowser Quantum
The deploy script only. No rules change, no game change, and the built site is byte-for-byte what 0.6.2 produced — this repairs the path that publishes it. Found trying to publish the 0.4.9f playtest build: every deploy died with "login failed: 404 404 page not found". The File Browser host has been upgraded to FileBrowser Quantum, a fork whose API differs from the v2.63 one deploy-web.ts was written against. Three things moved at once, each fatal on its own: auth is a session COOKIE rather than a JWT sent back as X-Auth, so a deploy that ignored it would authenticate and then be rejected by every upload; the password is an X-Password header, URL-encoded, rather than a JSON body field; and the path is a query parameter, with every resource call also having to name a `source` — Quantum can serve several named stores and refuses any call that does not say which, a concept the v2.63 API did not have. Rewritten against the running instance's own bundle rather than guessed, the same discipline the v2.63 version was written with. Two things worth knowing next time: the bundle is gzip-compressed, so it needs gunzip before it can be grepped; and an endpoint that exists answers a bad password with 401 while a missing one answers 404, which is how each path was confirmed against the live host without holding a password. The source is discovered from GET /api/settings/sources — one configured source is used silently, several makes the script stop and list them rather than deploy into the wrong store. FB_SOURCE overrides it, FB_OTP carries a two-factor code. Verified by deploying with it rather than by reading: 0.4.9f went up this way. This is that same file byte-identical, brought across to the main line — both branches carried the same broken script, so deploying 0.6.x would have failed identically. Also removes dist-test/, an untracked hand-made copy of a v0.6.2 dist/ build that no script or test references. build-web.ts hardcodes dist and wipes it on every run, so nothing in the repo could have produced that directory or would ever read it. .gitignore is deliberately unchanged: the answer for a directory that should not exist is to delete it, not to hide it. 715 tests pass, tsc clean, site builds. |
||
|
|
7804756f11 |
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. |