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
This commit is contained in:
Jesse
2026-08-19 17:32:17 -04:00
co-authored by Claude Sonnet 5
parent fbaa3d4147
commit 37b1e5b969
18 changed files with 506 additions and 187 deletions
+17 -6
View File
@@ -110,8 +110,8 @@ const esc = (s: string): string =>
*/
function piecePreview(links: string[], label: string): string {
const cell = {
row: 0, col: 0, kind: 'trk', label, running: false, enhancements: [],
tray: null, cars: [], facility: null, links, what: '',
row: 0, col: 0, kind: 'trk', label, running: false, enhancements: [], enhancementsWhat: [],
trains: [], adTracks: null, cars: [], standingWest: 0, facility: null, links, what: '',
};
return officeSvg([cell] as never, 99).replace(
/<text class="bs-(coord|rowlab)"[^>]*>[^<]*<\/text>/g,
@@ -301,7 +301,14 @@ function render(): void {
: 'ATTACH TO THIS CARD';
return [{ row: cell.row, col: cell.col, label }];
});
grid.innerHTML = officeSvg(f.cells, f.runningRow, ghostCoords, legalCaps, f.limits);
grid.innerHTML = officeSvg(f.cells, f.runningRow, ghostCoords, legalCaps, f.limits, selectedCrew);
// Wire the roster chips: clicking one sets `selectedCrew`, the same value the "Which train are
// you switching?" picker writes, so the board and the action panel drive one value either way.
for (const g of Array.from(grid.querySelectorAll('g[data-crew]'))) {
const trayId = (g as HTMLElement).dataset['crew'];
if (trayId) (g as unknown as HTMLElement).onclick = () => { selectedCrew = trayId; render(); };
}
// Highlighting rides on top of the drawing: outline the legal squares and make them clickable.
for (const [key, list] of spotsAt) {
@@ -337,8 +344,10 @@ function render(): void {
*/
const crew = pickedCrew(f);
if (crew && !forPlay) {
const here = grid.querySelector(`g[data-cell="${crew.from.row},${crew.from.col}"]`);
if (here) here.classList.add('bs-from');
// Marked on the crew strip, not the whole card: the Office is the one square a second train may
// share, and outlining the card would claim it belongs to both.
const strip = grid.querySelector(`g[data-cell="${crew.from.row},${crew.from.col}"] .bs-crew`);
if (strip) strip.classList.add('bs-from');
for (const c of crew.to) {
const g = grid.querySelector(`g[data-cell="${c.row},${c.col}"]`);
if (g) g.classList.add('bs-focus');
@@ -629,7 +638,9 @@ function renderDistrict(f: Frame): void {
else sec.classList.add('folded');
const cars = f.cells.reduce((n, c) => n + c.cars.length, 0);
const crew = f.cells.filter((c) => c.tray).length;
// Trains, not cards-with-a-crew: the Office is the one card that may hold more than one, and a
// card-count silently read "1 crew on the board" with two trains standing at a busy Station.
const crew = f.cells.reduce((n, c) => n + c.trains.length, 0);
$('districtsummary').textContent =
`${f.cells.length} cards · ${f.facilities.length} facilities · ${cars} cars standing` +
(crew > 0 ? ` · ${crew} crew on the board` : '');