Implement nose coupling so run-arounds work, and stop coupling wiping the district
This commit is contained in:
@@ -7,6 +7,7 @@ import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import type {
|
||||
GameState,
|
||||
GridCoord,
|
||||
OfficeArea,
|
||||
RollingStock,
|
||||
@@ -14,6 +15,8 @@ import type {
|
||||
TurnoutOrientation,
|
||||
} from '../src/engine/state.ts';
|
||||
import { coordKey } from '../src/engine/state.ts';
|
||||
import { createGame } from '../src/engine/setup.ts';
|
||||
import { applyIntent, areaOf } from '../src/engine/apply.ts';
|
||||
import type { MoveContext, Occupancy, Port } from '../src/engine/track.ts';
|
||||
import {
|
||||
allReachable,
|
||||
@@ -95,6 +98,27 @@ const lockedFacility = (): TrackCard => ({
|
||||
|
||||
const car = (): RollingStock => ({ type: 'boxcar', loaded: false });
|
||||
|
||||
function straightCard(): TrackCard {
|
||||
return {
|
||||
geometry: { kind: 'track', geometry: 'straight', axis: 'ew' },
|
||||
baseOperationalRail: true, standing: [], facility: null, modifiers: [], enhancements: [],
|
||||
};
|
||||
}
|
||||
|
||||
/** A minimal game wrapped around a prepared Office Area, for engine-level switching tests. */
|
||||
function gameWith(area: OfficeArea): GameState {
|
||||
const s = createGame({
|
||||
id: 'g', seed: 5,
|
||||
config: {
|
||||
mode: 'solitaire', victory: 'highestAfterDays', length: 'standard',
|
||||
optionalRules: { reducedVisibility: false, sisterTrains: false, employeeRotation: false, emergencyToolbox: false },
|
||||
},
|
||||
playerNames: ['p'],
|
||||
});
|
||||
s.officeAreas.set(0, area);
|
||||
return s;
|
||||
}
|
||||
|
||||
function areaFrom(cards: Record<string, TrackCard>, officeCoord: GridCoord): OfficeArea {
|
||||
const grid = new Map<string, TrackCard>();
|
||||
for (const [k, v] of Object.entries(cards)) grid.set(k, v);
|
||||
@@ -503,3 +527,43 @@ describe('curves are arcs, and arcs make sidings possible', () => {
|
||||
assert.ok(seen.has('0,1'), 'the siding does not rejoin the Running Track');
|
||||
});
|
||||
});
|
||||
|
||||
describe('coupling lifts only the cars the crew ran over', () => {
|
||||
it('leaves cars standing elsewhere in the district alone', () => {
|
||||
// The reducer cleared EVERY card in the Office Area on any coupling, so picking up one boxcar
|
||||
// deleted every car standing anywhere — including loads worked over several Stages onto an
|
||||
// industry track the crew never went near.
|
||||
const grid: Record<string, TrackCard> = {
|
||||
'0,-1': straightCard(),
|
||||
'0,0': straightCard(),
|
||||
'0,1': straightCard(),
|
||||
'0,2': straightCard(),
|
||||
};
|
||||
grid['0,-1']!.standing.push({ type: 'boxcar', loaded: false }); // on the path
|
||||
grid['0,2']!.standing.push({ type: 'hopper', loaded: true }); // nowhere near it
|
||||
|
||||
const area = areaFrom(grid, { row: 0, col: 0 });
|
||||
const s = gameWith(area);
|
||||
s.trays.set('crew', {
|
||||
id: 'crew', trainNumber: 1, trainIsExtra: false, engineFront: true, consist: [],
|
||||
direction: 'west', facing: 'w', position: { at: 'grid', owner: 0, coord: { row: 0, col: 0 } }, movesUsed: 0,
|
||||
});
|
||||
s.clock.phase = 'localOps';
|
||||
s.clock.currentActor = 0;
|
||||
s.turn.option = 'switch';
|
||||
s.turn.movesRemaining = 6;
|
||||
|
||||
const r = applyIntent(s, 0, { type: 'switch.move', trayId: 'crew', to: { row: 0, col: -1 }, reverse: false });
|
||||
assert.ok(r.ok, 'the move should be legal');
|
||||
assert.deepEqual(
|
||||
s.trays.get('crew')!.consist.map((c: RollingStock) => c.type),
|
||||
['boxcar'],
|
||||
'the crew should have picked up the car it ran over',
|
||||
);
|
||||
assert.equal(
|
||||
areaOf(s, 0).grid.get('0,2')!.standing.length,
|
||||
1,
|
||||
'a car standing off the path was deleted by an unrelated coupling',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user