93 lines
5.6 KiB
TypeScript
93 lines
5.6 KiB
TypeScript
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 };
|
|
}
|