v0.1.0 — undo, the crew's reach on the board, workers on the card, and four engine bugs
Fifteen items from two playtest sessions. Three that read as drawing faults were engine bugs: cars could be added to a train that was not being made up (50 offers in 8 games), the make-up panel merged two trains and could couple a car to the wrong one, and an Office upgrade silently deleted what a Modifier had added. A fourth was a sentinel inside a coordinate's own value range — a Mainline placement travelling as row -1, which is an ordinary district row. Trains are now drawn the way they stand: west on the left, nose toward the way the engine faces, on both the district card and the Division chip. Undo steps back through the game by replaying the save without its last intent. The switching walk keeps its rejections, so the board can say why a square is not offered. Laborers and Porters are on the card, and the rule that a district only grows outwards is finally written down. Versions start here: third digit for fixes, second for a feature set, 1.0 for a release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GgtkX8JnvKa8y2tuJ8aQf4
This commit is contained in:
@@ -991,3 +991,93 @@ describe('the Crew Tray is a train, and must be made up to leave (§8.2, Appendi
|
||||
assert.equal(check(s, 0, { type: 'switch.dropCars', trayId: id, count: 2 }), 'CONSIST_EMPTY');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('only the train being made up may take cars (§7)', () => {
|
||||
/** A tray with a short consist, placed wherever the test needs it. */
|
||||
function trayAt(s: GameState, position: CrewTray['position']): void {
|
||||
s.clock.phase = 'newTrain';
|
||||
s.trays.set('t', {
|
||||
id: 't', trainNumber: 9, trainIsExtra: false, engineAt: 0,
|
||||
consist: [], direction: 'west', position, movesUsed: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const addHopper: Intent = { type: 'newTrain.placeCar', trayId: 't', carType: 'hopper', loaded: true };
|
||||
|
||||
it('accepts a car for a train standing at a Division Point', () => {
|
||||
const s = game();
|
||||
trayAt(s, { at: 'divisionPoint', side: 'east' });
|
||||
assert.equal(check(s, 0, addHopper), null);
|
||||
});
|
||||
|
||||
it('refuses to load a train standing in the district', () => {
|
||||
// The New Train phase fills the train it is WAITING on — `trainNeedingCars` requires a Division
|
||||
// Point. `check` did not, so the Division Yard would hand cars to a train sitting on a siding
|
||||
// in your own district: cars appeared on a train nobody was making up.
|
||||
const s = game();
|
||||
trayAt(s, { at: 'grid', owner: 0, coord: at(0, 1) });
|
||||
assert.equal(check(s, 0, addHopper), 'NOT_BEING_MADE_UP');
|
||||
});
|
||||
|
||||
it('refuses to load a train out on the Mainline', () => {
|
||||
const s = game();
|
||||
trayAt(s, { at: 'mainline', index: 1 });
|
||||
assert.equal(check(s, 0, addHopper), 'NOT_BEING_MADE_UP');
|
||||
});
|
||||
|
||||
it('refuses to load a train whose consist is already what its card calls for', () => {
|
||||
// Train 9 is freight 3 + caboose 1. A full train is not the one being made up, even standing
|
||||
// at a Division Point — otherwise the phase and `check` disagree about who is being filled.
|
||||
const s = game();
|
||||
trayAt(s, { at: 'divisionPoint', side: 'east' });
|
||||
const tray = s.trays.get('t')!;
|
||||
tray.consist = [
|
||||
{ type: 'hopper', loaded: true },
|
||||
{ type: 'hopper', loaded: true },
|
||||
{ type: 'hopper', loaded: true },
|
||||
{ type: 'caboose', loaded: true },
|
||||
];
|
||||
assert.equal(check(s, 0, addHopper), 'NOT_BEING_MADE_UP');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('an Office upgrade keeps what Modifiers added (§9)', () => {
|
||||
it('does not wipe a Restaurant when the Depot becomes a Station', () => {
|
||||
// The upgrade wrote the tier's printed numbers straight over the facility, so a Restaurant
|
||||
// played beside a Depot — +1 passenger out, +1 porter — was silently deleted by the next
|
||||
// upgrade. The card had been spent and the effect vanished with no message.
|
||||
const s = game();
|
||||
const office = () => areaOf(s, 0).grid.get(coordKey(areaOf(s, 0).officeCoord))!.facility!;
|
||||
|
||||
const play = (tier: 'depot' | 'station'): void => {
|
||||
for (const [id, card] of s.cards) {
|
||||
if (card.kind.kind !== 'office' || card.kind.tier !== tier) continue;
|
||||
s.decks.hands.set(0, [id]);
|
||||
s.turn.option = null;
|
||||
applyIntent(s, 0, { type: 'localOps.choose', option: 'draw' });
|
||||
const r = applyIntent(s, 0, { type: 'card.play', cardId: id });
|
||||
assert.ok(r.ok, `${tier} upgrade should apply`);
|
||||
return;
|
||||
}
|
||||
throw new Error(`no ${tier} card`);
|
||||
};
|
||||
|
||||
play('depot');
|
||||
const base = { out: office().capacity.outbound, porters: office().porters };
|
||||
|
||||
// A Restaurant beside the Office: +1 passenger out, +1 porter.
|
||||
office().capacity.outbound += 1;
|
||||
office().porters += 1;
|
||||
|
||||
play('station');
|
||||
|
||||
const station = { out: 2, porters: 2 }; // Station's printed numbers
|
||||
assert.equal(office().capacity.outbound, station.out + 1, 'the Modifier survives the upgrade');
|
||||
assert.equal(office().porters, station.porters + 1, 'the porter survives the upgrade');
|
||||
assert.ok(office().capacity.outbound > base.out, 'and the upgrade still raised the tier');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user