v0.4.4 — New Game dialog for the opening hand and the three revenue rates, Yard renamed Interchange, and two movement bugs behind a mirrored consist

This commit is contained in:
Jesse
2026-08-14 22:39:50 -04:00
parent d1314066bd
commit 6655e20ea8
29 changed files with 5450 additions and 3902 deletions
+42 -3
View File
@@ -332,7 +332,20 @@ describe('ABS Signals amend the collision rule', () => {
// ---------------------------------------------------------------------------
describe('one Revenue to EVERY player when a train completes its run', () => {
describe('train revenue per transit — paid to EVERY player when a train completes its run', () => {
/**
* `trainPerTransit` is a dial now, and it defaults to ZERO: at 1 it was worth ~5.4 Revenue against
* a bot mean of 7.0, drowning out the freight and passenger economies the game is about. These
* tests are about the rule, so they set the rate; the last one is about the default.
*/
const paying = (rate = 1, seed = 5): GameState =>
createGame({
id: 'g',
seed,
config: { ...config, houseRules: { revenue: { trainPerTransit: rate } } },
playerNames: ['p'],
});
/** A train sitting on the A/D track, made up and ready to highball. */
const readyToLeave = (s: GameState, direction: 'east' | 'west' = 'east'): string => {
const id = 'leaving';
@@ -364,7 +377,7 @@ describe('one Revenue to EVERY player when a train completes its run', () => {
});
it('pays every player once the train runs off the end of the Division', () => {
const s = game();
const s = paying();
const before = s.players.map((p) => p.revenue);
readyToLeave(s, 'east');
@@ -395,7 +408,7 @@ describe('one Revenue to EVERY player when a train completes its run', () => {
});
it('pays nothing for a local crew, which is switching rather than running', () => {
const s = game();
const s = paying();
const before = s.players[0]!.revenue;
const id = readyToLeave(s, 'east');
s.trays.get(id)!.trainNumber = null;
@@ -404,6 +417,32 @@ describe('one Revenue to EVERY player when a train completes its run', () => {
assert.equal(s.players[0]!.revenue, before, 'a switching crew was paid as though it had completed a run');
});
it('pays at the rate the game was dealt with, and nothing at the default of zero', () => {
// Zero is not "one, suppressed": no revenue event is emitted at all, so the history does not
// fill with "+0 Revenue" for work that did not pay.
const run = (rate: number): { revenue: number; events: number } => {
const s = rate === 0 ? game() : paying(rate);
readyToLeave(s, 'east');
let events = 0;
let done = false;
for (let i = 0; i < 20 && !done; i++) {
s.movedThisPhase = new Set();
s.clock.phase = 'mainline';
const r = advance(s);
events += r.events.filter(
(e) => e.type === 'revenueChanged' && e.reason === 'a train completed its run',
).length;
done = r.events.some((e) => e.type === 'trainCompleted');
}
assert.ok(done, `the train never left the Division at rate ${rate}`);
return { revenue: s.players[0]!.revenue, events };
};
assert.deepEqual(run(0), { revenue: 0, events: 0 }, 'the default paid for a transit');
assert.deepEqual(run(1), { revenue: 1, events: 1 });
assert.deepEqual(run(5), { revenue: 5, events: 1 }, 'the top of the range pays 5 in one event');
});
});
// ---------------------------------------------------------------------------