02289e94b81d0c5ae05e2634737a71e7b2d7ccd7
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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_01X6cF1iYvJ1kNmzYBzu4QX6 |
||
|
|
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 |
||
|
|
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_01EAgJSmeV8zrMh55Mj85ESb |
||
|
|
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_016JczK5i33ZNSf2PtzZqdhS |
||
|
|
bfd2708ecc |
v0.5.5 — a remembered session for a game that no longer exists
Reported after updating to v0.5.4: clicking Multiplayer went straight into a game with no lobby and no controls, and the board was blank. Three things lined up. start() enters a remembered session WITHOUT checking it still exists — that is what makes reconnection seamless, and it is why the lobby was skipped. The v0.5.4 update had refused to resume that game, its save being recorded under v0.5.3 and the engine-version check being exact (D7). And createRemoteSession had no onerror at all, so EventSource retried the resulting 404 forever in silence while frame stayed null and nothing rendered. The only escape was clearing site data, and nothing on screen said so. v0.5.3's Manage Game -> End had just widened the same dead end: it closes every watcher's stream, so a player whose game an administrator ended would sit frozen on a stale board indefinitely, for exactly the same reason. GET /api/session?token= is new: a cheap yes/no on whether a token still names a live game. EventSource fires error identically for a transient blip — the expected shape of a game idle for minutes (§9) — and for a 404 it will retry forever, and exposes no status code either way, so the client asks rather than guessing. Only a definite 404 closes the stream and reports the game gone; a flaky network still self-heals. The page then forgets the stored session, says why (ended by an administrator, or the service was updated, which does not carry games across), and drops into the lobby. Forgetting the token is what stops the next load repeating it. It also stops rendering nothing while it waits — "… connecting to the game" sits in the presence banner until the first push arrives, because a page showing nothing is indistinguishable from a broken one, which is what this looked like. Recorded but NOT fixed, in TODO.md: three releases in a row destroyed every game in progress, and v0.5.4's changes were rendering only. The refusal is right, but the test is exact equality against the PACKAGE version, which moves for reasons unrelated to the rules. Three options costed; the recommendation is to replay the save and refuse only if an intent actually rejects — the real question rather than a proxy for it, and a full replay measures ~100 ms. Verified live: /api/session answers 200 for a seated token, 404 once an administrator ends the game, 404 for a garbage token, and /api/stream 404s in the same state — which is the response EventSource had been retrying silently. 673 tests pass. |
||
|
|
e76bd77099 |
v0.5.1 — multiplayer Phase 4: lobby, sessions, reconnection
A real server existed since v0.5.0 but nobody could reach it without a hand-built ?seat=&secret= URL. This is what makes it a game you can actually create or join. The server now hosts more than one game: src/server/lobby.ts (new) is pure logic — creating, joining, bot seats, host transfer, starting — same split session.ts already draws for a running game. persistence.ts gained one directory per gameId plus a top-level index so index.ts resumes every saved game on boot. /api/stream and /api/intent now authenticate by session token instead of ?seat=&secret= — the token alone proves identity (lobby-and-sessions.md §1), so the join secret's job ends at the lobby door. Bots fill empty seats at Lobby.Start only, never take over a disconnected human (D8): session.ts gained driveBots(), playing developerBot forward through consecutive bot seats after every accepted intent. Disconnect keeps the seat and says so — Push gained an optional presence field, built entirely by http.ts and never routed through the engine, since a disconnect is transport news, not a GameEvent. Host rights pass to the earliest-joined remaining player if the host drops before start. Client: src/web/lobby.ts adds create/join forms and a live seating screen; localStorage replaces ?seat= for reconnecting straight back into a game already joined. A Multiplayer button sits beside New game; the New Game dialog itself is untouched. Found only by the live smoke test, not by typechecking: /api/intent read its token from the JSON body while the client sends it in the query string (matching /api/stream) — every intent failed "no such game" until caught by curl-level verification. Doc fix: multiplayer.md's D18 said the player cap was 6; lobby-and-sessions.md §2 says 2-4 with the reasoning and the test coverage to back it. The two had drifted apart. D18 now reads 2-4. Not verified: an actual browser walking through the lobby screens — none available in this environment, same limitation Phase 2's RemoteSession shipped under. 656 tests, 0 failures. tools/jitsi-harness/ deliberately left untracked — unrelated side-project work, not part of this release. |
||
|
|
c3c5cbfeec |
v0.5.0 — multiplayer Phases 2 and 3: a server that runs a game and survives being restarted
Phases 0-1 shipped in v0.4.0 (seat/identity split, per-player turn state, the Session boundary). This lands Phase 2 (server core, one game, no lobby) and Phase 3 (persistence and resumption) per docs/architecture/multiplayer.md §12. Phases 4-6 (lobby/reconnection, the 22 opponent-directed cards, StartOS packaging) are still ahead. Phase 2: src/server/session.ts hosts a game in pure logic (no sockets) on top of game.ts's existing Game/submit/currentActor/actionMenu; it verifies seat === currentActor(game) itself before calling submit, since submit() trusts its caller and a server can't. src/server/http.ts and index.ts add POST /api/game, GET /api/stream (SSE, per-seat), POST /api/intent, and static serving of dist/. src/sim/frame-delta.ts is a purpose-built per-seat board delta for one live push at a time. Found and fixed along the way: actionMenu(game, seat) only used seat for the hand field, so a server computing every connected seat's Menu would have handed the acting player's legal moves to a waiting seat. Verified with a live end-to-end smoke test (2-player game, two SSE streams, a rejected intent from the wrong seat, an idempotent resend) plus test/server/session.test.ts and test/redaction.test.ts. Not verified: an actual browser (none available in this environment). Phase 3: src/server/persistence.ts writes game.json and turn-timings.json, atomic-rewrite-then- rename. game.ts gained fromMultiplayerSave, fixing a narration-attribution bug found while testing it (fromSave's replay loop drops the actor argument, invisible in solitaire, unreadable the moment there's more than one seat — fromSave itself still has this gap, deliberately untouched). Verified live: server killed and restarted mid-game, both seats reconnected exactly where they left off. Two rules bugs found while building this: the New Train phase never implemented its car-placement round (every car of every train was placed by the Superintendent alone, in every mode, all along — now reads the round position off tray.consist.length); and victory conditions are now one shared, configurable GameConfig set across solitaire/competitive/coop instead of a fixed length lookup and a dead firstToTarget condition. Also folds in the three fixes already released on the patch line as v0.4.9b/c/d: a switching train's crew badge failing to draw once it left the Office square, an unload that always took the westmost car regardless of which was picked, and a legal decision that could render with zero buttons. docs/testing/0.5.0-test-plan.md and three reported-bug save files (docs/station-master-seed*.json) included for reproducibility. tools/jitsi-harness/ deliberately left untracked — unrelated side-project work, not part of this release. 635 tests, 0 failures. |
||
|
|
6655e20ea8 | v0.4.4 — New Game dialog for the opening hand and the three revenue rates, Yard renamed Interchange, and two movement bugs behind a mirrored consist | ||
|
|
393a669418 | v0.4.2 fixed passenger trains not stopping at office. changeed revenue for train completing run - 1 point to every player when train exits division | ||
|
|
49f8504b05 | v0.4.0 — multiplayer Phases 0 and 1: seat and player split apart, turn state per player, the page behind a Session, and eight seat/player mix-ups fixed with tests that fail without them |