Commit Graph
19 Commits
Author SHA1 Message Date
Jesse.MarkowitzandClaude Opus 5 a6657241de v0.8.0.14 — the coaches that never come back, and a district that ends at its own sign
Six reports from the Day 2-3 playtest of v0.8.0.13.

GAMES IN PROGRESS DO NOT SURVIVE THIS ONE. Modifiers are now bounded by the
Limits, which makes a once-legal move illegal, so a save holding one is refused
at that move: whistle-6945.day3.stage10 stops at intent 528 of 539. Jesse's call,
knowing it strands the game on the box. The file is untouched and v0.8.0.13
still finishes it.

The Sparrow running empty and Tom unable to unload his passengers are the same
shortage from opposite ends, and both are the rules working as printed. §9.2
boarding discards the emptied coach into the CLASSIFICATION yard, detraining
draws a fresh empty out of the DIVISION yard, and §2.2 sends Classification back
only when the Division Yard runs bare — so coaches move one way. Measured over
the save: sixteen in the Division Yard at setup, zero from Day 2 Stage 8 to the
end, fifteen piled in Classification, the Division Yard steady at 46-47 freight
cars with no prospect of going bare. Jesse's ruling is Gitea#2's: the shortage
stays and the game says so. A train made up short now reports what its card
wanted and why none is coming (`makeUpShort` — `trainNeedingCars` answered null
for "done" and for "cannot be done" alike, so the phase moved on in silence); the
yard panel warns while the condition lasts; the Depot's blocked panel was right
all along.

The modifier outside the Limits was working as designed and the design was
Jesse's own call, now reversed. What decided it is what the board shows — a card
beyond your own sign, in territory §8.1 and §10 reason about. The case that
motivated the exemption was checked on the reported move rather than argued away:
the Power Plant sat at (-1,3) against a sign at column 3 and two spots inside
were free, legal and adjacent.

Switching filled the history with coordinates — a line per move, plus one per
mandatory coupling. It is still LOGGED in full; what the panel draws is the line
saying somebody switched, the first move, work at an INDUSTRY (named, not a
coordinate), the Small Yard sort, and a closing summary. The suppressed lines are
still WRITTEN, marked `trace`: dropping them outright was the first attempt and
the step-queue suite caught it, because dwellForStep pays nothing for a step that
said nothing, so the board stopped replaying switching at all. The last move
rides in the closing line rather than being kept in place — nothing knows a move
was the last until the turn is over, by which time the line has been streamed to
every client and cannot be revised. Two things fell out of reading those lines:
every move ended with a tutorial sentence the opener already gives, and the move
count said "of 6" with the six hardcoded, which is wrong on a night Stage.

Make-up lines name their train — they all read "the train being made up", so
looking back for train 10 found nothing under that name — and "a empty tank" is
now "an empty tank". The Small Yard's options read as the train they would build
instead of `[1,2,3,0]`; the one Jesse wanted was the first of five and unreadable.
Two of those five were junk: bringing the last car to the end is the identity and
would have spent a Move, and a two-car reversal duplicated its only real option.
Both are filtered by the resulting order, not by the case that made them.

A Small Yard may now put cars AHEAD of the engine, which was Jesse's own open
question. Two sources disagreed and the design notes won: the v0.4.5 card text
says the sort puts the engine at the nose, implications.md says "any order,
including cars ahead of the engine". `engineAt` is optional on the intent, so
older saves replay to the same train. The menu did not multiply — the engine is a
separate short list against the consist as it stands, eight options for a
four-car train rather than twenty. §8.2 needed no new code: badlyMadeUp is
deliberately direction-free, so a PUSHING train is fit to run and only a
broken-backed one is held. The button warns by asking that predicate rather than
copying it, and immediately earned itself — every one of train 10's eight options
is refused, the one asked for at the table included, because that train carries a
caboose and each sort moves it off the rear. That is the right answer rather than
a gap: the train is already made up, so every offer would break it, and the labels
say which is which. A made-up order is always on the menu for a train that needs
one, because "bring car k to the tail" is enumerated for every car and the caboose
is one of them.

Labels read WEST TO EAST, with the engine drawn as the board's own ◀ / ▶ arrow.
"Front to back" is not a direction a table can read — which end is the front
depends on which way the train points — and board-svg has reversed east-facing
consists since v0.8.0, so the button now describes the same train as the picture.

The Freight Agent, Porter and Laborer groups now say what the role is for, where
the role is chosen. Tom reached for the Freight Agent to detrain passengers,
which is a Porter's action in the Cargo phase; both halves were working and
neither was visible.

TODO closes #107 (the nose sort) and gains #108 (the coach ratchet, with the
measurement, to revisit on a second game's data).

999 fast tests and 35 sim tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DmdqqCNoiqE7GBo6wthBnR
2026-09-17 20:49:31 -04:00
Jesse.MarkowitzandClaude Opus 5 76c6e103b3 v0.8.0.9 — the bot plans its switching turn, stops wasting its draws, and the engine walks each route once
The developer bot, re-measured decision by decision against the bot before it,
goes from about -0.3 revenue a game to about 4.8:

- plans the whole switching turn before its first Move (sim/switch-planner.ts),
  +2.89 over 1600 paired seeds; closes TODO #53
- takes a face-up card only if it could play it, +1.52 over 1600 seeds
- stops running Second Sections by accident in the New Train phase, +0.32
- lays track by what the district can do afterwards, +0.12 over 6400 seeds,
  run-arounds in 22 of 60 districts against 9

The engine is 2.8x faster with play proven identical: a route cache scoped to
one unchanged position, applyIntent split into prepareIntent + commitEvents,
and less allocation in exploreMoves. npm test now leaves out the bot
simulations, which run as npm run test:sim.

No rule changed; games in progress resume. Rejected candidates and the
Second Section card question are in CHANGELOG.md and TODO.md (#104-#106).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nnuCv8UodHucFfx3LWEoX
2026-09-15 15:30:42 -04:00
Jesse.Markowitz 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
2026-08-26 15:20:56 -04:00
Jesse c3c5cbfeec v0.5.0 — multiplayer Phases 2 and 3: a server that runs a game and survives being restarted
Phases 0-1 shipped in v0.4.0 (seat/identity split, per-player turn state, the Session boundary).
This lands Phase 2 (server core, one game, no lobby) and Phase 3 (persistence and resumption) per
docs/architecture/multiplayer.md §12. Phases 4-6 (lobby/reconnection, the 22 opponent-directed
cards, StartOS packaging) are still ahead.

Phase 2: src/server/session.ts hosts a game in pure logic (no sockets) on top of game.ts's existing
Game/submit/currentActor/actionMenu; it verifies seat === currentActor(game) itself before calling
submit, since submit() trusts its caller and a server can't. src/server/http.ts and index.ts add
POST /api/game, GET /api/stream (SSE, per-seat), POST /api/intent, and static serving of dist/.
src/sim/frame-delta.ts is a purpose-built per-seat board delta for one live push at a time. Found
and fixed along the way: actionMenu(game, seat) only used seat for the hand field, so a server
computing every connected seat's Menu would have handed the acting player's legal moves to a
waiting seat. Verified with a live end-to-end smoke test (2-player game, two SSE streams, a
rejected intent from the wrong seat, an idempotent resend) plus test/server/session.test.ts and
test/redaction.test.ts. Not verified: an actual browser (none available in this environment).

Phase 3: src/server/persistence.ts writes game.json and turn-timings.json, atomic-rewrite-then-
rename. game.ts gained fromMultiplayerSave, fixing a narration-attribution bug found while testing
it (fromSave's replay loop drops the actor argument, invisible in solitaire, unreadable the moment
there's more than one seat — fromSave itself still has this gap, deliberately untouched). Verified
live: server killed and restarted mid-game, both seats reconnected exactly where they left off.

Two rules bugs found while building this: the New Train phase never implemented its car-placement
round (every car of every train was placed by the Superintendent alone, in every mode, all along —
now reads the round position off tray.consist.length); and victory conditions are now one shared,
configurable GameConfig set across solitaire/competitive/coop instead of a fixed length lookup and
a dead firstToTarget condition.

Also folds in the three fixes already released on the patch line as v0.4.9b/c/d: a switching
train's crew badge failing to draw once it left the Office square, an unload that always took the
westmost car regardless of which was picked, and a legal decision that could render with zero
buttons.

docs/testing/0.5.0-test-plan.md and three reported-bug save files (docs/station-master-seed*.json)
included for reproducibility. tools/jitsi-harness/ deliberately left untracked — unrelated
side-project work, not part of this release. 635 tests, 0 failures.
2026-08-20 23:50:38 -04:00
JesseandClaude Sonnet 5 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
2026-08-19 17:32:17 -04:00
JesseandClaude Sonnet 5 fbaa3d4147 switching paths: two routes to the same square
Builds docs/plans/switching-paths.md. A passing loop can offer two legal
routes between the same two squares, coupling different cars — the engine
only ever found one, an artifact of search order (Reported by Jesse, undo
379). exploreMoves now enumerates every simple route (per-path visited set,
capped at 4000 frontier nodes) and dedupes on outcome — destination, entry
side, and origin-tagged cars — rather than on reaching the square at all.

switch.move gains an optional `via: GridCoord` naming one intermediate
square on the chosen route; absent, it resolves exactly as before, so
every existing save and bot decision replays identically (575/575, then
579/579 with the new tests). Threaded through the label, the action-list
dedupe, the hover highlight (data-route), and the history (trayMoved.via).

Ruling recorded as Gap 14 in docs/rules/open-questions.md: the player may
choose the path; §A.4's "may not go around" a car does not reach a
different track the player declined to enter.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAt2YCXgd5qCjBcF2x34aK
2026-08-19 16:52:10 -04:00
JesseandClaude Opus 5 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
2026-08-19 13:38:06 -04:00
Jesse 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.
2026-08-19 12:12:30 -04:00
Jesse 1bf1e95058 v0.3.0 — playtest fixes, two new rules, and §7 enforced 2026-08-12 21:08:28 -04:00
JesseandClaude Opus 5 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
2026-08-08 05:23:17 -04:00
Jesse 1c321d4cea make turnout description more clear. fix curves from breaking running track. 2026-08-05 10:03:50 -04:00
Jesse 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 2026-08-05 08:28:22 -04:00
Jesse 2d348e1ecf fix tracks to all be EW. no more NS tracks 2026-08-05 01:00:11 -04:00
Jesse 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.
2026-08-01 13:35:53 -04:00
Jesse dd300ac154 Fix caching, Limits placement, and explain the board better 2026-07-31 23:34:18 -04:00
Jesse 2eca9de09f various fixes. added version#s and added a playable browser build of the solitaire game (including deploy to filebrowser script) 2026-07-31 22:03:13 -04:00
Jesse b085d5a0bb Implement Enhancements, Mainline modifiers and Maneuvers; fix unplayable track 2026-07-31 11:02:54 -04:00
Jesse 401b879140 Expand game engine, replay, tests, and documentation 2026-07-31 08:10:54 -04:00
Jesse e7bc07df85 Initial commit 2026-07-31 07:19:57 -04:00