4d222a7eba6efb82a9bbf74c9720f4afbd01a7d4
33
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
9a9e50b3c6 |
v0.8.0.11 — sixteen fixes from the second multiplayer playtest
Arrivals name whose Office they reached, and no longer tell every seat they can work the train. The turn chart follows the animation queue, so being five behind looks five behind across the whole screen rather than half of it. Pause sits beside Skip and preserves the dwell a held step still owed. A one-render look at another player's Office Area. The district summary counts the board being shown. LIMITS is printed beneath its card instead of through its border. The Mainline region divider is visible. Only Hilly mentions FAST/SLOW, because it is the only card that reads it. A passenger Modifier on a Whistle Post reports itself dormant rather than claiming the facility "only receives". An automatic phase says what the Division is doing instead of answering by negation. The version appears once in the header rather than twice on every .s9pk. Save files carry the join code, the Stage and the date. The New Train phase, reviewed before being changed: the make-up panel now says what the train STILL needs rather than only what its card calls for, explains that a player adds one car before the round passes on, marks the train being loaded on the Division map, and gives an addable car in the yard the same amber every other clickable thing on the page wears. Reasoning, measurements and the reports behind each are in CHANGELOG.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nnuCv8UodHucFfx3LWEoX |
||
|
|
4adf149ba5 |
v0.8.0.10 — playtest fixes: clearance rulings, the log, the map, and a save file
From the first two multiplayer playtests of v0.8.0.9, each traced before fixing. The engine: - A train on a card BEHIND the one departing no longer triggers a clearance ruling or an opposite-direction bar (#26). Reproduced from the exported save: X15 was held over X18 behind it, and X18 then collided into the full Whistle Post. Games in progress holding a ruling the engine no longer asks for will not resume (28 of 40 recorded four-seat games); shipped as is at Jesse's call. - `mainlineModified` carries the card's previous kind, so the log can say what a Realignment converted (#27). The screen: - The turn chart and the Division map name the player whose move is on screen while bot turns replay, not the live actor (#25). - The owning player's name is no longer outlined by the turn arrow's stroke, which made it unreadable (#24). - A Mainline card flashes on the map when a Realignment changes it (#28). - The history is held back with the board and revealed step by step, instead of arriving whole while the board is still catching up (#29). - A ruling made by holding the office reads "Superintendent Player X" (#30), and no line names a player twice (#31). - A seated player can download their own game as a save file: the play page's Save replay button, fed by GET /api/save?token=… (#32). The StartOS action cannot do this — an action result is text only. Closes #24 Closes #25 Closes #26 Closes #27 Closes #28 Closes #29 Closes #30 Closes #31 Closes #32 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nnuCv8UodHucFfx3LWEoX |
||
|
|
88a42ae9e2 |
v0.7.9.7 — a device that said it was available all day after it was gone
The last item off 0.7.9.6's sweep, parked there as the one genuine maybe. It had a second half worth more than the first. #101 — Telegraph (+4), Telephone (+8) and Radio (+12) are "once a day, when dispatching facing trains, add +N to the other train's number". `enhancementText(key)` takes only the KEY, so the tooltip could not vary with anything: a spent Radio read "Once a day, add +12..." for the rest of the Day, advertising a bonus that was not there. That is `trainRules` before #100, in another corner of the same view. THE HALF THAT ACTUALLY SURPRISES. `spendDispatchBonus` reads `areaOf(s, s.clock.superintendent)` — the SUPERINTENDENT's own devices, not the train owner's — and the Fedora moves every STAGES_PER_SHIFT (3) Stages, four times a Day. So a player's Radio does nothing at all for three-quarters of the Day, and is spent automatically, without its owner being asked, during the quarter it is theirs. Neither half was anywhere on the board. The card now reads as one of three states — available and dispatching, unspent but idle while somebody else holds the Fedora, or spent until the next Day — and names the shift length, because "not now" without "for how long" is half an answer. A spent device is struck through on the board. Shown on EVERY district, not only the viewer's (Jesse's call): it is public, and a rival's spent Radio is what you want to know before forcing a meet. What counts as a device is `enhancementRule(key)?.dispatchBonus` rather than three keys written out in the view — the ladder lives in ENHANCEMENT_RULES and a fourth rung would otherwise be silently exempt. A STALE COMMENT CORRECTED, AND PINNED. `advance.ts` warned that indexing a SEAT-keyed area with the PLAYER holding the Fedora "is right only while seating is the identity map". It read as a live Employee Rotation bug and was not one: `areaOf(s, p)` IS `areaAtSeat(s, seatOf(s, p))`. A comment that sends the next reader chasing a bug that does not exist costs about what the bug would. Rewritten, and the claim is now a test — seating set to a real permutation, and the Superintendent's own district rather than the seat with the same index is the one that reads as dispatching. TWO THINGS MUTATION CAUGHT THAT PASSING DID NOT. A test asserted the ABSENCE of /spent|Fedora/ with the Fedora held, and a mutant with the `dispatchBonus` guard deleted PASSED it — the leaked text in that case says "Available today, and this district is dispatching", which contains neither word. A test that something was left alone has to compare it against what it should be, so it asserts equality with `enhancementText` now, in both Fedora states. And the replay wire format needed the field: cells pack positionally, so the flag is index 9 and reads `?? []`, the same tolerance `standingWest` uses — older recordings report no device spent, which is what they drew at the time, so every published replay is unchanged. 943 tests pass, up from 934. NOT VERIFIED AT A TABLE, like 0.7.9.6. #39 and #35 still stand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y5boPxP6JHRYMm8adXaF5R |
||
|
|
ebd16983e2 |
v0.7.9.4 — Gitea#20 step 1, and a Red Flag you can see
Step 1 of the common board done as its own release rather than as the first hour of 0.8.0, since both halves of it are worth having whether or not anything is ever published to a call. #95 — the public projection helpers. `projectDistrict(state, seat)`, `projectDivision(state)`, `projectSharedTable(state)`, `publicSnapshot(state)` and `currentActorOfState(state)`, with `snapshot()` REBUILT to compose from the same helpers rather than keeping a second copy of the shared table, so a player's frame and a spectator's cannot come to disagree about the clock, the phase, whose turn it is or the score. Behaviour-neutral; the 897 existing tests passing unchanged is the proof. The public view is composed UPWARD, never by calling `snapshot()` once per seat. That shortcut is the trap the plan names: `snapshot` assembles one player's view, so a public view made of player views builds every private field and then has to remember to strip it — and it defaults its viewer to player zero, so a careless spectator call would have served seat 0's hand. Districts are keyed by SEAT with the player resolved through `playerAtSeat`, because Employee Rotation moves players between districts and a board that treated seat and player index as interchangeable would relabel every district the first time anybody rotated. One plan finding is struck off rather than fixed: it warns a display reading `clock.currentActor` could highlight the wrong district during a decision. Measured over six seeds and 3,600 decision points, that field and `actingPlayer` never disagreed. `currentActorOfState` exists anyway, as one place for the next reader to ask. #91 — the redaction net, systematically. v0.7.9.2's two leaks were found by reading a plan, not by a test, which is the whole argument for this: a suite made of the leaks somebody happened to notice proves nothing about the next one. Serialise a seat's Frame, the PublicFrame a spectator gets and the narration they receive, then search all three for every opponent card id, every card name unique to one opponent's hand, the seed and any private decision or menu data — across a fresh game, a blind draw, mid-game, a pending decision, Employee Rotation before and after the seating moves, a reconnect push (a full Frame, and its own opportunity to leak) and a played-out game. And the allow-list, which is the plan's stated acceptance bar rather than the tests: every property of `publicSnapshot` is written down with its reason and compared on every run, so adding a field fails the suite until somebody has said out loud that a spectator may see it. Both v0.7.9.2 leaks were fields nobody had ever asked that question about. Proved by mutation rather than by passing: restoring the seed line fails 6 tests, restoring the blind-draw card name fails 1, adding a private field to the public projection fails 7, and making `players[]` carry hand contents instead of a count fails 5. Two false failures were worth the lesson. A card NAME is a type, not an identity — "right-hand curve" names a dozen cards and one is legitimately a cell label the moment anybody lays track, so searching for it fails on correct code, which is worse than not searching; a name is evidence only when every card bearing it is in the one hand. And a one-digit seed makes the seed check meaningless: seed 7 matched "Train 7". One item on the plan's list has no test because it has no referent — there is no secret objective in this game, `objectiveOf` deriving from `config.minCombinedRevenue` and the player's own Revenue, both public. #94 — a Red Flag standing at an Office's Limits is on the map. It is a token set out ON the board that holds the next train arriving from that side, and it was announced once in the log and drawn nowhere, so a train stops short three Stages later with its only explanation scrolled out of the panel. `DivisionView`'s office node carries `redFlag` and the map draws a staff and pennant AT THE END IT GUARDS — west on the left, east on the right — because which approach it covers is the whole of the information; a flag in the middle of the cell would say one is out and leave the reader to hover for the half that decides whether to run a train. The tooltip leads with it, ahead of everything that merely describes the cell. The third of these in a row after Gitea#21 and #22: when the engine gains something that changes what a train may do, the question to ask is where it is drawn, not whether it works. 909 tests pass, up from 897. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ss2y7FyhxkHjGj7xnUPCgY |
||
|
|
7ade60e21f |
v0.7.9.1 — the engine was right twice; the screen was not
Gitea#22: the Division map drew every westbound train in the wrong half of its Mainline card. `regionOfTransit` counts from the end a train entered, which is what the collision rules ask; the map wanted "which printed box, left to right" and used the same number, so an eastbound train came out right by luck and a westbound one came out mirrored. It cost a collision — Train 3 was cleared to follow T5 and ran into TX17, which the picture had drawn ahead of T5 rather than behind it. One mirror in `view.ts`, at the boundary the map is drawn from; the collision rules are untouched. Gitea#21: a second tank car would not come off at a refinery, and "Blocked — why nothing is moving" answered by describing the refinery's green box. The real answer was Train 3's printed rule — the Express works one freight car per location — so the refusal was correct and the panel sent the player to spend a Freight Agent action that could not have helped. No rule changed. The panel now names the budget, asking the reducer's own predicate so its words cannot drift from the rule. Both were replayed from the saves attached to the issues and verified in the exact position each report names. The map fix is proved by mutation: reverting the mirror fails two tests, and making the renderer ignore the region fails a third. Every existing region test ran eastbound, where the mirror is the identity, which is why the bug survived them. 891 tests pass, up from 884. Closes #21 Closes #22 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3Qk7uresKCHksdZajXCLg |
||
|
|
31b942cc38 |
TODO.md had stopped tracking its own closures
Nothing was wrong with the ordering. What had gone wrong is that eight
items were done or superseded in one place and still open in another,
which reads as live work — worse than no entry at all. Found by
reviewing the file against the Gitea tracker and the git log, not by
reading it.
NEXT WAS 43 CHRONOLOGICAL ENTRIES, 26 OF THEM STRUCK THROUGH, and the
live work had stopped being visible in it. It is grouped by what each
item is WAITING ON now — blocked on a table, the screen, waiting on
something outside the code, unscheduled, standing practice — with the
closed entries moved to a Settled block at the end of the section.
EVERY NUMBER IS KEPT AS A STABLE ID. The rest of the file refers to
items by number ("decide with item 15", "same drawing pass as 19-21"),
so renumbering would have silently broken every cross-reference.
Numbers are ids, not positions, and the file now says so. The one
reference that pointed at a settled item (item 22, the dead centre of
the Division map) is rewritten to name the principle that outlived it
rather than the item.
EIGHT STALE ITEMS CLOSED. The Heavy Grade orientation was fixed in
|
||
|
|
cf018b4a5f |
A Heavy Grade shows which way it climbs
Jesse: "heavy grade mainline card tooltip states climbs east, but card doesn't show it." The Frame has carried gradeUp all along and the tip has said it; the card drew nothing, so the one Mainline card whose orientation is set per game was the one you had to hover to read. A brown wedge in the lower right rising toward the climb, with a bone arrow lying along its slope, centred on the triangle's centroid. East is right on this map (Gitea#18), which is what lets a wedge be read without a compass. Four passes. Up the hypotenuse the arrow began at the wedge's thin corner, where there is no height, so its head read as clipped. Level, it was contained but did not read as climbing. At 30° — steeper than the wedge's own 22.5° — it had to be tucked into the fat half. Parallel to the slope is the shape that fits: the gap to the hypotenuse is then constant, so the arrow can sit centred. The wedge grew to 58x24 to pay for it, since a centred arrow has less room than an off-centre one. Sizes are a search result, clearing every edge by 2.88px. The first containment test bounded the arrow against the CARD, which it never left, while the wedge clipped it — a green check on a visibly broken glyph. It checks the TRIANGLE now with a 2px floor, plus parallelism derived from the wedge and centroid placement. Orientation is always set, measured not assumed: 400 seeds x 4 player counts, 535 grades placed, 0 without one, 276 east / 259 west. Stays in the unshipped v0.7.9. 874 tests pass, one new. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX |
||
|
|
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. |
||
|
|
51710498f5 |
v0.5.6 — seats counted from 1, a name you can read, and a line that stops repeating
Three things off the first proper look at a live table.
The lobby listed chairs as Seat 0 to Seat 3. Zero-based is right inside —
it indexes seating, the seats array and every route, and none of that
changes — but nobody sitting at a table calls their chair "seat 0". There
were four of these rather than one: the lobby list, the topline's Seat N
for a remote session, the presence banner's fallback name, and the admin
summary's. All go through a single seatLabel now, and a test fails the
build if any "Seat ${...}" interpolates a raw seat again, since the
conversion has to happen in exactly one place or the two conventions drift
apart. Verified by mutation — putting the raw seat back fails the suite.
seatLabel lives in sim/view.ts, not web/game.ts. Putting it in game.ts was
the first attempt and test/session.test.ts caught it: the page may not
import values from that module, because they are the local engine by
another name and importing one reopens the Phase 1 boundary. The test was
right and the placement was wrong.
.bs-name.bs-turn carried font-weight:700 over a base of 600. At 11px a
monospace face has to be synthesised the rest of the way and the extra ink
lands as blur, so the one name you most need to read was the one you could
not. The bump is gone; amber against #e6e9ee was always doing the work, and
blue "(you)" and amber "their move" stay clearly distinct without it.
The west-to-east chain under the Division map now shows only during Day 1
Stage 1. It answers who is where and why, which is a question you have once
— at the start, when the chain has just been rolled and the names are new.
By Stage 2 the map has been answering it for a while and the line is
something to read past.
675 tests pass (673 + 2).
|
||
|
|
689de2ff0f |
v0.5.4 — the map says whose railroad is whose
Six things found playing the StartOS build, all of them the game telling you what it already knew. The lobby's Start button did not look disabled when it was. The reported symptom was "it says it's waiting for a player but Start is enabled" — it wasn't: the note and the disabled assignment are two lines apart in the same block. The page had only `header button:disabled` and `#actions button:disabled`, and #lb-start is in neither, so a disabled button kept its normal face AND still lit up under the cursor from the generic button:hover. It advertised a click it would refuse. The rule is generic now. The game code was rendered as "— code TRESTLE-5109" in dim text beside a heading, reading like a reference number rather than the thing you have to send somebody. It is a labelled block at 22px with a Copy button, and a clipboard refusal says the code can be selected instead of failing silently. The blurb under it was also WRONG — it claimed the chairs were "in the order everyone joined", which stopped being true in v0.4.1 when the §4.4 D12 started deciding. It now says what actually happens. Every Office on the Division map was labelled with its tier, which every other player's Office also has, so four districts read identically and "where does Bob sit" had no answer on the one map showing where trains are. The owner's name takes the headline and the tier moves beside the A/D count. Amber marks whose move it is — the same "happening here" the action panel uses — and "(you)" is spelled out on the reader's own district, because colour alone cannot say which of four railroads is yours. Turn colour wins over the you-colour when both apply: whose turn it is changes every few seconds, which railroad is yours never does. Under the map, the chain in words with the roll behind it: "West to East: Alice (1) → Bot 2 (5) → Bot 1 (11)". state.openingRolls has been kept for exactly this since v0.4.1 and nothing had displayed it. It also answers "is the host always at the eastern end" outright — no. Alice there is the host, rolled lowest, and sits at the western end. Supporting: Frame gained viewer and viewerSeat. Every private field on it was already scoped to one player, but nothing said which player, so a page could draw a railroad without being able to say whose it was — harmless in solitaire, the first question at four seats. Frame also gained openingRolls. Bots are Bot 1 / Bot 2 rather than all Bot, since two of them are two different railroads. The standalone replay gets all of it: players, actor and viewer are not delta'd keys in compress, so they ride whole on every frame and replay.ts passes the same roster. Verified: 673 tests pass (668 + 5). The new ones were mutation-checked — removing the (you) suffix, never applying the turn mark, and reinstating the pre-v0.4.1 identity seating each fail the suite. The seating test deliberately asserts across six seeds that the eastern end is NOT always player 0, which is the claim it exists to defend. |
||
|
|
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. |
||
|
|
f9c4d9fa92 |
v0.4.9d — three playtest bugs: a train that looked gone, an unload that ignored the pick, a hang that wasn't one
Patched directly onto 0.4.9a rather than the in-progress 0.5.0 line. A switching train vanished from the board the moment it left the Office. `selectedTrain` in `officeSvg` (board-svg.ts), which gates whether a card draws a train's crew badge, was only ever assigned inside the `cell.adTracks !== null` branch — true for the Office card alone. A train standing anywhere else, which is everywhere it stands while actually being switched, drew no badge at all. Game state was never affected; confirmed against the reported save with the engine directly. Fix hoists the assignment out of the guard so it runs for any card with a train on it. Unloading always took the westmost car, whichever one was picked. `laborer.beginUnload` carried the player's chosen `carIndex`, and `check()` validated that specific car, but the `unloadBegan` event it produced carried only the car's type — the reducer that performs the swap re-derived the target with `industryTrack.cars.findIndex(c => c.loaded)`, which always answers the first loaded car in track order regardless of what was requested. Fix adds `carIndex` to the event and uses it directly. A legal decision could render with zero buttons, which looked exactly like a hang. The "where does this Extra start" decision is titled by `trainCardTitle`, beginning "Making up Extra X22…" — the same prefix `renderActions()` stripped from the action list on the assumption it only ever belonged to the separate yard-chip car-placement panel. With no tray yet being filled, that panel is null, so nothing rendered the Extra's decision either: a legal, correctly-computed option with no button anywhere on the page. Replaying the reported save found no engine deadlock at any step — the engine always had a move. Fix matches the exact title of the one group the yard-chip panel covers instead of a title-prefix regex. New tests for all three: a train parked on an ordinary facility card must draw its crew badge; three differently-typed loaded cars, unloading index 2, must leave indices 0 and 1 alone; the existing softlock regression test now mirrors the real render filter instead of only the raw menu, plus a deterministic test for the exact reported scenario. 590 tests, 0 failures. |
||
|
|
859817d173 |
v0.4.9 — X,Y coordinates, no-switching can still clear the mainline, Q3 corrected, a clearance bug, three sounds, and the box art
A playtest review of seed 58228926 (day 6), plus one long-standing display complaint and the first real audio beyond a placeholder. - Coordinate labels read X,Y everywhere shown to a player, not the internal Y,X storage order. Display-only. - "No switching" now means may not add or drop cars, not "never touch it" — these trains can still be moved onto Secondary Track to clear the mainline. - Q3 corrected: Expedite governs WHERE a train may be left standing, not WHEN it leaves. The forced same-Stage departure is gone; a new fault costs 1 Revenue if an expedited train is left off the station when a Mainline Phase begins. Resolves "3/4 Express prints a rule it can never use" as a side effect. - evaluateClearance now checks every occupant on a Mainline card before offering a judgment call, instead of returning on whichever it found first — found while explaining a playtest report, fixed with a regression test. - Three new synthesised sounds: arrive, depart, crash. - The splash page shows the box art. Full detail, measurements and reasoning in CHANGELOG.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FvU99NEakShRMg3nN3fHAZ |
||
|
|
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 |
||
|
|
461c4d9bac |
v0.4.8 — the Limits bound the whole district, the nine spots really are nine, and an action points at its square
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQAJ4dND7enLj54eLyiF2C |
||
|
|
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.
|
||
|
|
08339effba | more info on train moves in history. train info added to tooltip on timetable and division map. support for multiple trains in area switching. more info on train7/8 wrt location of coach on initial train build. | ||
|
|
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 | ||
|
|
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 | ||
|
|
1bf1e95058 | v0.3.0 — playtest fixes, two new rules, and §7 enforced | ||
|
|
6ead39c530 | fixed train# specific rules. cleaned up display topline and train display on division map | ||
|
|
5d825b97d2 |
v0.1.0 — undo, the crew's reach on the board, workers on the card, and four engine bugs
Fifteen items from two playtest sessions. Three that read as drawing faults were engine bugs: cars could be added to a train that was not being made up (50 offers in 8 games), the make-up panel merged two trains and could couple a car to the wrong one, and an Office upgrade silently deleted what a Modifier had added. A fourth was a sentinel inside a coordinate's own value range — a Mainline placement travelling as row -1, which is an ordinary district row. Trains are now drawn the way they stand: west on the left, nose toward the way the engine faces, on both the district card and the Division chip. Undo steps back through the game by replaying the save without its last intent. The switching walk keeps its rejections, so the board can say why a square is not offered. Laborers and Porters are on the card, and the rule that a district only grows outwards is finally written down. Versions start here: third digit for fixes, second for a feature set, 1.0 for a release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GgtkX8JnvKa8y2tuJ8aQf4 |
||
|
|
f4c0f49604 | More bug fixes. Show the timetable, and stop the arrival message contradicting itself. Fixed division point in labels. And highlighted discard as targets. | ||
|
|
543584a2ca | user card Hand becomes the action surface, the board shows the train, and a reverse stops turning it around, other bug fixes too. | ||
|
|
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 | ||
|
|
2d348e1ecf | fix tracks to all be EW. no more NS tracks | ||
|
|
4bba991f41 | Fixed division end of track labels and other minor bugs. Trains now show their information. Play cards now required to get to 3 or less, no other restrictions. auto hide now more prominent. it and sounds now are in replay. | ||
|
|
c3c5c59a45 | Show the whole Division mainline. district now has option for auto hiding on phases where not needed. scaffold for multi-player geometry showing division. regions on mainline cards implemented. bug fix on laying rail cards. optimized replay (carried links forward). Add sounds, first pass | ||
|
|
982952fd6c | Draw freight on the card, and teach the bot to build a railroad | ||
|
|
f52ff0e9ac |
Draw the board as track, split the site into three pages, and fix curve geometry
Adds SVG board rendering shared by the game and the replay, a splash page with a shareable replay directory, hover tooltips for reference detail, and makes curves two-port arcs so sidings and run-arounds can finally be built. |