Connect printed high Ecaz Homeworld alliance victory

This commit is contained in:
Quintine committed 2026-10-05 14:41:11 +11:00
1 parent cfa0fe3952
commit 17bc4537c3
14 files changed
+878 -8

No files matched your search

@@ -0,0 +1,120 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction, normalizeAutomaticGame, viewGame, RuleError, type Game } from '../game/engine';
import { quoteVictory, VictoryQuoteError } from '../game/victory-quote';
import { strongholdProgress } from '../game/victory-progress';
import { ecazHomeworldFixture, ecazSeat, observeEcazPosition } from './fixture-ecaz-homeworld-victory';
import { biddingPhysicalIds } from './fixture-homeworld-occupied-bidding';
import { TERRITORIES } from '../game/board';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
function originalCollection(game: Game): Game {
game = normalizeAutomaticGame(game);
for (let step = 0; !(game.phase === 7 && !game.phaseOpening && !game.response && !game.decision) && step < 300; step++) {
if (game.phaseOpening) {
const player = game.players.find(player => !game.phaseOpening!.passed.includes(player.id))!;
game = applyAction(game, player.id, { type: 'ready' });
} else if (game.response) {
const player = game.players.find(player => !game.response!.passed.includes(player.id))!;
game = applyAction(game, player.id, { type: 'passResponse' });
} else if (game.decision?.kind === 'homeworldOccupiedIncome') {
const player = game.decision.player, offer = viewGame(game, player).homeworldOccupiedIncome!;
game = applyAction(game, player, { type: 'decision', event: offer.event, world: offer.world, ownAmount: offer.amount });
} else if (game.decision?.kind === 'guildTiming')
game = applyAction(game, game.decision.player, { type: 'decision', take: true });
else if (game.phase === 5 && game.active && !game.decision)
game = applyAction(game, game.active, { type: 'endMovement' });
else if (game.phase === 6 && !game.decision) {
const player = game.players.find(player => !game.ready.includes(player.id))!;
game = applyAction(game, player.id, { type: 'ready' });
} else throw new Error('The original Collection fixture requires another supported owned choice.');
}
assert.equal(game.phase, 7);
return game;
}
function originalMentat(game: Game) {
for (const player of game.players) if (!game.ready.includes(player.id))
game = applyAction(JSON.parse(JSON.stringify(game)), player.id, { type: 'ready' });
assert.equal(game.phase, 8);
return game;
}
void test('actual original Mentat awards the high Ecaz two-native-world alliance win with one joint stronghold and no ordinary point inflation', () => {
const fixture = ecazHomeworldFixture(true);
let game = originalCollection(fixture.game);
const ordinary = strongholdProgress(game).progress.find(row => row.player === 'ec')!;
assert.equal(ordinary.strongholds.length, 1); assert.equal(ordinary.jointlyOccupied.length, 1); assert.equal(ordinary.qualifies, false);
assert.equal(game.status, 'playing'); assert.deepEqual(game.winner, []);
const row = viewGame(game, 'ec').ecazHomeworldVictory!;
assert.equal(row.population, 7); assert.equal(row.qualifies, true);
assert.deepEqual(new Set(row.distinctNativeFactions), new Set(['atreides', 'emperor']));
const cards = biddingPhysicalIds(game), forces = game.players.map(player => ({ id: player.id, forces: player.forces, reserves: player.reserves, tanks: player.tanks }));
for (const difficulty of DIFFICULTIES) {
let result: Game = JSON.parse(JSON.stringify(game));
for (const player of result.players) {
const view = viewGame(result, player.id); view.players.find(seat => seat.id === player.id)!.bot = difficulty;
const ready = botActions(view).find(action => action.type === 'ready');
assert.ok(ready, `${difficulty} has its original legal Collection readiness`);
result = applyAction(result, player.id, ready);
}
assert.equal(result.status, 'finished');
assert.deepEqual(new Set(result.winner), new Set(['ec', 'al']));
assert.deepEqual(biddingPhysicalIds(result), cards);
}
game = originalMentat(game);
assert.equal(game.status, 'finished'); assert.deepEqual(new Set(game.winner), new Set(['ec', 'al']));
assert.deepEqual(biddingPhysicalIds(game), cards);
assert.deepEqual(game.players.map(player => ({ id: player.id, forces: player.forces, reserves: player.reserves, tanks: player.tanks })), forces);
});
void test('native population6 does not award this win despite the same actual joint site and two occupied native factions', () => {
const fixture = ecazHomeworldFixture(true);
ecazSeat(fixture.game, 'ec').reserves--; ecazSeat(fixture.game, 'ec').tanks++;
observeEcazPosition(fixture.game, 'controlled-native-six-before-original-check');
let game = originalCollection(fixture.game);
assert.equal(viewGame(game, 'ec').ecazHomeworldVictory!.high, false);
game = originalMentat(game);
assert.equal(game.status, 'playing'); assert.deepEqual(game.winner, []);
});
for (const predicted of ['ecaz', 'guild'] as const) void test(`actual BG prediction of ${predicted} replaces the printed Ecaz alliance win without changing public progress`, () => {
let game = originalCollection(ecazHomeworldFixture(true).game);
const progress = viewGame(game, 'ec').ecazHomeworldVictory!;
ecazSeat(game, 'observer').prediction = { faction: predicted, turn: game.turn };
assert.equal(viewGame(game, 'al').ecazHomeworldVictory!.qualifies, progress.qualifies);
game = originalMentat(game);
assert.deepEqual(game.winner, ['observer']); assert.equal(game.status, 'finished');
});
void test('the original winner quote unions a simultaneous ordinary side before applying private prediction', () => {
const fixture = ecazHomeworldFixture(true);
const game = originalCollection(fixture.game);
// Labelled surviving native counters: a different faction holds three ordinary sites.
const sites = TERRITORIES.filter(site => site.type === 'stronghold' &&
site.id !== fixture.jointStronghold && !site.sectors.includes(game.storm)).slice(0, 3)
.map(site => `${site.id}:${site.sectors[0]}`);
assert.equal(sites.length, 3);
const other = ecazSeat(game, 'observer');
assert.equal(other.faction, 'beneGesserit');
delete other.advisors; other.forces = Object.fromEntries(sites.map(site => [site, 1])); other.reserves -= 3;
observeEcazPosition(game, 'controlled-separate-ordinary-winner');
const quote = quoteVictory(game, game);
assert.deepEqual(new Set(quote.winner), new Set(['ec', 'al', 'observer']));
assert.equal(quote.ecazHomeworld?.qualifies, true);
});
void test('a wrong detached public Ecaz source cannot authorize an otherwise ordinary original victory quote', () => {
const game = originalCollection(ecazHomeworldFixture(true).game), changed = structuredClone(game);
changed.turn++;
const before = structuredClone(game);
assert.throws(() => quoteVictory(game, changed), VictoryQuoteError);
assert.deepEqual(game, before);
const markerless = structuredClone(game); delete markerless.homeworldEcazVictoryPreview;
assert.deepEqual(quoteVictory(markerless).winner, []);
assert.equal(viewGame(markerless, 'ec').ecazHomeworldVictory, null);
const malformed = structuredClone(game); delete malformed.homeworldOccupationHistory;
const frozen = structuredClone(malformed);
assert.throws(() => applyAction(malformed, 'ec', { type: 'ready' }), RuleError);
assert.deepEqual(malformed, frozen);
});
+257
View File
@@ -0,0 +1,257 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { ecazHomeworldVictoryProgress, type EcazHomeworldVictoryProgress } from '../game/ecaz-homeworld-victory';
import { VictoryProgressError, strongholdProgress } from '../game/victory-progress';
import { createRicheseNoField, deployRicheseNoField } from '../game/richese-no-field';
import { homeworldGameIntegrity } from '../game/homeworld-game';
import { splitLocation } from '../game/board';
import { conserveEcazPosition, ecazHomeworldFixture, ecazSeat, observeEcazPosition } from './fixture-ecaz-homeworld-victory';
for (const advanced of [false, true]) {
void test(`${advanced ? 'Advanced' : 'Basic'} original audit setup and conserved split holdings satisfy the printed High Ecaz condition`, () => {
const { game, setup, movement, jointStronghold } = ecazHomeworldFixture(advanced);
assert.equal(setup.homeworldEcazVictoryPreview, true);
assert.equal(setup.homeworldOccupationPreview, true);
assert.equal(movement.phase, 5);
assert.equal(movement.response, null);
assert.equal(movement.decision, null);
assert.ok(!game.nexusCards);
assert.ok(!game.techTokens && !game.strongholdCards && !game.discoveryEnabled);
const before = JSON.stringify(game);
const row = ecazHomeworldVictoryProgress(game)!;
assert.deepEqual(row, {
owner: 'ec', members: game.order.filter(id => id === 'ec' || id === 'al'),
population: 7, minimum: 7, high: true, jointStrongholds: [jointStronghold],
foreignHomeworlds: [
{ world: 'homeworld:atreides', card: 'caladan', native: 'at', faction: 'atreides', holder: 'ec' },
{ world: 'homeworld:emperor', card: 'kaitain', native: 'em', faction: 'emperor', holder: 'al' },
],
distinctNativeFactions: ['atreides', 'emperor'], qualifies: true, blocked: null,
});
assert.equal(JSON.stringify(game), before);
assert.deepEqual(ecazHomeworldVictoryProgress(JSON.parse(before)), row);
assert.equal(strongholdProgress(game).progress.find(row => row.player === 'ec')!.qualifies, false);
homeworldGameIntegrity(game);
});
void test(`${advanced ? 'Advanced' : 'Basic'} six native Ecaz counters are Low; seven restores the independent printed win`, () => {
const { game } = ecazHomeworldFixture(advanced);
ecazSeat(game, 'ec').reserves = 6;
observeEcazPosition(game, 'controlled-native-ecaz-six');
const low = ecazHomeworldVictoryProgress(game)!;
assert.equal(low.population, 6);
assert.equal(low.high, false);
assert.equal(low.qualifies, false);
assert.equal(low.foreignHomeworlds.length, 2);
ecazSeat(game, 'ec').reserves = 7;
observeEcazPosition(game, 'controlled-native-ecaz-seven');
assert.equal(ecazHomeworldVictoryProgress(game)!.qualifies, true);
});
for (const holders of [['ec', 'ec'], ['al', 'al']] as const)
void test(`${advanced ? 'Advanced' : 'Basic'} both distinct foreign worlds can be physically held by ${holders[0]}`, () => {
const { game } = ecazHomeworldFixture(advanced, 'guild', holders);
const row = ecazHomeworldVictoryProgress(game)!;
assert.deepEqual(row.foreignHomeworlds.map(world => world.holder), [...holders]);
assert.equal(row.qualifies, true);
});
void test(`${advanced ? 'Advanced' : 'Basic'} two worlds without one actual joint stronghold never qualify`, () => {
const { game } = ecazHomeworldFixture(advanced);
ecazSeat(game, 'al').forces = {};
conserveEcazPosition(game);
const row = ecazHomeworldVictoryProgress(game)!;
assert.deepEqual(row.jointStrongholds, []);
assert.equal(row.foreignHomeworlds.length, 2);
assert.equal(row.qualifies, false);
});
void test(`${advanced ? 'Advanced' : 'Basic'} a departed physical garrison is not a retained victory holding`, () => {
const { game } = ecazHomeworldFixture(advanced);
delete game.homeworlds!.custody!.visitors['homeworld:atreides'];
observeEcazPosition(game, 'controlled-caladan-departure');
const row = ecazHomeworldVictoryProgress(game)!;
assert.deepEqual(row.distinctNativeFactions, ['emperor']);
assert.equal(row.qualifies, false);
assert.equal(row.blocked, null);
});
void test(`${advanced ? 'Advanced' : 'Basic'} established source authority distinguishes native repopulation from physical foreign presence`, () => {
const { game } = ecazHomeworldFixture(advanced);
ecazSeat(game, 'at').reserves = 20;
observeEcazPosition(game, 'controlled-native-caladan-repopulation');
const row = ecazHomeworldVictoryProgress(game)!;
assert.equal(row.qualifies, advanced);
assert.equal(row.foreignHomeworlds.length, advanced ? 2 : 1);
assert.equal(row.blocked === null, advanced);
});
void test(`${advanced ? 'Advanced' : 'Basic'} third Homeworld visitors preserve Advanced retained authority but block unresolved Basic occupation`, () => {
const { game } = ecazHomeworldFixture(advanced);
game.homeworlds!.custody!.visitors['homeworld:atreides'].observer = { normal: 1, elite: 0 };
ecazSeat(game, 'observer').reserves--;
observeEcazPosition(game, 'controlled-third-homeworld-visitor');
const row = ecazHomeworldVictoryProgress(game)!;
assert.equal(row.qualifies, advanced);
assert.equal(row.blocked === null, advanced);
});
}
void test('Emperor Kaitain and Salusa are two physical worlds of only one other native faction', () => {
const { game } = ecazHomeworldFixture();
delete game.homeworlds!.custody!.visitors['homeworld:atreides'];
game.homeworlds!.custody!.visitors['homeworld:emperor:salusa'] = { ec: { normal: 1, elite: 0 } };
observeEcazPosition(game, 'controlled-two-emperor-worlds');
const row = ecazHomeworldVictoryProgress(game)!;
assert.deepEqual(row.foreignHomeworlds.map(world => world.card), ['kaitain', 'salusa_secundus']);
assert.deepEqual(row.distinctNativeFactions, ['emperor']);
assert.equal(row.qualifies, false);
assert.equal(row.blocked, null);
});
void test('own Ecaz and ally-native worlds are excluded rather than counted as other native factions', () => {
const { game } = ecazHomeworldFixture();
delete game.homeworlds!.custody!.visitors['homeworld:atreides'];
// Deliberately hypothetical public custody, not a legal alliance action:
// the real engine separately prohibits allies occupying each other's homes.
game.homeworlds!.custody!.visitors['homeworld:ecaz'] = { al: { normal: 1, elite: 0 } };
game.homeworlds!.custody!.visitors['homeworld:guild'] = { ec: { normal: 1, elite: 0 } };
const row = ecazHomeworldVictoryProgress(game)!;
assert.deepEqual(row.foreignHomeworlds.map(world => world.world), ['homeworld:emperor']);
assert.deepEqual(row.distinctNativeFactions, ['emperor']);
assert.equal(row.qualifies, false);
assert.equal(row.blocked, null);
});
void test('joint stronghold under storm still counts; third fighters contest it and unrelated advisors do not', () => {
const { game, jointStronghold, jointLocation } = ecazHomeworldFixture();
game.storm = splitLocation(jointLocation).sector; // Hypothetical public-board storm, not a clock transition.
assert.equal(ecazHomeworldVictoryProgress(game)!.qualifies, true);
ecazSeat(game, 'observer').forces = { [jointLocation]: 1 };
ecazSeat(game, 'observer').reserves--;
conserveEcazPosition(game);
assert.deepEqual(ecazHomeworldVictoryProgress(game)!.jointStrongholds, []);
ecazSeat(game, 'observer').advisors = { [jointStronghold]: {} };
conserveEcazPosition(game);
assert.deepEqual(ecazHomeworldVictoryProgress(game)!.jointStrongholds, [jointStronghold]);
assert.equal(ecazHomeworldVictoryProgress(game)!.qualifies, true);
});
void test('allied BG advisors are not a second joint fighter, and mandatory release cannot invent one', () => {
const { game, jointStronghold, jointLocation } = ecazHomeworldFixture(true, 'beneGesserit');
ecazSeat(game, 'al').advisors = { [jointStronghold]: {} };
assert.equal(ecazHomeworldVictoryProgress(game)!.qualifies, false);
ecazSeat(game, 'ec').forces = {};
conserveEcazPosition(game);
assert.equal(strongholdProgress(game).released.length, 1);
assert.deepEqual(ecazHomeworldVictoryProgress(game)!.jointStrongholds, []);
delete ecazSeat(game, 'al').advisors;
ecazSeat(game, 'ec').forces = { [jointLocation]: 1 };
conserveEcazPosition(game);
assert.equal(ecazHomeworldVictoryProgress(game)!.qualifies, true);
});
void test('a placed HMS is an actual joint stronghold, not an ordinary board pointer', () => {
const { game } = ecazHomeworldFixture(true, 'ixians');
game.mobileStronghold = { location: 'red_chasm:7' };
for (const id of ['ec', 'al']) ecazSeat(game, id).forces = { 'hidden_mobile_stronghold:0': 1 };
conserveEcazPosition(game);
assert.deepEqual(ecazHomeworldVictoryProgress(game)!.jointStrongholds, ['hidden_mobile_stronghold']);
assert.equal(ecazHomeworldVictoryProgress(game)!.qualifies, true);
ecazSeat(game, 'al').forces = { 'red_chasm:7': 1 };
conserveEcazPosition(game);
assert.deepEqual(ecazHomeworldVictoryProgress(game)!.jointStrongholds, []);
});
void test('public No-Field presence gives the same joint site for every concealed value without inventory reads', () => {
const { game, jointLocation } = ecazHomeworldFixture(true, 'richese');
ecazSeat(game, 'al').forces = {};
conserveEcazPosition(game);
let expected: EcazHomeworldVictoryProgress | null | undefined;
for (const id of ['field-zero', 'field-three', 'field-five']) {
const field = deployRicheseNoField(createRicheseNoField(['field-zero', 'field-three', 'field-five']), {
tokenId: id, controller: 'al', location: splitLocation(jointLocation),
});
ecazSeat(game, 'al').noField = field;
Object.defineProperty(field, 'tokens', { get() { throw new Error('Private inventory accessed'); } });
const row = ecazHomeworldVictoryProgress(game);
assert.equal(row!.qualifies, true);
if (expected) assert.deepEqual(row, expected);
expected = row;
}
});
void test('public projection does not read hands, leaders, spice, predictions or technology income', () => {
const { game } = ecazHomeworldFixture();
const expected = ecazHomeworldVictoryProgress(game)!;
for (const player of game.players)
for (const key of ['hand', 'leaders', 'spice', 'prediction'])
Object.defineProperty(player, key, { get() { throw new Error('Private player field accessed'); } });
Object.defineProperty(game, 'techTokens', { get() { throw new Error('Unneeded technology accessed'); } });
assert.deepEqual(ecazHomeworldVictoryProgress(game), expected);
});
void test('old contexts without the fresh marker return null without reserve, elite or Homeworld reads', () => {
const { game } = ecazHomeworldFixture();
delete game.homeworldEcazVictoryPreview;
for (const player of game.players)
for (const key of ['reserves', 'elites'])
Object.defineProperty(player, key, { get() { throw new Error('Old context reserve accessed'); } });
Object.defineProperty(game, 'homeworlds', { get() { throw new Error('Old context Homeworld accessed'); } });
assert.equal(ecazHomeworldVictoryProgress(game), null);
});
void test('missing or legacy source history blocks a potential two-native win without creating holdings', () => {
for (const legacy of [false, true]) {
const { game } = ecazHomeworldFixture();
if (legacy) delete game.homeworlds!.historyVersion;
else delete game.homeworldOccupationHistory;
const row = ecazHomeworldVictoryProgress(game)!;
assert.deepEqual(row.foreignHomeworlds, []);
assert.deepEqual(row.distinctNativeFactions, []);
assert.equal(row.qualifies, false);
assert.notEqual(row.blocked, null);
delete game.homeworlds!.custody!.visitors['homeworld:atreides'];
assert.equal(ecazHomeworldVictoryProgress(game)!.blocked, null);
}
});
void test('Basic expiry stays unresolved; Advanced retains occupation while its original holder remains', () => {
for (const advanced of [false, true]) {
const { game } = ecazHomeworldFixture(advanced);
game.turn++;
const row = ecazHomeworldVictoryProgress(game)!;
assert.equal(row.qualifies, advanced);
assert.equal(row.blocked === null, advanced);
}
});
void test('a physical unobserved new sole garrison cannot manufacture Advanced source qualification', () => {
const { game, movement } = ecazHomeworldFixture();
game.homeworldOccupationHistory = movement.homeworldOccupationHistory;
const row = ecazHomeworldVictoryProgress(game)!;
assert.equal(row.qualifies, false);
assert.deepEqual(row.foreignHomeworlds, []);
assert.notEqual(row.blocked, null);
});
void test('nonexistent Homeworlds, malformed public data and altered JSON source provenance reject with the victory error class', () => {
const { game } = ecazHomeworldFixture();
const cases = [
(g: typeof game) => { g.homeworlds!.custody!.visitors['homeworld:nonexistent'] = { ec: { normal: 1, elite: 0 } }; },
(g: typeof game) => { g.homeworlds!.custody!.visitors['homeworld:ecaz'] = { ec: { normal: 1, elite: 0 } }; },
(g: typeof game) => { ecazSeat(g, 'al').ally = null; },
(g: typeof game) => { ecazSeat(g, 'ec').reserves = -1; },
(g: typeof game) => { g.turn = 0; },
(g: typeof game) => { ecazSeat(g, 'ec').faction = 'harkonnen'; },
(g: typeof game) => { g.homeworldOccupationHistory!.sources[1].cause = 'setup'; },
(g: typeof game) => { g.homeworldOccupationHistory!.qualifications[0].player = 'observer'; },
(g: typeof game) => { g.homeworldOccupationHistory!.snapshots[1] = '[]'; },
(g: typeof game) => { delete g.homeworldOccupationPreview; },
];
for (const mutate of cases) {
const restored: typeof game = JSON.parse(JSON.stringify(game));
mutate(restored);
assert.throws(() => ecazHomeworldVictoryProgress(restored), VictoryProgressError);
}
});
+92
View File
@@ -0,0 +1,92 @@
import assert from 'node:assert/strict';
import { applyAction, createGame, initializeHomeworldOccupationGameForAudit, joinGame, newPlayer, type Game } from '../game/engine';
import { FACTIONS, type FactionId } from '../game/catalog';
import { TERRITORIES } from '../game/board';
import { homeworldContext, homeworldGameIntegrity } from '../game/homeworld-game';
import { homeworldForceGroups } from '../game/homeworld-custody';
import { observeHomeworldOccupation } from '../game/homeworld-occupation-history';
import { advanceOriginal } from './fixture-homeworld-occupied-producers';
export const ecazSeat = (game: Game, id: string) => game.players.find(player => player.id === id)!;
/** Controlled positions are conserved test placements, NOT naturally achieved
* games. Setup and source history are original; no synthetic source facts,
* replacement deck, optional cards or fabricated occupation receipt is used. */
export function conserveEcazPosition(game: Game): void {
const groups = homeworldForceGroups(homeworldContext(game), game.homeworlds!.custody!);
for (const player of game.players) {
const abroad = groups.filter(home => home.native !== player.id)
.reduce((n, home) => n + (home.forces[player.id]?.normal ?? 0) + (home.forces[player.id]?.elite ?? 0), 0);
player.tanks = 20 - player.reserves - abroad - Object.values(player.forces).reduce((a, n) => a + n, 0);
if (player.elites) {
const total = player.faction === 'fremen' ? 3 : player.faction === 'ixians' ? 7 : 5;
const abroadElite = groups.filter(home => home.native !== player.id)
.reduce((n, home) => n + (home.forces[player.id]?.elite ?? 0), 0);
player.elites.tanks = total - player.elites.reserves - abroadElite - Object.values(player.elites.forces).reduce((a, n) => a + n, 0);
}
}
homeworldGameIntegrity(game);
}
export function observeEcazPosition(game: Game, event: string, cause: 'change' | 'turnStart' | 'turnEnd' = 'change'): void {
conserveEcazPosition(game);
game.homeworldOccupationHistory = observeHomeworldOccupation(game.homeworldOccupationHistory!,
homeworldContext(game), game.homeworlds!.custody!, game.turn, cause, event);
}
export function ecazHomeworldFixture(advanced = true, ally: FactionId = 'guild', holders: readonly ['ec' | 'al', 'ec' | 'al'] = ['ec', 'al']) {
const factions = ['ecaz', ally, 'atreides', 'emperor', ...(ally === 'beneGesserit' ? ['guild'] : ['beneGesserit'])] as FactionId[];
const expansions = [...new Set(factions.map(faction => FACTIONS.find(row => row.id === faction)!.expansion)
.filter(expansion => expansion !== 'base'))];
let game = createGame('ECAZHOMEWORLDVICTORY', newPlayer('ec', 'Ecaz', 'ecaz'), advanced, expansions);
for (const [index, faction] of factions.slice(1).entries())
joinGame(game, newPlayer(['al', 'at', 'em', 'observer'][index], faction, faction));
game = applyAction(game, 'ec', { type: 'homeworlds', enabled: true });
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
game = initializeHomeworldOccupationGameForAudit(game);
game = advanceOriginal(game, current => current.status === 'playing');
const setup = structuredClone(game);
game = advanceOriginal(game, current => current.phase === 5 && !current.response && !current.decision);
const movement = structuredClone(game);
const cards = [...game.deck, ...game.discard, ...(game.richeseCache ?? []), ...(game.richeseRemoved ?? []),
...game.players.flatMap(player => player.hand)].map(card => card.id).sort();
const originalTanks = Object.fromEntries(game.players.map(player => [player.id, {
normal: player.tanks, elite: player.elites?.tanks ?? 0,
}]));
for (const player of game.players) {
Object.assign(player, { forces: {}, reserves: 20 - player.tanks, ally: null });
delete player.advisors;
if (player.elites) Object.assign(player.elites, {
reserves: (player.faction === 'ixians' ? 7 : player.faction === 'fremen' ? 3 : 5) - player.elites.tanks,
forces: {},
});
}
const joint = TERRITORIES.find(site => site.type === 'stronghold' && !site.sectors.includes(game.storm))!;
assert.ok(joint, 'Original Movement must have a non-storm joint stronghold.');
const jointStronghold = joint.id;
const jointLocation = `${joint.id}:${joint.sectors[0]}`;
const custody = game.homeworlds!.custody!;
custody.visitors = {
'homeworld:atreides': { [holders[0]]: { normal: 1, elite: 0 } },
'homeworld:emperor': { [holders[1]]: { normal: 1, elite: 0 } },
};
if (advanced) custody.salusa = { normal: 0, elite: 0 };
ecazSeat(game, 'ec').ally = 'al';
ecazSeat(game, 'al').ally = 'ec';
ecazSeat(game, 'ec').forces = { [jointLocation]: 1 };
ecazSeat(game, 'al').forces = { [jointLocation]: 1 };
ecazSeat(game, 'ec').reserves = 7;
ecazSeat(game, 'al').reserves = 20 - originalTanks.al.normal - 1 - holders.filter(holder => holder === 'al').length;
for (const id of ['at', 'em']) {
ecazSeat(game, id).reserves = 0;
if (ecazSeat(game, id).elites) ecazSeat(game, id).elites!.reserves = 0;
}
observeEcazPosition(game, 'controlled-conserved-ecaz-two-native-worlds');
for (const player of game.players) {
assert.ok(player.tanks >= originalTanks[player.id].normal, 'Controlled position cannot resurrect original Tanks.');
assert.ok((player.elites?.tanks ?? 0) >= originalTanks[player.id].elite, 'Controlled position cannot resurrect original special Tanks.');
}
assert.deepEqual([...game.deck, ...game.discard, ...(game.richeseCache ?? []), ...(game.richeseRemoved ?? []),
...game.players.flatMap(player => player.hand)].map(card => card.id).sort(), cards);
return { game, setup, movement, cards, jointStronghold, jointLocation };
}