517238a7275d09752f7edbca1d315891e963828e
16
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b90c0413d2 |
v0.8.0.17 — four things the game knew and the screen did not say
All four reported from a table on Day 1 of v0.8.0.16, and all the same shape. ABS SIGNALS COULD ONLY BE PLAYED ON ONE MAINLINE CARD, while its tooltip said "any Mainline card". The engine was never wrong: check accepts any node whose kind is mainline and legalActions filters by check, so all of them were legal. The failure was the LABEL — describeIntent named i.placement and never i.node, so every placement described itself as plain "play ABS Signals", and the action list drops duplicate labels. All but the lowest-index node were discarded before the menu saw them. This is the THIRD time that trap has fired and the file documents the other two three lines apart: a turnout's two rotations, and three Department discards. Same fix — name what distinguishes them. The card is also called what the card face calls it. prettyKey rendered absSignals as "Abs Signals" beside a tooltip saying ABS, an acronym no key-splitter can recover, so the authored names now win. Three of those names were transcribed in sentence case and were CORRECTED rather than adopted: the repository says "Yard Office" 36 times against "Yard office" twice. A lookup that imports its own source's typos is the drift it exists to prevent. NOTHING ON A MAINLINE CARD SHOWED WHAT WAS STANDING ON IT. Played, ABS left no mark and you found out by hovering — the same complaint the Heavy Grade wedge answered, and it matters more here because ABS decides whether a second train on that card is safe. It draws a signal mast with a lit lamp now; a signal is the literal object and needs no room for words, which is what lets it sit clear of a name as long as "Uncontrolled Siding" on a 152px cell. The Mainline modifiers draw as BRK, AIR and HLP. Realignment is deliberately not among them: reduce takes the `became` branch and changes node.card, so a realigned Trestle IS an Uncontrolled Siding afterwards. Asserted, so the absence reads as a finding. A FREIGHT AGENT TURN SAID A CAR MOVED WHEN NONE HAD. Three faults behind one line. It asserted an outcome, where §6.3 requires no action and the bot declines deliberately — unjamming a healthy box destroys a load that cost a whole action to stock. An idle Agent was then silent, which read as a dropped turn; a new freightAgentIdled event says so and why, reducing to nothing exactly like switchingEnded. And the work named a coordinate rather than the industry, though a `place` helper has existed for precisely that since the switching lines moved to it. "Loaded a loaded boxcar INTO the green Outbound box at the Freight House", with the direction in capitals because to-or-from was the question asked. THE LOG AND THE ACTION MENU SPELLED THE SAME SQUARE DIFFERENTLY. view.ts wrote (col,row) — X,Y, east/west then north/south — with a comment saying why; narrate.ts wrote the internal storage order with no comment at all. So the menu offered a move to "(1,-1)" and the log reported it at "(-1,1)", side by side. Pinned by a test that renders one square through BOTH describers and compares them to each other: a test written against either file alone would have passed. THE DOCUMENTATION IS REACHABLE FROM A RUNNING GAME, AND ALL OF IT IS PUBLISHED. v0.8.0.16 published the Quickstart and nothing it points at — its §8 links five documents by relative path and every one 404'd on the package, verified against the running container. The build publishes the full set, and the test reads the links OUT OF the guide rather than listing them. They are linked from the This Game card, where reference already lives, rather than the header that must not wrap; no mode awareness is needed, because solitaire and multiplayer are the same page on the same origin. THE REFERENCES DROPPED THE VERSION FROM THEIR NAMES. Four described v0.8.0.16 and had since the v0.8.0.15 audit; the v0.4.5 was the prototype edition they were first written against, kept only because 36 citations pointed at it — and it read as documentation five minor versions stale. They are quickstart.md, rules.md, home-deck.md, mainline-deck.md and components.md now, kept current with each release rather than published as editions. Two errors surfaced while checking them against this release, which is the argument for doing it: home-deck.md filed ABS Signals under Enhancements "played into your district" that "change what a square does" — it does neither, this release's bug written down — and mainline-deck.md, which lists everything playable onto a Mainline card, never mentioned it at all. 1010 fast tests and 35 sim tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MUizFYCMHRWhbWwXhp7WPR |
||
|
|
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 #18 |
||
|
|
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. |
||
|
|
83a5450866 |
v0.6.1 — five of six playtest bugs: one button per train, and a load that has to go somewhere
Gameplay testing on 0.4.9d returned six reports. Five are fixed; the sixth could not be reproduced and is written up in TODO.md with the two questions that would pin it down. TWO TRAINS AT ONE PLATFORM ANSWERED TO ONE BUTTON. `porter.board` and `porter.detrain` carried no tray, so there was one button per platform however many trains stood at it and the reducer filled the first empty coach on the A/D tracks. `check` and the reducer were not even asking the same question: `check` skipped a train whose card refuses passenger work and the reducer did not. Both intents now carry an optional `trayId`, one function resolves the train and the coach for check/execute/reduce alike, `legal.ts` offers one candidate per train, and the label names it. A LOAD COULD BE MADE AND BROKEN WITHOUT GOING ANYWHERE. A Freight House could unload the boxcar it had just loaded; a platform could detrain the passengers it had just boarded. Full Revenue at both ends for a movement that never happened. Jesse's rule: a load made anywhere in an Office Area may not be broken anywhere in that Office Area, ever — it has to be carried to another district. The load carries the seat that made it (`RollingStock.origin`), stripped by `pooled` at every yard push. Measured at -0.60 +/- 0.10 Revenue a game (t = -6.1) over 400 paired deals: 78 worse, 3 better, 319 unchanged — free Revenue coming off the board, not a nerf. THE GROCER'S WAREHOUSE SHIPPED AND THE REFINERY RECEIVED. Both were `flow: 'both'` on the reading that "Freight House" was a collective term for exactly those two, and therefore what §9.3 described. The engine has dealt a Freight House CARD since before v0.4.9, so §9.3 names it and the argument goes. The card set agrees: all three Refinery modifiers grant +1 outbound. Refinery outbound-only, Grocer's inbound-only, Freight House the one two-way industry — which leaves exactly the one same-district pairing the rule above refuses. NOT REPRODUCED: cars left behind when backing up over them. Five layouts tried, including cars spotted at an industry; every one couples the lot. Three are pinned in `apply.test.ts`. One way to create such cars was closed anyway — `flyingSwitch` wrote its cut past `carsOn`. Both published replays that had gone dead were re-recorded; a rules change retires a save, and `harness.test.ts` is what catches it. The same change ships as v0.4.9e on the 0.4.9 line, branched from the v0.4.9d commit — the engine files these fixes touch are identical across the two lines, so the patch applied cleanly both ways. Also carries the two "Queued 2026-08-22, from playing on StartOS" TODO items that were staged before this work started (Games in Progress readability, and getting back into a game after losing a browser). They are notes, and items 9-12 below them are numbered against them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011nbvwWMef8CuEP6t5cgkTv |
||
|
|
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. |
||
|
|
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. |
||
|
|
9f3b92d08e |
v0.4.7 — the switching game: track order, the cut on your own card, and four rules
Eight play reports and one design that had been written up and not built. The
through-line is switching: what a card can hold, which end of a train a cut comes
off, which way a train meets cars standing on the line, and what the board and the
log say about all of it.
TRACK ORDER FOR STANDING CARS, AND THE CUT ON YOUR OWN CARD
Two reports turned out to be one root cause. `TrackCard.standing` claimed "in track
order (§A.3)" and had no defined orientation at all, while `CrewTray.consist` does
(nose first, relative to facing) — so every transfer between them was a conversion
nothing performed. §A.3 says what it should be: cars occupy the track "in the same
order they originally held, left-to-right". Left-to-right is west-to-east, and that
is now the defined orientation of `standing` and of an industry track through
`carsOn`. It is the board's orientation, not the train's, so it does not change when
a different train touches the card.
- Setting out is batch-invariant. Four cars at once, four singles and two pairs
parked three different orders, one of them physically impossible. Successive
cuts off the same end stack up towards the engine, so the insertion point is the
train's own place in the row.
- Approaching a cut from either end now mirrors. `couples` is built nearest-first
along the direction of travel and reverses onto the nose, so the farthest car met
ends up nose-most — which is what makes a run-around worth its Move.
- A train no longer drives through its own cut. The walk began at the neighbour of
the start square and never read the start card, so a crew could set cars out and
pull straight away from them. Coupling is mandatory (§A.4) and your own square is
no exception; the cut counts against the four-car limit. Setting out off the end
you are not leaving by still works.
`CrewTray.standingWest` records where a train stands among the cars on its card — a
train may set out off both ends on one square, so which side a cut is on is not
recoverable from the array alone.
On the board, the cut is drawn split at the train — west cars left, east cars right,
engine in the gap — and each car's tooltip says whether it stands ahead of or behind
the engine. The history says which end a cut came off, and a move's button separates
"takes your own boxcar back off this card" from cars found standing on the line.
Decided: taking your own cut back on the square you are standing on is UNDOING the
drop. It is exempt from trains 3/4's per-location freight budget, X13's "drop but not
pick up" and X22's "empties only", and it refunds the budget the drop spent.
Otherwise a legal-looking drop becomes silently one-way.
Measured, 200 paired seeds, developer bot: -0.55 revenue (t = -3.63), freight revenue
1.11 -> 0.56. That cost is the bot's, not the rule's — its trains run engine-first,
so at a stub industry it sets a car out between itself and the only way out, and the
correct play is §A.5's facing-point move, which is the cross-turn planning TODO.md
already records as out of reach of any bot. Filtering self-recoupling moves out of its
options took recoupling from 625 of 1,029 set-outs to 101 of 677, and all 101 that
remain are that case. Read the number as a bot measurement, not a balance one.
THE SUPERINTENDENT'S RULING NAMES THE TRAINS IT IS ABOUT
Reported: the Superintendent could not tell which train he was clearing. The heading
asks the question now — "may Train 6 follow Train 4 onto the same Mainline card?" —
and the trains moved to the FRONT of each button, because the button splits its label
at the first em-dash and showed only the head.
AN INDUSTRY TRACK HOLDS FOUR CARS, LIKE EVERY OTHER CARD
Reported at undo 188: "we wanted to drop two cars, but were only allowed to drop one."
An industry track was built as long as its box count, so a one-box industry had room
for one car. Box count is how much WORK an industry can hold, not how much RAIL it
has. Ordinary track was the other exception, unbounded; both are gone and every card
holds four.
THE FREIGHT AGENT MAY STAGE A LOAD BEFORE THE CAR IS THERE
§6.3 asks nothing of the industry track — the empty car belongs to §9.3's Load the
car, which is the Laborer's action. The gate now lives only there, so cargo can wait
on the dock while the car to ship it in is still being switched in. Nothing can jam:
a load in a green box is waiting, not stuck.
THE TRUCK DOCK UNLOADS, AND BRINGS NOBODY
+1 inbound, no Laborer. It printed +1 outbound and +1 Laborer, which made it a
longer-host-list copy of Forklifts. Beside Packing Sheds it now does nothing at all,
and the hand tooltip says so before it is played.
Also in this release, from the days before: Mainline card tooltips computed from the
crossing rule; an Extra starts from the Division Point its number sends it to; a
modifier's suppressed grant comes back when a Whistle Post is upgraded; the Oil
Refinery and the Grocer's Warehouse ship as well as receive, per the card reference;
and the dormant defences name the attack they answer. `.claude/` is now gitignored —
it holds Claude Code's worktrees, i.e. a second checkout of this repository.
570 tests, typecheck clean. The three published replays were re-recorded twice —
legality changed, so bot play changed. Full detail in CHANGELOG.md.
|
||
|
|
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 | ||
|
|
216006b091 | v0.3.1 — multiplayer groundwork: 13 engine tests covering 2-4 player games, a seat parameter on snapshot and actionMenu so a player sees their own railroad rather than seat 0's, and the 22 opponent-directed cards held out of every deck until they are built | ||
|
|
1bf1e95058 | v0.3.0 — playtest fixes, two new rules, and §7 enforced | ||
|
|
9c76b1a5e2 | Track becomes a deck card, Departments become decks, and trains must be made up to leave, industry restrictions enforced, update display and add a new game button | ||
|
|
821d3cdffa | adding classification and division yard updates, train collisions, engine and car updates, turnout improvements. | ||
|
|
d261ad7af8 | Fix parking trains, freight deadlock, and Whistle Post lock-in | ||
|
|
b085d5a0bb | Implement Enhancements, Mainline modifiers and Maneuvers; fix unplayable track | ||
|
|
401b879140 | Expand game engine, replay, tests, and documentation | ||
|
|
e7bc07df85 | Initial commit |