v0.4.7 — the switching game: track order, the cut on your own card, and four rules
Eight play reports and one design that had been written up and not built. The
through-line is switching: what a card can hold, which end of a train a cut comes
off, which way a train meets cars standing on the line, and what the board and the
log say about all of it.
TRACK ORDER FOR STANDING CARS, AND THE CUT ON YOUR OWN CARD
Two reports turned out to be one root cause. `TrackCard.standing` claimed "in track
order (§A.3)" and had no defined orientation at all, while `CrewTray.consist` does
(nose first, relative to facing) — so every transfer between them was a conversion
nothing performed. §A.3 says what it should be: cars occupy the track "in the same
order they originally held, left-to-right". Left-to-right is west-to-east, and that
is now the defined orientation of `standing` and of an industry track through
`carsOn`. It is the board's orientation, not the train's, so it does not change when
a different train touches the card.
- Setting out is batch-invariant. Four cars at once, four singles and two pairs
parked three different orders, one of them physically impossible. Successive
cuts off the same end stack up towards the engine, so the insertion point is the
train's own place in the row.
- Approaching a cut from either end now mirrors. `couples` is built nearest-first
along the direction of travel and reverses onto the nose, so the farthest car met
ends up nose-most — which is what makes a run-around worth its Move.
- A train no longer drives through its own cut. The walk began at the neighbour of
the start square and never read the start card, so a crew could set cars out and
pull straight away from them. Coupling is mandatory (§A.4) and your own square is
no exception; the cut counts against the four-car limit. Setting out off the end
you are not leaving by still works.
`CrewTray.standingWest` records where a train stands among the cars on its card — a
train may set out off both ends on one square, so which side a cut is on is not
recoverable from the array alone.
On the board, the cut is drawn split at the train — west cars left, east cars right,
engine in the gap — and each car's tooltip says whether it stands ahead of or behind
the engine. The history says which end a cut came off, and a move's button separates
"takes your own boxcar back off this card" from cars found standing on the line.
Decided: taking your own cut back on the square you are standing on is UNDOING the
drop. It is exempt from trains 3/4's per-location freight budget, X13's "drop but not
pick up" and X22's "empties only", and it refunds the budget the drop spent.
Otherwise a legal-looking drop becomes silently one-way.
Measured, 200 paired seeds, developer bot: -0.55 revenue (t = -3.63), freight revenue
1.11 -> 0.56. That cost is the bot's, not the rule's — its trains run engine-first,
so at a stub industry it sets a car out between itself and the only way out, and the
correct play is §A.5's facing-point move, which is the cross-turn planning TODO.md
already records as out of reach of any bot. Filtering self-recoupling moves out of its
options took recoupling from 625 of 1,029 set-outs to 101 of 677, and all 101 that
remain are that case. Read the number as a bot measurement, not a balance one.
THE SUPERINTENDENT'S RULING NAMES THE TRAINS IT IS ABOUT
Reported: the Superintendent could not tell which train he was clearing. The heading
asks the question now — "may Train 6 follow Train 4 onto the same Mainline card?" —
and the trains moved to the FRONT of each button, because the button splits its label
at the first em-dash and showed only the head.
AN INDUSTRY TRACK HOLDS FOUR CARS, LIKE EVERY OTHER CARD
Reported at undo 188: "we wanted to drop two cars, but were only allowed to drop one."
An industry track was built as long as its box count, so a one-box industry had room
for one car. Box count is how much WORK an industry can hold, not how much RAIL it
has. Ordinary track was the other exception, unbounded; both are gone and every card
holds four.
THE FREIGHT AGENT MAY STAGE A LOAD BEFORE THE CAR IS THERE
§6.3 asks nothing of the industry track — the empty car belongs to §9.3's Load the
car, which is the Laborer's action. The gate now lives only there, so cargo can wait
on the dock while the car to ship it in is still being switched in. Nothing can jam:
a load in a green box is waiting, not stuck.
THE TRUCK DOCK UNLOADS, AND BRINGS NOBODY
+1 inbound, no Laborer. It printed +1 outbound and +1 Laborer, which made it a
longer-host-list copy of Forklifts. Beside Packing Sheds it now does nothing at all,
and the hand tooltip says so before it is played.
Also in this release, from the days before: Mainline card tooltips computed from the
crossing rule; an Extra starts from the Division Point its number sends it to; a
modifier's suppressed grant comes back when a Whistle Post is upgraded; the Oil
Refinery and the Grocer's Warehouse ship as well as receive, per the card reference;
and the dormant defences name the attack they answer. `.claude/` is now gitignored —
it holds Claude Code's worktrees, i.e. a second checkout of this repository.
570 tests, typecheck clean. The three published replays were re-recorded twice —
legality changed, so bot play changed. Full detail in CHANGELOG.md.
This commit is contained in:
+176
-47
@@ -7,12 +7,12 @@ import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { applyIntent, check, areaOf, facilityCarTypes, movesFor, reduce } from '../src/engine/apply.ts';
|
||||
import { HAND_LIMIT, INDUSTRY_PROFILES, MOVES_PER_LOCAL_OPS } from '../src/engine/content.ts';
|
||||
import { HAND_LIMIT, INDUSTRY_PROFILES, MAX_CONSIST, MOVES_PER_LOCAL_OPS, officeProfile } from '../src/engine/content.ts';
|
||||
import type { Intent } from '../src/engine/intents.ts';
|
||||
import { legalActions } from '../src/engine/legal.ts';
|
||||
import { createGame } from '../src/engine/setup.ts';
|
||||
import type { CrewTray, GameConfig, GameState, GridCoord, TrackCard } from '../src/engine/state.ts';
|
||||
import { coordKey, railFacingOf, turnOf } from '../src/engine/state.ts';
|
||||
import { carsOn, coordKey, railFacingOf, spaceOn, turnOf } from '../src/engine/state.ts';
|
||||
import { cardDescription, snapshot } from '../src/sim/view.ts';
|
||||
|
||||
const config: GameConfig = {
|
||||
@@ -453,10 +453,9 @@ describe('Freight Agent operations (§6.3)', () => {
|
||||
inboundBox: [],
|
||||
capacity: { outbound: 3, inbound: 0 },
|
||||
menAtWork: [null, null, null],
|
||||
// An empty hopper is SPOTTED. §9.3 refuses to stage cargo against no car — "otherwise you
|
||||
// are just dropping cargo onto the tracks" — so a fixture with a bare track is a facility
|
||||
// the Freight Agent cannot legally touch.
|
||||
industryTrack: { length: 4, cars: [{ type: 'hopper', loaded: false }] },
|
||||
// An empty hopper is SPOTTED, which is what the LABORERS need (§9.3). The Freight Agent
|
||||
// does not: stocking works over a bare track too, and the tests below cover that.
|
||||
industryTrack: { cars: [{ type: 'hopper', loaded: false }] },
|
||||
laborers: 3,
|
||||
porters: 0,
|
||||
usedThisStage: { laborers: 0, porters: 0 },
|
||||
@@ -478,6 +477,38 @@ describe('Freight Agent operations (§6.3)', () => {
|
||||
assert.equal(areaOf(s, 0).grid.get(coordKey(coord))!.facility!.outboundBox.length, 1);
|
||||
});
|
||||
|
||||
it('stocks the green box before the car is spotted (§6.3)', () => {
|
||||
/**
|
||||
* REPORTED FROM PLAY: "A freight agent should be able to load an outbound green box prior to
|
||||
* having the car there that matches the load he's putting in there."
|
||||
*
|
||||
* §6.3 asks for nothing but a car in the Division Yard and room in the box. The empty-car
|
||||
* requirement is §9.3's, on the Laborer who moves the load out of the box — see the test below.
|
||||
* The engine used to hoist it forward onto stocking and so refused the ordinary play of having
|
||||
* the cargo waiting while the car is still being switched in.
|
||||
*/
|
||||
const s = game();
|
||||
const coord = withFacility(s);
|
||||
const f = areaOf(s, 0).grid.get(coordKey(coord))!.facility!;
|
||||
f.industryTrack.cars = [];
|
||||
applyIntent(s, 0, { type: 'localOps.choose', option: 'freightAgent' });
|
||||
const r = applyIntent(s, 0, { type: 'freightAgent.stockOutbound', at: coord, carType: 'hopper' });
|
||||
assert.ok(r.ok, 'staging cargo against a bare industry track is legal');
|
||||
assert.equal(f.outboundBox.length, 1);
|
||||
});
|
||||
|
||||
it('still refuses to stock with no matching loaded car in the Division Yard', () => {
|
||||
// The requirement §6.3 DOES make: the load has to come from somewhere.
|
||||
const s = game();
|
||||
const coord = withFacility(s);
|
||||
s.yards.divisionYard = s.yards.divisionYard.filter((c) => !(c.type === 'hopper' && c.loaded));
|
||||
applyIntent(s, 0, { type: 'localOps.choose', option: 'freightAgent' });
|
||||
assert.equal(
|
||||
check(s, 0, { type: 'freightAgent.stockOutbound', at: coord, carType: 'hopper' }),
|
||||
'NO_SUITABLE_CAR',
|
||||
);
|
||||
});
|
||||
|
||||
it('allows only one Freight Agent operation per Stage', () => {
|
||||
// This is the bottleneck the whole economy rests on (card-reference.md §7).
|
||||
const s = game();
|
||||
@@ -553,7 +584,7 @@ describe('Load/Unload: the four-action freight pipeline (§9.3)', () => {
|
||||
inboundBox: [],
|
||||
capacity: { outbound: 3, inbound: 0 },
|
||||
menAtWork: [{ type: 'hopper', dir: 'out' }, null, null],
|
||||
industryTrack: { length: 4, cars: [{ type: 'hopper', loaded: false }] },
|
||||
industryTrack: { cars: [{ type: 'hopper', loaded: false }] },
|
||||
laborers: 3,
|
||||
porters: 0,
|
||||
usedThisStage: { laborers: 0, porters: 0 },
|
||||
@@ -611,6 +642,46 @@ describe('Load/Unload: the four-action freight pipeline (§9.3)', () => {
|
||||
assert.equal(check(s, 0, { type: 'laborer.advanceLoad', at: coord, box: 2 }), 'BOX_EMPTY');
|
||||
});
|
||||
|
||||
it('holds a staged load in the green box until a car is spotted (§9.3)', () => {
|
||||
/**
|
||||
* THE OTHER HALF OF THE STOCKING RULE. The Freight Agent may stage a load with nothing on the
|
||||
* industry track (§6.3), but "Load the car" requires "*a load in the Green Loading Box AND an
|
||||
* empty car of the required type on the industry's track*" — so the load simply waits in the
|
||||
* box, which is a wait and not a jam: only a load on MEN | AT | WORK locks the track.
|
||||
*/
|
||||
const s = game();
|
||||
const coord = facilityWithLoad(s);
|
||||
const f = areaOf(s, 0).grid.get(coordKey(coord))!.facility!;
|
||||
f.menAtWork = [null, null, null];
|
||||
f.outboundBox = [{ type: 'hopper', loaded: true }];
|
||||
f.industryTrack.cars = [];
|
||||
|
||||
assert.equal(
|
||||
check(s, 0, { type: 'laborer.startLoad', at: coord }),
|
||||
'NO_EMPTY_CAR_SPOTTED',
|
||||
'no car on the track — the Laborers cannot start it',
|
||||
);
|
||||
|
||||
// A crew sets an empty hopper out, and the same staged load may now start down the sign.
|
||||
f.industryTrack.cars = [{ type: 'hopper', loaded: false }];
|
||||
assert.equal(check(s, 0, { type: 'laborer.startLoad', at: coord }), null);
|
||||
const r = applyIntent(s, 0, { type: 'laborer.startLoad', at: coord });
|
||||
assert.ok(r.ok);
|
||||
assert.equal(f.outboundBox.length, 0, 'the load left the green box');
|
||||
assert.notEqual(f.menAtWork![0], null, 'and is on MEN');
|
||||
});
|
||||
|
||||
it('will not start a load onto a spotted car of the wrong type', () => {
|
||||
// Strict matching: a hopper load cannot be swapped onto a tank (§9.3).
|
||||
const s = game();
|
||||
const coord = facilityWithLoad(s);
|
||||
const f = areaOf(s, 0).grid.get(coordKey(coord))!.facility!;
|
||||
f.menAtWork = [null, null, null];
|
||||
f.outboundBox = [{ type: 'hopper', loaded: true }];
|
||||
f.industryTrack.cars = [{ type: 'tank', loaded: false }];
|
||||
assert.equal(check(s, 0, { type: 'laborer.startLoad', at: coord }), 'NO_EMPTY_CAR_SPOTTED');
|
||||
});
|
||||
|
||||
it('takes four Laborer actions in total for one point', () => {
|
||||
// The asymmetry the whole economy is calibrated against (card-reference.md §2).
|
||||
const s = game();
|
||||
@@ -694,7 +765,7 @@ describe('legalActions shares its rules with apply (component 6)', () => {
|
||||
inboundBox: [],
|
||||
capacity: { outbound: 3, inbound: 0 },
|
||||
menAtWork: [null, null, null],
|
||||
industryTrack: { length: 4, cars: [] },
|
||||
industryTrack: { cars: [] },
|
||||
laborers: 3,
|
||||
porters: 0,
|
||||
usedThisStage: { laborers: 0, porters: 0 },
|
||||
@@ -752,7 +823,7 @@ describe('an industry handles every commodity its profile names (regression)', (
|
||||
outboundBox: [], inboundBox: [],
|
||||
capacity: { outbound: p.baseOut, inbound: p.baseIn },
|
||||
menAtWork: [null, null, null],
|
||||
industryTrack: { length: 2, cars: [] },
|
||||
industryTrack: { cars: [] },
|
||||
laborers: p.baseLoaders, porters: 0,
|
||||
usedThisStage: { laborers: 0, porters: 0 },
|
||||
} as never;
|
||||
@@ -792,7 +863,7 @@ describe('a Modifier only goes beside a host that can use it (regression)', () =
|
||||
allows: { outbound: true, inbound: false },
|
||||
outboundBox: [], inboundBox: [], capacity: { outbound: 1, inbound: 0 },
|
||||
menAtWork: [null, null, null],
|
||||
industryTrack: { length: 2, cars: [] },
|
||||
industryTrack: { cars: [] },
|
||||
laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 },
|
||||
},
|
||||
} as never);
|
||||
@@ -869,37 +940,40 @@ describe("a Modifier grants only what its host's flow can use", () => {
|
||||
outboundBox: [], inboundBox: [],
|
||||
capacity: { outbound: out ? 1 : 0, inbound: into ? 1 : 0 },
|
||||
menAtWork: [null, null, null],
|
||||
industryTrack: { length: 1, cars: [] },
|
||||
industryTrack: { cars: [] },
|
||||
laborers: 1, porters: 0, usedThisStage: { laborers: 0, porters: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
/** Plays an Ice House next to the facility at `at`, and returns that facility. */
|
||||
const iceHouseBeside = (kind: string, out: boolean, into: boolean) => {
|
||||
/** Plays `modifier` next to the facility at `at`, and returns the facility and its card. */
|
||||
const modifierBeside = (modifier: string, kind: string, out: boolean, into: boolean) => {
|
||||
const s = game();
|
||||
const area = areaOf(s, 0);
|
||||
const at = { row: -1, col: 4 };
|
||||
area.grid.set(coordKey(at), withFacility(kind, out, into) as never);
|
||||
applyIntent(s, 0, { type: 'localOps.choose', option: 'draw' });
|
||||
|
||||
const ice = [...s.cards.entries()].find(
|
||||
([, c]) => c.kind.kind === 'modifier' && c.kind.modifier === 'iceHouse',
|
||||
const card = [...s.cards.entries()].find(
|
||||
([, c]) => c.kind.kind === 'modifier' && c.kind.modifier === modifier,
|
||||
);
|
||||
assert.ok(ice, 'no Ice House in the deck');
|
||||
s.decks.hands.set(0, [ice![0]]);
|
||||
assert.ok(card, `no ${modifier} in the deck`);
|
||||
s.decks.hands.set(0, [card![0]]);
|
||||
const r = applyIntent(s, 0, {
|
||||
type: 'card.play', cardId: ice![0], placement: { row: at.row, col: at.col + 1 },
|
||||
type: 'card.play', cardId: card![0], placement: { row: at.row, col: at.col + 1 },
|
||||
});
|
||||
assert.ok(r.ok, `the Ice House should be playable beside a ${kind}`);
|
||||
return area.grid.get(coordKey(at))!.facility!;
|
||||
assert.ok(r.ok, `the ${modifier} should be playable beside a ${kind}`);
|
||||
const host = area.grid.get(coordKey(at))!;
|
||||
return { f: host.facility!, card: host };
|
||||
};
|
||||
|
||||
const iceHouseBeside = (kind: string, out: boolean, into: boolean) =>
|
||||
modifierBeside('iceHouse', kind, out, into).f;
|
||||
|
||||
it('gives a Packing Sheds the outbound slot its card prints', () => {
|
||||
// The host ships, so every printed grant lands. This is the control for the case below.
|
||||
const f = iceHouseBeside('packingSheds', true, false);
|
||||
assert.equal(f.capacity.outbound, 2, 'the outbound slot did not arrive');
|
||||
assert.equal(f.laborers, 2, 'the laborer did not arrive');
|
||||
assert.equal(f.industryTrack.length, 2, 'the siding should lengthen with the slot it serves');
|
||||
});
|
||||
|
||||
it("drops the outbound slot on a Grocer's Warehouse, which only receives", () => {
|
||||
@@ -910,23 +984,47 @@ describe("a Modifier grants only what its host's flow can use", () => {
|
||||
assert.equal(f.laborers, 2, 'the laborer should still arrive — Laborers have no direction');
|
||||
assert.equal(f.capacity.outbound, 0, 'outbound capacity was credited to a facility that cannot ship');
|
||||
assert.equal(f.capacity.inbound, 1, 'the modifier must not silently move the grant to the other direction');
|
||||
assert.equal(f.industryTrack.length, 1, 'the siding lengthened for a slot that does not exist');
|
||||
});
|
||||
|
||||
it('does not give a Passenger Facility a siding by way of a Modifier', () => {
|
||||
it('gives a Truck Dock a second red box and not one more car length', () => {
|
||||
/**
|
||||
* The case as it was described: "if I have a Grocer's Warehouse that receives, it has one red
|
||||
* box. If it has a Truck Dock next to it, that should give it a second red box. There is no
|
||||
* change to the number of cars that are allowed to be dropped here."
|
||||
*
|
||||
* Both halves matter, and the second half is the one that used to be wrong: the Truck Dock's
|
||||
* inbound grant lengthened the industry track along with the box, because the track's length WAS
|
||||
* the box count. The boxes are how much work the industry can hold; the card holds four cars
|
||||
* either way.
|
||||
*/
|
||||
const { f, card } = modifierBeside('truckDock', 'grocersWarehouse', true, true);
|
||||
assert.equal(f.capacity.inbound, 2, 'the Truck Dock did not add the red box it prints');
|
||||
assert.equal(f.capacity.outbound, 1, 'the Truck Dock added a green box, which it does not print');
|
||||
|
||||
assert.equal(
|
||||
spaceOn(card as never), MAX_CONSIST,
|
||||
'a Modifier changed how many cars may be set out at the industry it stands beside',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not give a Passenger Facility an industry track by way of a Modifier', () => {
|
||||
// FOUND by rendering 12,000 frames of real games rather than a fixture: a Waiting Area beside the
|
||||
// Office lengthened `industryTrack`, and the board then drew the Depot a siding square to spot a
|
||||
// car on. The same phantom siding the renderer had just been fixed for, arriving by another door.
|
||||
// Office lengthened `industryTrack`, and the board then drew the Depot a square to spot a car on.
|
||||
// A Waiting Area's "+1" is a passenger slot; passengers board off the platform.
|
||||
//
|
||||
// The length field is gone now — an industry track holds four cars like any other card, so there
|
||||
// was never a number for a Modifier to raise. What is still worth pinning down is the property
|
||||
// that bug was really about: an Office keeps its cars in `standing` and has no industry track for
|
||||
// a Modifier to hand it, no matter what the Modifier prints.
|
||||
const s = game();
|
||||
const area = areaOf(s, 0);
|
||||
applyIntent(s, 0, { type: 'localOps.choose', option: 'draw' });
|
||||
|
||||
// Forced open so this tests the freight/passenger distinction rather than the `allows` gating —
|
||||
// the Office starts as a Whistle Post, which is not a Passenger Facility and takes nothing.
|
||||
const office = area.grid.get(coordKey(area.officeCoord))!.facility!;
|
||||
const officeCard = area.grid.get(coordKey(area.officeCoord))!;
|
||||
const office = officeCard.facility!;
|
||||
office.allows = { outbound: true, inbound: true };
|
||||
const before = office.industryTrack.length;
|
||||
|
||||
const waiting = [...s.cards.entries()].find(
|
||||
([, c]) => c.kind.kind === 'modifier' && c.kind.modifier === 'waitingArea',
|
||||
@@ -941,37 +1039,68 @@ describe("a Modifier grants only what its host's flow can use", () => {
|
||||
assert.equal(office.capacity.outbound, 1, 'the passenger slot should still be granted');
|
||||
assert.equal(office.porters, 1, 'the porter should still be granted');
|
||||
assert.equal(
|
||||
office.industryTrack.length, before,
|
||||
carsOn(officeCard), officeCard.standing,
|
||||
'a Modifier gave a Passenger Facility an industry track to spot cars on',
|
||||
);
|
||||
});
|
||||
|
||||
it('says which bonus was dropped, and why', () => {
|
||||
// The number genuinely does not move; what was missing was the reason.
|
||||
it('suppresses a grant the host cannot use — and gives it back when it can', () => {
|
||||
/**
|
||||
* REPORTED from play: "Restaurant attached to a whistle stop, then upgrade to depot — depot only
|
||||
* shows one green / one red box. I expected two, because Restaurant increases outbound by one."
|
||||
*
|
||||
* `hosts: ['office']` includes a Whistle Post, which is NOT a Passenger Facility, so the +1
|
||||
* outbound is genuinely unusable while the Office is a Whistle Post — suppressing it is right,
|
||||
* and saying so is what the panel is for. Losing it FOREVER was the bug: the upgrade applied
|
||||
* only the difference between two tiers and knew nothing about what had been discarded.
|
||||
*
|
||||
* This used to be written against an Ice House on a Grocer's Warehouse. That case no longer
|
||||
* suppresses anything, because the Grocer's is a both-direction facility — which was the other
|
||||
* half of the same report.
|
||||
*/
|
||||
const s = game();
|
||||
const area = areaOf(s, 0);
|
||||
const at = { row: -1, col: 4 };
|
||||
area.grid.set(coordKey(at), withFacility('grocersWarehouse', false, true) as never);
|
||||
const office = area.grid.get(coordKey(area.officeCoord))!;
|
||||
assert.equal(office.facility!.allows.outbound, false, 'a Whistle Post is not a Passenger Facility');
|
||||
|
||||
applyIntent(s, 0, { type: 'localOps.choose', option: 'draw' });
|
||||
const ice = [...s.cards.entries()].find(
|
||||
([, c]) => c.kind.kind === 'modifier' && c.kind.modifier === 'iceHouse',
|
||||
const restaurant = [...s.cards.entries()].find(
|
||||
([, c]) => c.kind.kind === 'modifier' && c.kind.modifier === 'restaurant',
|
||||
)!;
|
||||
|
||||
// Before it is played, the card in hand warns which of its two printed hosts cannot use it.
|
||||
const what = cardDescription(s, ice[0]);
|
||||
assert.match(what, /only receives/, 'the hand card does not warn about the dead half');
|
||||
|
||||
s.decks.hands.set(0, [ice[0]]);
|
||||
s.decks.hands.set(0, [restaurant[0]]);
|
||||
assert.ok(applyIntent(s, 0, {
|
||||
type: 'card.play', cardId: ice[0], placement: { row: at.row, col: at.col + 1 },
|
||||
}).ok);
|
||||
type: 'card.play',
|
||||
cardId: restaurant[0],
|
||||
placement: { row: area.officeCoord.row - 1, col: area.officeCoord.col },
|
||||
}).ok, 'the Restaurant could not be played beside the Office');
|
||||
|
||||
const fv = snapshot(s, [], null).facilities.find((v) => v.name.includes('Grocer'));
|
||||
assert.ok(fv, "the Grocer's Warehouse is missing from the panel");
|
||||
const f = office.facility!;
|
||||
assert.equal(f.capacity.outbound, 0, 'a Whistle Post gained a passenger slot it cannot have');
|
||||
assert.equal(f.porters, 1, 'the porter has no direction gate and should have landed');
|
||||
|
||||
const fv = snapshot(s, [], null).facilities.find((v) => v.name.includes('Whistle'));
|
||||
assert.ok(fv, 'the Office is missing from the panel');
|
||||
assert.equal(fv!.suppressed.length, 1, 'the dropped bonus is not reported');
|
||||
assert.match(fv!.suppressed[0]!, /Ice House/);
|
||||
// And the modifier is now recorded against the host it served, which never used to happen.
|
||||
assert.deepEqual(fv!.modifiers, ['Ice House'], 'the modifier is not listed against its host');
|
||||
assert.match(fv!.suppressed[0]!, /Restaurant/);
|
||||
assert.deepEqual(fv!.modifiers, ['Restaurant'], 'the modifier is not listed against its host');
|
||||
|
||||
// Upgrading makes it a Passenger Facility, and the slot the Restaurant always printed arrives.
|
||||
reduce(s, { type: 'officeUpgraded', player: 0, from: 'whistlePost', to: 'depot' });
|
||||
assert.equal(f.allows.outbound, true);
|
||||
assert.equal(
|
||||
f.capacity.outbound,
|
||||
officeProfile('depot').passengerOut + 1,
|
||||
"the Restaurant's slot did not come back when the Office could finally use it",
|
||||
);
|
||||
|
||||
// And it is paid ONCE: a second upgrade must not grant it again.
|
||||
const afterDepot = f.capacity.outbound;
|
||||
reduce(s, { type: 'officeUpgraded', player: 0, from: 'depot', to: 'station' });
|
||||
assert.equal(
|
||||
f.capacity.outbound,
|
||||
afterDepot + (officeProfile('station').passengerOut - officeProfile('depot').passengerOut),
|
||||
'the Restaurant was paid a second time on the next upgrade',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user