v0.6.0 — saves survive a release, Employee Rotation is real, and the lobby
asks what game you want Three queued items. The last matters most. A RELEASE NO LONGER DESTROYS EVERY GAME IN PROGRESS. Four consecutive releases killed every game on the box, one of them a release that changed only how the board is drawn. The reasoning behind the refusal was always right — a move legal under old rules may not be legal under new ones, and half-replaying a save is worse than refusing it. The TEST was wrong: it compared engineVersion for exact equality, and that stamp is the package version, which moves for a CSS fix. Whether a save still replays has an exact answer, so it is now asked directly. loadGame reads the file and judges nothing; tryResumeSession replays the intents and reports the first one the engine refuses. A save stamped with a version this server has never run resumes fine provided its moves replay — verified against a file hand-stamped 0.4.9-ancient. One that genuinely does not replay is still refused, but the log names the move rather than two version strings: "move 3 of 8 (localOps.choose) is rejected by the current rules with OPTION_ALREADY_CHOSEN". fromMultiplayerSave had to stop lying first. It has always stopped at the first unacceptable intent and done so in silence, which was survivable only because the version gate meant a doomed replay was never attempted. Now that the replay IS the check, it returns where it stopped and why. Deliberately not done: resuming a partly-replayable game at its last good move. That silently rewinds a game to a position nobody played to while every browser holding a later Frame carries on unaware. Refusing leaves the file intact, so putting the previous version back still recovers it. EMPLOYEE ROTATION IS IMPLEMENTED, SISTER TRAINS IS DELETED. Two of the four optional-rule flags were read by nothing at all. Employee Rotation is four lines in advance.ts, because the seat/player split (D9) exists for precisely this rule: seating is the only thing that moves, so Revenue, hands, the Superintendent and whose turn it is travel with the player, and the Office, district, grid and any trains standing in it stay with the chair. Inheriting the district you move into is the point of the rule, not a side effect. "Left" is seat + 1, matching playerLeftOf. Sister Trains is deleted rather than built: Q9 records that the Second Section card supersedes it, and that card exists, so the flag was a toggle for a rule the game no longer has. THE LOBBY ASKS WHAT GAME YOU WANT TO PLAY. Creating a game asked for a name, a mode and a table size; every other dial was hardcoded. A Game settings block now carries the same set the solitaire dialog does — seed, starting hand, the three revenue rates, Days, the combined-Revenue floor, both collision caps, the opponent-card toggle — plus the three surviving optional rules. Mode and table size set the defaults and everything stays editable. The seed is honoured, so a game can be reproduced or compared. Verified: 682 tests pass (679 + 3). The rotation tests were mutation-checked both ways — disabling the rotation and turning the table the wrong way each fail the suite. Live: a save stamped 0.4.9-ancient resumed, an injected illegal move was refused by name, and a create with every dial set to a non-default value came back out of game.json with all of them intact, including seed 777. Two of my own assertions were wrong on the way and the tests caught them: the Fedora legitimately passes at Stage 12 (§5) so it cannot be compared against its own earlier value, and dispatchUsedToday is cleared at every Day boundary so it cannot mark a district.
This commit is contained in:
@@ -25,7 +25,6 @@ const baseConfig = (over: Partial<GameConfig> = {}): GameConfig => ({
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: {
|
||||
reducedVisibility: false,
|
||||
sisterTrains: false,
|
||||
employeeRotation: false,
|
||||
emergencyToolbox: false,
|
||||
},
|
||||
|
||||
@@ -24,7 +24,6 @@ const config: GameConfig = {
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: {
|
||||
reducedVisibility: false,
|
||||
sisterTrains: false,
|
||||
employeeRotation: false,
|
||||
emergencyToolbox: false,
|
||||
},
|
||||
|
||||
@@ -25,7 +25,6 @@ const config: GameConfig = {
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: {
|
||||
reducedVisibility: false,
|
||||
sisterTrains: false,
|
||||
employeeRotation: false,
|
||||
emergencyToolbox: false,
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@ const config: GameConfig = {
|
||||
maxCollisionsPerDay: 0,
|
||||
maxCollisionsTotal: 0,
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: { reducedVisibility: false, sisterTrains: false, employeeRotation: false, emergencyToolbox: false },
|
||||
optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false },
|
||||
};
|
||||
const game = (seed = 5): GameState => createGame({ id: 'g', seed, config, playerNames: ['p'] });
|
||||
const at = (row: number, col: number): GridCoord => ({ row, col });
|
||||
|
||||
@@ -48,6 +48,9 @@ const KNOWN_UNREDUCED = [
|
||||
'dispatchBonusUsed',
|
||||
'expediteFault',
|
||||
'phaseBegan',
|
||||
// Employee Rotation moves `seating` in the phase driver and then describes what it did, which is
|
||||
// the pattern every entry on this list follows.
|
||||
'seatsRotated',
|
||||
'stageBegan',
|
||||
'trainArrived',
|
||||
'trainCompleted',
|
||||
|
||||
@@ -17,7 +17,6 @@ const config: GameConfig = {
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: {
|
||||
reducedVisibility: false,
|
||||
sisterTrains: false,
|
||||
employeeRotation: false,
|
||||
emergencyToolbox: false,
|
||||
},
|
||||
|
||||
@@ -36,7 +36,6 @@ const config: GameConfig = {
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: {
|
||||
reducedVisibility: false,
|
||||
sisterTrains: false,
|
||||
employeeRotation: false,
|
||||
emergencyToolbox: false,
|
||||
},
|
||||
|
||||
@@ -37,7 +37,7 @@ const config: GameConfig = {
|
||||
maxCollisionsPerDay: 0,
|
||||
maxCollisionsTotal: 0,
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: { reducedVisibility: false, sisterTrains: false, employeeRotation: false, emergencyToolbox: false },
|
||||
optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false },
|
||||
};
|
||||
const game = (seed = 5): GameState => createGame({ id: 'g', seed, config, playerNames: ['p'] });
|
||||
const at = (row: number, col: number): GridCoord => ({ row, col });
|
||||
@@ -737,7 +737,7 @@ describe('regions on a Mainline card (§2.1, §8.2)', () => {
|
||||
seed: 4,
|
||||
config: {
|
||||
mode: 'solitaire', days: 5, minCombinedRevenue: 0, maxCollisionsPerDay: 0, maxCollisionsTotal: 0, pvpCardsAllowed: false,
|
||||
optionalRules: { reducedVisibility: false, sisterTrains: false, employeeRotation: false, emergencyToolbox: false },
|
||||
optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false },
|
||||
},
|
||||
playerNames: ['Solitaire'],
|
||||
});
|
||||
@@ -800,7 +800,7 @@ describe('Q13 — a train that catches the one ahead runs into it', () => {
|
||||
id: 'rear', seed: 3,
|
||||
config: {
|
||||
mode: 'solitaire', days: 5, minCombinedRevenue: 0, maxCollisionsPerDay: 0, maxCollisionsTotal: 0, pvpCardsAllowed: false,
|
||||
optionalRules: { reducedVisibility: false, sisterTrains: false, employeeRotation: false, emergencyToolbox: false },
|
||||
optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false },
|
||||
},
|
||||
playerNames: ['bot'],
|
||||
});
|
||||
|
||||
@@ -12,14 +12,14 @@ import assert from 'node:assert/strict';
|
||||
|
||||
import { advance, pump } from '../src/engine/advance.ts';
|
||||
import { applyIntent, areaAtSeat, areaOf } from '../src/engine/apply.ts';
|
||||
import { STAGES_PER_SHIFT, crewTrayCount } from '../src/engine/content.ts';
|
||||
import { STAGES_PER_DAY, STAGES_PER_SHIFT, crewTrayCount } from '../src/engine/content.ts';
|
||||
import { createGame } from '../src/engine/setup.ts';
|
||||
import type { GameConfig, GameState, PlayerIndex } from '../src/engine/state.ts';
|
||||
import { coordKey, playerAtSeat, playerLeftOf, seatOf, subdivisions } from '../src/engine/state.ts';
|
||||
import { developerBot, playGame } from '../src/sim/bot.ts';
|
||||
import { snapshot } from '../src/sim/view.ts';
|
||||
import { divisionSvg } from '../src/sim/board-svg.ts';
|
||||
import { impediments } from '../src/sim/narrate.ts';
|
||||
import { impediments, narrate } from '../src/sim/narrate.ts';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { actionMenu } from '../src/web/game.ts';
|
||||
@@ -37,7 +37,7 @@ const competitive: GameConfig = {
|
||||
maxCollisionsPerDay: 0,
|
||||
maxCollisionsTotal: 0,
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: { reducedVisibility: false, sisterTrains: false, employeeRotation: false, emergencyToolbox: false },
|
||||
optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false },
|
||||
};
|
||||
|
||||
const game = (players: number, seed = 4242): GameState =>
|
||||
@@ -794,3 +794,81 @@ describe('the map says whose railroad is whose', () => {
|
||||
assert.equal(divisionSvg(f.division).includes('bs-owner'), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Employee Rotation (Appendix B)', () => {
|
||||
/**
|
||||
* Straight to the Day boundary, which is the only moment a rotation happens — the same shortcut
|
||||
* `advance.test.ts` uses to roll the clock over without playing twelve Stages of real turns.
|
||||
*/
|
||||
const atDayEnd = (on: boolean): GameState => {
|
||||
const s = createGame({
|
||||
id: 'rot',
|
||||
seed: 4242,
|
||||
config: { ...competitive, optionalRules: { ...competitive.optionalRules, employeeRotation: on } },
|
||||
playerNames: ['Alice', 'Bob', 'Carol'],
|
||||
});
|
||||
s.clock.stage = STAGES_PER_DAY;
|
||||
s.clock.phase = 'shiftChange';
|
||||
return s;
|
||||
};
|
||||
|
||||
it('is off unless asked for — the clock alone must not move anybody', () => {
|
||||
const s = atDayEnd(false);
|
||||
const before = [...s.seating];
|
||||
advance(s);
|
||||
assert.equal(s.clock.day, 2, 'the clock did not roll over');
|
||||
assert.deepEqual(s.seating, before, 'seats moved with the rule switched off');
|
||||
});
|
||||
|
||||
it('moves every player one chair left at the Day boundary', () => {
|
||||
const s = atDayEnd(true);
|
||||
const before = [...s.seating];
|
||||
advance(s);
|
||||
assert.equal(s.clock.day, 2);
|
||||
// "One chair to the left" is seat + 1, the direction `playerLeftOf` already turns the table.
|
||||
const expected = before.map((_, seat, all) => all[(seat - 1 + all.length) % all.length]!);
|
||||
assert.deepEqual(s.seating, expected);
|
||||
// Everyone moved, and nobody was lost or duplicated on the way round.
|
||||
assert.deepEqual([...s.seating].sort(), [...before].sort());
|
||||
assert.notDeepEqual(s.seating, before);
|
||||
});
|
||||
|
||||
it('takes your points and the Fedora with you, and leaves the district behind', () => {
|
||||
const s = atDayEnd(true);
|
||||
const traveller = 1 as PlayerIndex;
|
||||
s.players[traveller]!.revenue = 17;
|
||||
const seatBefore = seatOf(s, traveller);
|
||||
/**
|
||||
* The Fedora is compared against the SAME game with the rule off, not against its own value
|
||||
* before the advance — Stage 12 is a shift change (§5), so it passes here anyway for reasons
|
||||
* that have nothing to do with rotation. What matters is that moving the chairs does not move
|
||||
* it: it names a player, and players are exactly what the rotation does not renumber.
|
||||
*/
|
||||
const control = atDayEnd(false);
|
||||
advance(control);
|
||||
// Identity, not a field: anything mutable is liable to be reset at a Day boundary anyway (the
|
||||
// once-a-Day dispatch reset clears `dispatchUsedToday` right there), and the claim under test
|
||||
// is about which OBJECT is attached to which chair.
|
||||
const districtLeftBehind = areaAtSeat(s, seatBefore);
|
||||
|
||||
advance(s);
|
||||
|
||||
assert.notEqual(seatOf(s, traveller), seatBefore, 'the traveller did not move');
|
||||
assert.equal(s.players[traveller]!.revenue, 17, 'Revenue is keyed by player and must travel');
|
||||
assert.equal(s.clock.superintendent, control.clock.superintendent, 'rotating the chairs moved the Fedora');
|
||||
// The Office stayed exactly where it was, so whoever sits there now inherits it as they find
|
||||
// it. That is the rule rather than a side effect of it — you take over the next station up the
|
||||
// line, mess and all.
|
||||
assert.equal(areaAtSeat(s, seatBefore), districtLeftBehind, 'the district moved with the player');
|
||||
assert.notEqual(areaOf(s, traveller), districtLeftBehind, 'the traveller kept their old district');
|
||||
});
|
||||
|
||||
it('says who is now sitting where, by name', () => {
|
||||
const s = atDayEnd(true);
|
||||
const { events } = advance(s);
|
||||
const rotated = events.find((e) => e.type === 'seatsRotated');
|
||||
assert.ok(rotated, 'no seatsRotated event was emitted');
|
||||
const line = narrate(rotated, { playerName: (p) => s.players[p]!.name }).text;
|
||||
for (const name of ['Alice', 'Bob', 'Carol']) assert.match(line, new RegExp(name));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,6 @@ const config: GameConfig = {
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: {
|
||||
reducedVisibility: false,
|
||||
sisterTrains: false,
|
||||
employeeRotation: false,
|
||||
emergencyToolbox: false,
|
||||
},
|
||||
|
||||
@@ -29,7 +29,6 @@ const config: GameConfig = {
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: {
|
||||
reducedVisibility: false,
|
||||
sisterTrains: false,
|
||||
employeeRotation: false,
|
||||
emergencyToolbox: false,
|
||||
},
|
||||
|
||||
@@ -25,7 +25,6 @@ const competitive: GameConfig = {
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: {
|
||||
reducedVisibility: false,
|
||||
sisterTrains: false,
|
||||
employeeRotation: false,
|
||||
emergencyToolbox: false,
|
||||
},
|
||||
|
||||
@@ -17,7 +17,6 @@ const config: GameConfig = {
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: {
|
||||
reducedVisibility: false,
|
||||
sisterTrains: false,
|
||||
employeeRotation: false,
|
||||
emergencyToolbox: false,
|
||||
},
|
||||
@@ -46,29 +45,30 @@ describe('game persistence (Phase 3)', () => {
|
||||
it('writes and reads back exactly what was written', () =>
|
||||
withTempDir(async (dir) => {
|
||||
await writeGame(dir, saved, '1.2.3');
|
||||
const result = await loadGame(dir, '1.2.3');
|
||||
const result = await loadGame(dir);
|
||||
assert.equal(result.found, true);
|
||||
if (!result.found) return;
|
||||
assert.equal(result.ok, true);
|
||||
if (!result.ok) return;
|
||||
assert.deepEqual(result.saved, saved);
|
||||
}));
|
||||
|
||||
it('refuses a version mismatch explicitly, naming both versions', () =>
|
||||
it('reports the version that wrote the file without judging it', () =>
|
||||
withTempDir(async (dir) => {
|
||||
// Reading a save no longer refuses on the version. The stamp is the PACKAGE version, which
|
||||
// moves for reasons unrelated to the rules, and gating on it destroyed every game in progress
|
||||
// across four releases — one of which only changed how the board is drawn. Whether a save
|
||||
// still replays is decided by replaying it (`tryResumeSession`); the version is kept because
|
||||
// it is worth naming in a failure, and nothing else.
|
||||
await writeGame(dir, saved, '1.2.3');
|
||||
const result = await loadGame(dir, '9.9.9');
|
||||
const result = await loadGame(dir);
|
||||
assert.equal(result.found, true);
|
||||
if (!result.found) return;
|
||||
assert.equal(result.ok, false);
|
||||
if (result.ok) return;
|
||||
assert.equal(result.storedVersion, '1.2.3');
|
||||
assert.equal(result.currentVersion, '9.9.9');
|
||||
assert.deepEqual(result.saved, saved);
|
||||
}));
|
||||
|
||||
it('reports not-found rather than throwing when nothing has been saved yet', () =>
|
||||
withTempDir(async (dir) => {
|
||||
const result = await loadGame(dir, '1.2.3');
|
||||
const result = await loadGame(dir);
|
||||
assert.deepEqual(result, { found: false });
|
||||
}));
|
||||
|
||||
@@ -77,11 +77,9 @@ describe('game persistence (Phase 3)', () => {
|
||||
await writeGame(dir, saved, '1.2.3');
|
||||
const grown: SavedGame = { ...saved, history: [...saved.history, { type: 'draw.end' }] };
|
||||
await writeGame(dir, grown, '1.2.3');
|
||||
const result = await loadGame(dir, '1.2.3');
|
||||
const result = await loadGame(dir);
|
||||
assert.equal(result.found, true);
|
||||
if (!result.found) return;
|
||||
assert.equal(result.ok, true);
|
||||
if (!result.ok) return;
|
||||
assert.equal(result.saved.history.length, 2);
|
||||
}));
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import type { GameConfig, PlayerIndex } from '../../src/engine/state.ts';
|
||||
import type { Push } from '../../src/server/session.ts';
|
||||
import { createSession, resumeSession } from '../../src/server/session.ts';
|
||||
import type { GameSession, Push } from '../../src/server/session.ts';
|
||||
import { createSession, resumeSession, tryResumeSession } from '../../src/server/session.ts';
|
||||
|
||||
const config: GameConfig = {
|
||||
mode: 'competitive',
|
||||
@@ -18,7 +18,6 @@ const config: GameConfig = {
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: {
|
||||
reducedVisibility: false,
|
||||
sisterTrains: false,
|
||||
employeeRotation: false,
|
||||
emergencyToolbox: false,
|
||||
},
|
||||
@@ -300,3 +299,51 @@ describe('summary() — what an administrator sees without replaying the game',
|
||||
assert.equal(s.waitingOn, null, 'a finished game must not name somebody to wait for');
|
||||
});
|
||||
});
|
||||
|
||||
describe('a save survives a release that did not change the rules', () => {
|
||||
/** Plays a couple of real moves so the history is worth replaying. */
|
||||
const played = (): ReturnType<GameSession['exportSave']> => {
|
||||
const s = createSession(42, config, ['Alice', 'Bob']);
|
||||
const actor = (s.connect(0 as PlayerIndex).menu !== null ? 0 : 1) as PlayerIndex;
|
||||
s.intent(actor, 1, { type: 'localOps.choose', option: 'draw' });
|
||||
return s.exportSave();
|
||||
};
|
||||
|
||||
it('resumes whatever version stamped it, so long as the moves still replay', () => {
|
||||
// This is the whole point. The engine version used to gate this, and it is the PACKAGE version
|
||||
// — it moves for a CSS fix. Four releases in a row destroyed every game in progress, one of
|
||||
// them for a change that only altered how the board is drawn.
|
||||
const saved = played();
|
||||
const r = tryResumeSession(saved);
|
||||
assert.equal(r.ok, true, 'a replayable save was refused');
|
||||
if (!r.ok) return;
|
||||
assert.deepEqual(r.session.exportSave().history, saved.history);
|
||||
});
|
||||
|
||||
it('refuses a save whose moves no longer replay, and says which move and why', () => {
|
||||
// A rules change is simulated by corrupting one intent — the engine cannot apply it, which is
|
||||
// exactly the shape a genuinely incompatible save has.
|
||||
const saved = played();
|
||||
const broken = {
|
||||
...saved,
|
||||
history: [...saved.history, { type: 'localOps.choose', option: 'not-a-real-option' } as never],
|
||||
};
|
||||
const r = tryResumeSession(broken);
|
||||
assert.equal(r.ok, false, 'a save the rules reject was accepted');
|
||||
if (r.ok) return;
|
||||
assert.equal(r.failure.of, broken.history.length);
|
||||
assert.equal(r.failure.stoppedAt, broken.history.length - 1, 'wrong move blamed');
|
||||
assert.equal(r.failure.intent, 'localOps.choose');
|
||||
assert.ok(r.failure.code.length > 0, 'no rejection code to act on');
|
||||
});
|
||||
|
||||
it('never silently truncates — the old loop stopped at a bad move and said nothing', () => {
|
||||
// The silence was survivable only because the version check meant a doomed replay was never
|
||||
// attempted. Now that the replay IS the check, a partial one must be impossible to mistake for
|
||||
// a whole one.
|
||||
const saved = played();
|
||||
const broken = { ...saved, history: [{ type: 'draw.end' } as never, ...saved.history] };
|
||||
const r = tryResumeSession(broken);
|
||||
assert.equal(r.ok, false, 'a truncated replay was returned as a healthy session');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,7 +41,6 @@ const solitaireConfig: GameConfig = {
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: {
|
||||
reducedVisibility: false,
|
||||
sisterTrains: false,
|
||||
employeeRotation: false,
|
||||
emergencyToolbox: false,
|
||||
},
|
||||
|
||||
@@ -29,7 +29,6 @@ const config: GameConfig = {
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: {
|
||||
reducedVisibility: false,
|
||||
sisterTrains: false,
|
||||
employeeRotation: false,
|
||||
emergencyToolbox: false,
|
||||
},
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ function gameWith(area: OfficeArea): GameState {
|
||||
id: 'g', seed: 5,
|
||||
config: {
|
||||
mode: 'solitaire', days: 5, minCombinedRevenue: 0, maxCollisionsPerDay: 0, maxCollisionsTotal: 0, pvpCardsAllowed: false,
|
||||
optionalRules: { reducedVisibility: false, sisterTrains: false, employeeRotation: false, emergencyToolbox: false },
|
||||
optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false },
|
||||
},
|
||||
playerNames: ['p'],
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ const config: GameConfig = {
|
||||
maxCollisionsPerDay: 0,
|
||||
maxCollisionsTotal: 0,
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: { reducedVisibility: false, sisterTrains: false, employeeRotation: false, emergencyToolbox: false },
|
||||
optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false },
|
||||
};
|
||||
const game = (seed = 5): GameState => createGame({ id: 'g', seed, config, playerNames: ['p'] });
|
||||
const at = (row: number, col: number): GridCoord => ({ row, col });
|
||||
|
||||
+5
-5
@@ -2680,7 +2680,7 @@ describe('the Division map shows the whole route', () => {
|
||||
maxCollisionsPerDay: 0,
|
||||
maxCollisionsTotal: 0,
|
||||
pvpCardsAllowed: false,
|
||||
optionalRules: { reducedVisibility: false, sisterTrains: false, employeeRotation: false, emergencyToolbox: false },
|
||||
optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false },
|
||||
},
|
||||
playerNames: ['A', 'B', 'C', 'D'].slice(0, players),
|
||||
});
|
||||
@@ -2695,7 +2695,7 @@ describe('the Division map shows the whole route', () => {
|
||||
seed: 7,
|
||||
config: {
|
||||
mode: 'solitaire', days: 5, minCombinedRevenue: 0, maxCollisionsPerDay: 0, maxCollisionsTotal: 0, pvpCardsAllowed: false,
|
||||
optionalRules: { reducedVisibility: false, sisterTrains: false, employeeRotation: false, emergencyToolbox: false },
|
||||
optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false },
|
||||
},
|
||||
playerNames: ['Solitaire'],
|
||||
});
|
||||
@@ -2748,7 +2748,7 @@ describe('the Division map shows the whole route', () => {
|
||||
id: 'div-chips', seed: 7,
|
||||
config: {
|
||||
mode: 'solitaire', days: 5, minCombinedRevenue: 0, maxCollisionsPerDay: 0, maxCollisionsTotal: 0, pvpCardsAllowed: false,
|
||||
optionalRules: { reducedVisibility: false, sisterTrains: false, employeeRotation: false, emergencyToolbox: false },
|
||||
optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false },
|
||||
},
|
||||
playerNames: ['Solitaire'],
|
||||
});
|
||||
@@ -3077,7 +3077,7 @@ describe('the tray is an engine plus its Rolling Stock', () => {
|
||||
id: 'eng', seed: 1038389,
|
||||
config: {
|
||||
mode: 'solitaire', days: 5, minCombinedRevenue: 0, maxCollisionsPerDay: 0, maxCollisionsTotal: 0, pvpCardsAllowed: false,
|
||||
optionalRules: { reducedVisibility: false, sisterTrains: false, employeeRotation: false, emergencyToolbox: false },
|
||||
optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false },
|
||||
},
|
||||
playerNames: ['Solitaire'],
|
||||
});
|
||||
@@ -3127,7 +3127,7 @@ describe('the tray is an engine plus its Rolling Stock', () => {
|
||||
id: 'yards', seed: 1038389,
|
||||
config: {
|
||||
mode: 'solitaire', days: 5, minCombinedRevenue: 0, maxCollisionsPerDay: 0, maxCollisionsTotal: 0, pvpCardsAllowed: false,
|
||||
optionalRules: { reducedVisibility: false, sisterTrains: false, employeeRotation: false, emergencyToolbox: false },
|
||||
optionalRules: { reducedVisibility: false, employeeRotation: false, emergencyToolbox: false },
|
||||
},
|
||||
playerNames: ['Solitaire'],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user