Commit Graph
33 Commits
Author SHA1 Message Date
Jesse.MarkowitzandClaude Opus 5 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
2026-09-23 07:07:21 -04:00
Jesse.MarkowitzandClaude Opus 5 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
2026-09-21 05:53:52 -04:00
Jesse.MarkowitzandClaude Opus 5 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
2026-09-16 16:06:01 -04:00
Jesse.MarkowitzandClaude Opus 5 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
2026-09-15 22:44:20 -04:00
Jesse.MarkowitzandClaude Opus 5 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
2026-09-07 21:54:18 -04:00
Jesse.MarkowitzandClaude Opus 5 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
2026-09-07 15:00:57 -04:00
Jesse.MarkowitzandClaude Opus 5 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
2026-09-07 11:41:23 -04:00
Jesse.MarkowitzandClaude Opus 5 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
cf018b4 and never checked off. The Fedora is TODO #29, done in this
same unshipped v0.7.9. The remaining six were the Division-map drawing
pass, all answered by Gitea#18 (closed 2026-08-26) replacing the layout
rather than fixing the drawing — 167 lines describing a wrapped map
that no longer exists, collapsed to 32 that keep the two ideas which
outlived their items: "SHARED in the middle, YOURS on the right", now
Gitea#20's territory, and why rotating the map to seat a viewer at the
bottom was refused (it moves the open gap between the buffer stops out
of the eye's path, and that gap is the only thing saying the Division
is a line and not a loop).

65 open items to 57; 2,400 lines to 2,179.

TWO PIECES OF DEAD CODE GITEA#18 LEFT BEHIND, found while confirming
the map really is one row before closing the items that say it is.
SIDE_GAP was declared and never read. And divisionSvg's header comment
still described "one row alone, two rows facing, a horseshoe of three,
a square of four" — contradicting the layout comment 200 lines below
it, which explains why that layout was dropped. A comment that
survives the code it describes is how the next reader gets it wrong.

No behaviour change. 878 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YTaNBL1jVxNqgFdjHkHoo3
2026-08-30 16:10:41 -04:00
Jesse.MarkowitzandClaude Sonnet 5 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
2026-08-30 05:28:31 -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.Markowitz 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.
2026-08-22 19:51:40 -04:00
Jesse 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).
2026-08-21 20:53:17 -04:00
Jesse 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.
2026-08-21 17:12:25 -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
Jesse 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.
2026-08-20 23:26:39 -04:00
JesseandClaude Sonnet 5 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
2026-08-19 23:51:35 -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 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 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. 2026-08-16 21:45:19 -04:00
Jesse 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 2026-08-14 22:39:50 -04:00
Jesse 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 2026-08-13 14:06:02 -04:00
Jesse 1bf1e95058 v0.3.0 — playtest fixes, two new rules, and §7 enforced 2026-08-12 21:08:28 -04:00
Jesse 6ead39c530 fixed train# specific rules. cleaned up display topline and train display on division map 2026-08-09 19:44:18 -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 f4c0f49604 More bug fixes. Show the timetable, and stop the arrival message contradicting itself. Fixed division point in labels. And highlighted discard as targets. 2026-08-05 23:08:03 -04:00
Jesse 543584a2ca user card Hand becomes the action surface, the board shows the train, and a reverse stops turning it around, other bug fixes too. 2026-08-05 19:18:26 -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 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. 2026-08-04 13:43:28 -04:00
Jesse 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 2026-08-04 11:26:18 -04:00
Jesse 982952fd6c Draw freight on the card, and teach the bot to build a railroad 2026-08-04 04:59:23 -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