Expand game engine, replay, tests, and documentation

This commit is contained in:
Jesse
2026-07-31 08:10:54 -04:00
parent e7bc07df85
commit 401b879140
30 changed files with 2253 additions and 552 deletions
+26 -13
View File
@@ -16,6 +16,7 @@
*/
import { MAX_CONSIST } from './content.ts';
import type { TrackGeometry } from './content.ts';
import type {
GridCoord,
OfficeArea,
@@ -78,15 +79,19 @@ type PortPair = readonly [Port, Port];
*/
function connectionsFor(card: TrackCard): readonly PortPair[] {
switch (card.geometry.kind) {
// A Modifier is not track: nothing connects to it and no train may enter (§9).
case 'modifier':
return [];
case 'limits':
return [['e', 'w']];
case 'facility':
return card.geometry.axis === 'ns' ? [['n', 's']] : [['e', 'w']];
// Q7 — the card art draws the through-track along the TOP edge with everything diverging
// downward, so a district is a Running Track with all Secondary Track hanging beneath it.
// This supersedes Gap 8's north-and-south stubs.
case 'office':
return [
['e', 'w'],
['e', 'n'],
['w', 'n'],
['e', 's'],
['w', 's'],
];
@@ -102,12 +107,17 @@ function connectionsFor(card: TrackCard): readonly PortPair[] {
[o.stem, o.diverge],
];
}
case 'runAround': {
const b = card.geometry.bypass ?? 'n';
// A CURVE joins the through track to one diverging side — handedness is printed on the
// card, not chosen (the design supplies both hands). A SHARP curve is geometrically the
// same but costs two Moves to cross.
// Curves and turnouts diverge DOWNWARD from the through track (Q7); handedness decides
// which end of the through track the leg leaves from, not whether it goes up or down.
case 'curved':
case 'sharpCurved': {
const stem = card.geometry.hand === 'left' ? 'w' : 'e';
return [
['e', 'w'],
['e', b],
['w', b],
[stem, 's'],
];
}
}
@@ -139,23 +149,26 @@ export type TrackVariant = {
bypass?: Port;
};
/**
* Q7 — everything diverges downward, so a turnout's leg is always south. Handedness (printed on
* the card) decides which end of the through track it leaves from.
*/
const TURNOUT_VARIANTS: readonly TurnoutOrientation[] = [
{ stem: 'e', through: 'w', diverge: 'n' },
{ stem: 'e', through: 'w', diverge: 's' },
{ stem: 'w', through: 'e', diverge: 'n' },
{ stem: 'w', through: 'e', diverge: 's' },
{ stem: 'n', through: 's', diverge: 'e' },
{ stem: 's', through: 'n', diverge: 'w' },
];
export function variantsFor(geometry: 'straight' | 'turnout' | 'runAround'): TrackVariant[] {
export function variantsFor(geometry: TrackGeometry): TrackVariant[] {
switch (geometry) {
case 'straight':
return [{ axis: 'ew' }, { axis: 'ns' }];
case 'turnout':
return TURNOUT_VARIANTS.map((t) => ({ turnout: t }));
case 'runAround':
return [{ bypass: 'n' }, { bypass: 's' }];
case 'curved':
case 'sharpCurved':
// Handedness is printed on the card, and the leg always goes down (Q7), so nothing is left
// to choose — one variant.
return [{ axis: 'ew' }];
}
}