Commit Graph
3 Commits
Author SHA1 Message Date
Jesse.Markowitz 40f07b0710 v0.6.0 — saves survive a release, Employee Rotation is real, and the lobby
asks what game you want

Three queued items. The last matters most.

A RELEASE NO LONGER DESTROYS EVERY GAME IN PROGRESS.

Four consecutive releases killed every game on the box, one of them a
release that changed only how the board is drawn. The reasoning behind the
refusal was always right — a move legal under old rules may not be legal
under new ones, and half-replaying a save is worse than refusing it. The
TEST was wrong: it compared engineVersion for exact equality, and that
stamp is the package version, which moves for a CSS fix.

Whether a save still replays has an exact answer, so it is now asked
directly. loadGame reads the file and judges nothing; tryResumeSession
replays the intents and reports the first one the engine refuses. A save
stamped with a version this server has never run resumes fine provided its
moves replay — verified against a file hand-stamped 0.4.9-ancient. One that
genuinely does not replay is still refused, but the log names the move
rather than two version strings: "move 3 of 8 (localOps.choose) is rejected
by the current rules with OPTION_ALREADY_CHOSEN".

fromMultiplayerSave had to stop lying first. It has always stopped at the
first unacceptable intent and done so in silence, which was survivable only
because the version gate meant a doomed replay was never attempted. Now
that the replay IS the check, it returns where it stopped and why.

Deliberately not done: resuming a partly-replayable game at its last good
move. That silently rewinds a game to a position nobody played to while
every browser holding a later Frame carries on unaware. Refusing leaves the
file intact, so putting the previous version back still recovers it.

EMPLOYEE ROTATION IS IMPLEMENTED, SISTER TRAINS IS DELETED.

Two of the four optional-rule flags were read by nothing at all. Employee
Rotation is four lines in advance.ts, because the seat/player split (D9)
exists for precisely this rule: seating is the only thing that moves, so
Revenue, hands, the Superintendent and whose turn it is travel with the
player, and the Office, district, grid and any trains standing in it stay
with the chair. Inheriting the district you move into is the point of the
rule, not a side effect. "Left" is seat + 1, matching playerLeftOf.

Sister Trains is deleted rather than built: Q9 records that the Second
Section card supersedes it, and that card exists, so the flag was a toggle
for a rule the game no longer has.

THE LOBBY ASKS WHAT GAME YOU WANT TO PLAY.

Creating a game asked for a name, a mode and a table size; every other dial
was hardcoded. A Game settings block now carries the same set the solitaire
dialog does — seed, starting hand, the three revenue rates, Days, the
combined-Revenue floor, both collision caps, the opponent-card toggle —
plus the three surviving optional rules. Mode and table size set the
defaults and everything stays editable. The seed is honoured, so a game can
be reproduced or compared.

Verified: 682 tests pass (679 + 3). The rotation tests were mutation-checked
both ways — disabling the rotation and turning the table the wrong way each
fail the suite. Live: a save stamped 0.4.9-ancient resumed, an injected
illegal move was refused by name, and a create with every dial set to a
non-default value came back out of game.json with all of them intact,
including seed 777.

Two of my own assertions were wrong on the way and the tests caught them:
the Fedora legitimately passes at Stage 12 (§5) so it cannot be compared
against its own earlier value, and dispatchUsedToday is cleared at every
Day boundary so it cannot mark a district.
2026-08-21 21:45:34 -04:00
Jesse 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.
2026-08-21 05:25:47 -04:00
Jesse 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.
2026-08-20 23:50:38 -04:00