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

This commit is contained in:
Jesse
2026-08-14 22:39:50 -04:00
parent d1314066bd
commit 6655e20ea8
29 changed files with 5450 additions and 3902 deletions
+22 -10
View File
@@ -38,11 +38,12 @@ import {
lengthProfile,
officeProfile,
trainProfile,
houseRules,
} from '../engine/content.ts';
import type { Intent } from '../engine/intents.ts';
import type { Facility, GameState, PlayerIndex, TrackCard, TurnoutOrientation } from '../engine/state.ts';
import { playerAtSeat, seatOf, turnOf } from '../engine/state.ts';
import type { Hand, TrackGeometry } from '../engine/content.ts';
import { playerAtSeat, railFacingOf, seatOf, turnOf } from '../engine/state.ts';
import type { Hand, HouseRules, TrackGeometry } from '../engine/content.ts';
import type { Port } from '../engine/track.ts';
import { connectionsFor, slopeOfPair, variantsFor } from '../engine/track.ts';
import type { Impediment } from './narrate.ts';
@@ -76,13 +77,14 @@ export type CellView = {
* not plan a move at all: "drop 1 car" tells you nothing when you cannot see what is on the back.
*
* `cars` runs nose first, matching the tray; `engineAt` is where the locomotive sits in it, and
* `facing` is the port it points at on this card.
* `facing` is which way the engine points — EAST OR WEST, never north or south, whatever the
* track under it runs. See `railFacingOf` in state.ts for why, and why the type says so.
*/
train: {
label: string;
cars: string[];
engineAt: number;
facing: string;
facing: 'e' | 'w';
/**
* WHAT THIS PARTICULAR TRAIN'S CARD SAYS.
*
@@ -188,13 +190,14 @@ export type TrainChip = {
* THE SAME TRAIN THE OFFICE CARD DRAWS, so the Division map can draw it the same way.
*
* `cars` is nose first and carries no engine; `engineAt` is where the engine sits among them and
* `facing` is the port it points at. The Division chip used to be a name and a number — and the
* number was Stages left to cross, which reads as redundant beside the position already drawn on
* the card. A train is worth drawing: what it is carrying, loaded or empty, and which end leads.
* `facing` is which way the engine points, east or west. The Division chip used to be a name and a
* number — and the number was Stages left to cross, which reads as redundant beside the position
* already drawn on the card. A train is worth drawing: what it is carrying, loaded or empty, and
* which end leads.
*/
cars: string[];
engineAt: number;
facing: string;
facing: 'e' | 'w';
/**
* Which region of a Mainline card the train is standing in, and which way it is going. Absent
* everywhere else: a Division Point is a single queue, and inside a district a train moves by
@@ -287,6 +290,14 @@ export type Frame = {
*/
/** Which of §6's three exclusive options the VIEWER has taken this Stage, if any. */
option: 'switch' | 'draw' | 'freightAgent' | null;
/**
* The settings this game was dealt under — the opening hand, and what the three economies pay.
*
* On the Frame rather than read off the config, for the same reason as everything else here: a
* remote client holds no `GameState`, and "what does a load pay in this game?" is a question it
* must be able to answer. Resolved, never partial, so nobody downstream re-applies defaults.
*/
houseRules: HouseRules;
status: GameState['status'];
outcome: GameState['outcome'];
/**
@@ -513,7 +524,7 @@ function trainOnCard(s: GameState, key: string): CellView['train'] {
label: t.trainNumber === null ? 'crew' : `T${t.trainIsExtra ? 'X' : ''}${t.trainNumber}`,
cars: t.consist.map(carLabel),
engineAt: Math.max(0, Math.min(t.consist.length, t.engineAt)),
facing: t.facing ?? (t.direction === 'west' ? 'w' : 'e'),
facing: railFacingOf(t),
what: t.trainNumber === null ? 'A local crew — no timetable, no card, no special rules.' : trainRules(t),
};
void id;
@@ -1044,6 +1055,7 @@ export function snapshot(
decision,
wasted,
option: turnOf(s, viewer).option,
houseRules: houseRules(s.config),
status: s.status,
outcome: s.outcome,
players: s.players.map((p) => ({
@@ -1579,7 +1591,7 @@ function trainChip(s: GameState, id: string): TrainChip {
consist: seated,
cars,
engineAt: at,
facing: t.facing ?? (t.direction === 'west' ? 'w' : 'e'),
facing: railFacingOf(t),
};
}