def48201e49ad24bfbe142d79aec2724ed41429c
24
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
04ca74c365 |
v0.8.5 — housekeeping from the audit, and the playtest line retired
The third release from the audit; nothing a player sees changes. CHANGELOG has the detail. The 0.4.9 playtest line is no longer maintained (Jesse, 2026-09-29): the deploy rule that existed for it is gone and #85 is moot. The table test (#39 #35 #42a #40) is closed — every line of the checklist was met at a table. #46 is done and cannot regrow: the 36 unused declarations are removed and `noUnusedLocals`/`noUnusedParameters` are on; two of them were dead bot functions from rejected candidates the round said it had deleted. The documents no longer teach `trainCapSlack` (a knob that throws), point at `as-built.md` (deleted in 0.8.2), model `officeType` (the engine says `tier`) or describe `collisionOccurred` (never emitted); the README's account of bot flags now matches the bot's. Five playtest saves committed in `docs/` against the repository's own rule are in the ignored `playtests/`. What the audit found and did not fix is written down as TODO #112-#117, each with its reason. #112 is `docs/plans/structure.md`, the proposal for `http.ts`, `main.ts` and `check`. #117 — `/api/save` hands a seat the seed mid-game — waits on a conversation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FrCWubm9GAftYCm2hWdKwK |
||
|
|
4d222a7eba |
v0.8.3 — the engine half of the audit, and the deal 0.8.2 silently changed
Seven rules faults and one dealing fault, from a four-way code audit (engine, server, client, tests) read against the code before anything was acted on. Each is pinned by a test that failed first. CHANGELOG has the reasoning; this is the list. THE DEAL. 0.8.2 put the Second Section card into the deck after its save check had run and without a line in its notes. A deck one card larger shuffles differently from the same seed, so every save on the test server refused at move 3 — the boot log shows thirteen of thirteen — while the release notes said three would resume. `withSavedDeal` (was `withSavedOpening`) now sets `secondSectionCard: false` for a config that predates the setting, and the thirteen replay exactly as 0.8.2 described: three resume, ten refuse, the same ten at the same moves. THE RULES. `check` never tested that a switching tray was in the actor's own district, so a rival's train could be shunted and the rival charged the Moves. Occupancy matched on coordinates alone, so a rival's crew blocked your track. A Department draw that emptied the deck duplicated the drawn card and destroyed the refill card. The unjam cleared the first load rather than the one named. The collision floor could not fire in Stage 12. The Expedite fault was charged once per clearance question rather than once per phase. A train held at the Limits was only ever released by another arrival, never by a departure. Docs: rules.md describes each as built (and no longer says an Expedited train departs at Shift Change — that was v0.4.8's reading, corrected in v0.4.9's code and never in the document); game-state.md's collision-floor note now matches the code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FrCWubm9GAftYCm2hWdKwK |
||
|
|
3befc420da |
v0.8.2 — every district opens on a Depot, and the docs are pages now
A second-digit bump for a playtest read back against the save file. Nine questions were asked of one three-Day game; three were bugs, three were the rules working and undocumented, three were decisions. Every save on the test server was replayed against this build BEFORE release, which is how the cost of each rule was known before it was chosen rather than discovered after. EVERY DISTRICT OPENS ON A DEPOT. A Whistle Post has one A/D track and is not a Passenger Facility, so the opening of every game was spent unable to work a passenger and one arrival away from a collision. Two A/D tracks and passengers from Stage 1 now; "Players start with Whistle Posts, not Depots" is the harder game, set when the game is created. The deck follows the choice — starting on Depots the four Depot upgrade cards are left out, because an upgrade must be to the next tier and a Depot card at a table of Depots is a dead draw. How much easier it is showed up as a test failure rather than an argument: the cue-coverage pool needed widening from 24 seeded games to 60 before it held one collision. NO SAVE WAS STRANDED BY IT, which took care. This is the one house rule that changes how a game is DEALT rather than how it plays, so replaying a save under the wrong opening is a different railroad from intent one — silently, with no error. `withSavedOpening` fills it on the replay paths ONLY. Putting it in the resolver instead made a fresh Cutthroat game deal Whistle Posts and read as Custom, which is how the distinction was found. THREE BUGS, ALL REPORTED FROM ONE GAME AND ALL CONFIRMED ON ITS SAVE. An Office held TWO TRAINS ON ONE A/D TRACK. The capacity test passed with nothing standing, the train the Interlocking had been holding at the Limits was moved into the free slot, and the arriving train was pushed in after it without anyone asking again whether there was room — so the collision §8.3 calls for never happened. The held train keeps priority; the newcomer now takes the consequence it would have met had the held train arrived first. THE HISTORY FROZE, permanently, and the log cap was not really the cause. Each seat's "what have I sent you" bookmark was an INDEX into an array the game trims, so once a seat's bookmark reached the limit the slice returned nothing for the rest of the game — at a different moment per seat, because each holds its own. That game's log ended at exactly the cap. Lines carry a sequence number now, which survives trimming; proven by pushing twice the cap through a simulated seat. §8.1 ASKED THE WRONG QUESTION TWICE. "Trains may pass" returned `clear` before the Subdivision was looked at, so a train entering a Double Track was released however busy the rest of it was — that, not anything about Control Points, is what let Train 8 out with no ruling. And a train standing at an Office was invisible to the scan, so one about to re-enter the very Subdivision being entered counted for nothing. Capacity is the test, not presence: a Depot with a track free is not in the way; a Whistle Post with its one track taken is. THINGS THAT HAPPENED SILENTLY NOW SAY SO — a train held against a facing one, a train released from the Limits (a side effect of somebody else's arrival, so it simply appeared at the Office), and the train an Interlocking is holding, whose explanatory tooltip has existed since #99 with NO renderer ever reading the flag. WHERE A MOVE IS REFUSED, AND WHY. `exploreMoves` decides where the rails go and the pick-up restrictions are enforced afterwards in `check`, so a square the rails reached and the card forbade was reachable, un-offered, and absent from the block list with nothing said. Those squares are blocked with the rule that blocks them now, and the reasons are got by ASKING `check` rather than re-deriving: a second implementation of the rules is exactly the failure the block list exists to avoid. A train may also always recover its own caboose — X13 prints "may drop but not pick up anything", and a train needs its caboose to be made up, so one that parted with it could never legally leave again. RULES DECIDED IN SEPTEMBER AND APPLIED HERE. A Modifier must sit square against its host, no diagonals. A passenger Modifier may not be played at a Whistle Post. Both were built, measured, held back for a fortnight so a playtest could finish, and applied now. A Second Section costs its card: `SECOND_SECTION` was declared in content.ts and never dealt, so the action was free and the bot ordered 26 accidental ones in a measured round. The card is dealt and spent — gating on a card the deck never holds would have deleted the mechanic rather than fixed it. THE DOCUMENTATION IS A SET OF PAGES, not five text files served as text/plain — a card reference is mostly tables, and as plain text a table is rows of pipes. Markdown is still the one copy; the build renders it, and publishes the .md beside each page. No Markdown library: this project has no runtime dependencies and one would be a poor first. The pages add what Markdown cannot carry without drifting — a nav across the set, a contents list built from the headings actually rendered, an anchor on every heading, a 70-character measure, and tables that are tables. They print as ink on paper. The references caught up with the rules, checked rather than assumed: two statements had gone from stale to misleading (the Quickstart told a new player to "get a Depot down as soon as one appears"), and four rules nobody could look up are written down — the Office tier table, §8.1 in practice, what the Circus Train pays for, and that a Realignment can be a card with no legal target. Adding one card to the deck reshuffles every seeded deal, which broke five fixtures. Each was a seed meaning "a game like this" — TODO #84, exactly — so seeds moved and pools widened rather than assertions weakening, and the clearance fixture pins its terrain the way `enhancements.test.ts` already does. The three published replays were re-recorded. Closes TODO #40, #42a, #108, #109 and #110. 1046 fast tests and 35 sim tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MUizFYCMHRWhbWwXhp7WPR |
||
|
|
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 |
||
|
|
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_01X6cF1iYvJ1kNmzYBzu4QX6 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
37b1e5b969 |
the Roster Pass: every train at the Office visible
Builds "The Roster Pass" / "Two Trains, One Card" (station display for multiple trains at one Office). The Office is the one square where more than one train may legally stand at once (one per A/D track), and CellView.train had room for exactly one — a second train at a busy Station was counted in the old A/D pips and never drawn. CellView.train -> CellView.trains: TrainView[], seat-filtered and collecting every match rather than the first (fixes a latent cross-district leak in the process: trainOnCard never checked seat). The A/D pips are replaced with one roster chip per A/D track, always, free or occupied; clicking a chip sets selectedCrew, wiring the board and the action panel to the same value. standingWest moves from CrewTray to TrackCard: two trays sharing one Office card need one shared split, not one each, and there is no such thing as "west of one particular A/D track". No save migration — Save replays through the engine — and a stale value on an emptied card is inert because nothing reads a split with no train standing there. The Division map's Office cell is now sized by A/D capacity rather than occupancy, so it holds still as trains arrive and leave; chips lay into fixed slots instead of centre-spreading onto the Limits cards either side. 584 tests, 0 failures. Verified end-to-end against the built app and a direct render of a 4-train Terminal (screenshotted). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SAt2YCXgd5qCjBcF2x34aK |
||
|
|
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 | ||
|
|
a7221dcf20 | v0.4.1 - bug fixes. initial d12 rolls determine what player in which seat. | ||
|
|
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 | ||
|
|
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 |