v0.3.0 — playtest fixes, two new rules, and §7 enforced

This commit is contained in:
Jesse
2026-08-12 21:08:28 -04:00
parent 6ead39c530
commit 1bf1e95058
38 changed files with 8514 additions and 5989 deletions
+21 -19
View File
@@ -212,7 +212,7 @@ describe('ports and geometry', () => {
modifiers: [],
enhancements: [],
};
assert.deepEqual(exitsFrom(r, 's'), ['w']);
assert.deepEqual(exitsFrom(r, 's'), ['e']);
});
});
@@ -241,14 +241,14 @@ describe('the 45° matching rule', () => {
});
it('matches a turnout to the curve of the same hand, and to no other', () => {
// A siding needs one card of each hand: a left turnout to drop off the main, a right curve to
// A siding needs one card of each hand: a right turnout to drop off the main, a left curve to
// climb back. This is the pairing that makes that true.
const leftTurnout = turnoutOf({ stem: 'w', through: 'e', diverge: 's' });
const rightTurnout = turnoutOf({ stem: 'e', through: 'w', diverge: 's' });
assert.ok(joins(leftTurnout, 's', arcCard('ne')));
assert.ok(!joins(leftTurnout, 's', arcCard('nw')));
assert.ok(joins(rightTurnout, 's', arcCard('nw')));
assert.ok(!joins(rightTurnout, 's', arcCard('ne')));
const rightTurnout = turnoutOf({ stem: 'w', through: 'e', diverge: 's' });
const leftTurnout = turnoutOf({ stem: 'e', through: 'w', diverge: 's' });
assert.ok(joins(rightTurnout, 's', arcCard('ne')));
assert.ok(!joins(rightTurnout, 's', arcCard('nw')));
assert.ok(joins(leftTurnout, 's', arcCard('nw')));
assert.ok(!joins(leftTurnout, 's', arcCard('ne')));
});
it('ignores slope on an east-west edge, where every card meets at the same height', () => {
@@ -290,7 +290,7 @@ describe('the 45° matching rule', () => {
// A printed card has a back: it turns 180° but never flips. That is the whole reason handedness
// is not decoration.
for (const geometry of ['curved', 'sharpCurved', 'turnout'] as const) {
for (const [hand, expected] of [['left', 'ne_sw'], ['right', 'nw_se']] as const) {
for (const [hand, expected] of [['left', 'nw_se'], ['right', 'ne_sw']] as const) {
const variants = variantsFor(geometry, hand);
assert.equal(variants.length, 2, `${geometry}/${hand} should offer 0° and 180°, and nothing else`);
const legs = variants.map((v) => {
@@ -605,11 +605,11 @@ describe('curves are arcs, and arcs make sidings possible', () => {
});
it('offers two rotations, and which two depends on the printed hand', () => {
// A card turns 180° but never flips, so a left-hand curve reaches `sw` and `ne` and can never
// become `se` or `nw` — those are the right-hand card.
// A card turns 180° but never flips, so a left-hand curve reaches `se` and `nw` and can never
// become `sw` or `ne` — those are the right-hand card.
for (const g of ['curved', 'sharpCurved'] as const) {
assert.deepEqual(variantsFor(g, 'left').map((v) => v.arc).sort(), ['ne', 'sw'], `${g} left`);
assert.deepEqual(variantsFor(g, 'right').map((v) => v.arc).sort(), ['nw', 'se'], `${g} right`);
assert.deepEqual(variantsFor(g, 'left').map((v) => v.arc).sort(), ['nw', 'se'], `${g} left`);
assert.deepEqual(variantsFor(g, 'right').map((v) => v.arc).sort(), ['ne', 'sw'], `${g} right`);
}
});
@@ -624,18 +624,18 @@ describe('curves are arcs, and arcs make sidings possible', () => {
});
it('lets a crew run a siding and rejoin the main', () => {
// THE CANONICAL SIDING, and the reason handedness matters: leave the Running Track on a LEFT
// turnout, drop onto its matching left curve (`ne`), run parallel, and climb back on a RIGHT
// curve (`nw`) into a RIGHT turnout. One card of each hand, and the slopes agree at both ends.
// THE CANONICAL SIDING, and the reason handedness matters: leave the Running Track on a RIGHT
// turnout, drop onto its matching right curve (`ne`), run parallel, and climb back on a LEFT
// curve (`nw`) into a LEFT turnout. One card of each hand, and the slopes agree at both ends.
// A crew must ENTER a turnout through its stem to take the diverging leg (§A.1).
const ew = (): TrackCard => straightCard();
const grid: Record<string, TrackCard> = {
'0,-2': ew(),
'0,-1': { geometry: { kind: 'track', geometry: 'turnout', turnout: { stem: 'w', through: 'e', diverge: 's' }, hand: 'left' }, baseOperationalRail: true, standing: [], facility: null, modifiers: [], enhancements: [] },
'0,-1': { geometry: { kind: 'track', geometry: 'turnout', turnout: { stem: 'w', through: 'e', diverge: 's' }, hand: 'right' }, baseOperationalRail: true, standing: [], facility: null, modifiers: [], enhancements: [] },
'-1,-1': arcCard('ne'),
'-1,0': ew(),
'-1,1': arcCard('nw'),
'0,1': { geometry: { kind: 'track', geometry: 'turnout', turnout: { stem: 'e', through: 'w', diverge: 's' }, hand: 'right' }, baseOperationalRail: true, standing: [], facility: null, modifiers: [], enhancements: [] },
'0,1': { geometry: { kind: 'track', geometry: 'turnout', turnout: { stem: 'e', through: 'w', diverge: 's' }, hand: 'left' }, baseOperationalRail: true, standing: [], facility: null, modifiers: [], enhancements: [] },
'0,0': ew(),
};
const area = areaFrom(grid, { row: 0, col: 0 });
@@ -679,7 +679,9 @@ describe('coupling lifts only the cars the crew ran over', () => {
const area = areaFrom(grid, { row: 0, col: 0 });
const s = gameWith(area);
s.trays.set('crew', {
id: 'crew', trainNumber: 1, trainIsExtra: false, engineAt: 0, consist: [],
// A local CREW, which is what this test is about — `trainNumber: null`. It used to say train 1,
// the Crack Limited, which prints "no switching" (§7) and may not make this move at all.
id: 'crew', trainNumber: null, trainIsExtra: false, engineAt: 0, consist: [],
direction: 'west', facing: 'w', position: { at: 'grid', owner: 0, coord: { row: 0, col: 0 } }, movesUsed: 0,
});
s.clock.phase = 'localOps';