Files
Dune/tests/fixture-ecaz-nexus-skills-modules.ts

355 lines
23 KiB
TypeScript

import assert from 'node:assert/strict';
import { applyAction, createGame, handLimit, joinGame, newPlayer, viewGame, type Game } from '../game/engine';
import { territory } from '../game/board';
import type { FactionId } from '../game/catalog';
import { DUKE_VIDAL_ID } from '../game/duke-vidal';
import { LEADER_SKILL_CARDS, type LeaderSkillId } from '../game/leader-skill-cards';
import { NEXUS_FACTIONS, validateNexusCards } from '../game/nexus-cards';
import { strongholdControllers, type StrongholdId } from '../game/stronghold-cards';
import { ownedTech } from '../game/tech-tokens';
import type { Card } from '../game/cards';
import { initializeAdvancedNativeSkillsSetup } from './fixture-advanced-native-skills';
import {
assertEcazSkillsModulesCustody, ecazSkillsModulesAction, ecazSkillsModulesPlayer,
ecazSkillsModulesTrainer, nextEcazSkillsModulesStep, reloadEcazSkillsModules,
} from './fixture-ecaz-skills-modules';
import type { StrongholdFactionsNativeStep } from './fixture-stronghold-factions';
/** Existing source composition, not an exceptional-Duke or allied-Occupy ruling.
* GF9 E3 pp.3/8/11/16 and publisher Ecaz Nexus face: docs/NEXUS_ECAZ_RULES.md.
* All14 normal/selected-disc effects: docs/LEADER_SKILLS_RULES.md.
* Original held-card effects/lifecycle: docs/STRONGHOLD_CARDS.md.
* The quiet living/unclaimed Duke window is the existing product boundary;
* printed dead/captured/Ghola recovery and shared-Duke training remain pending.
*/
export const ECAZ_NEXUS_SKILLS_MODULES_SOURCES = {
nexus: 'docs/NEXUS_ECAZ_RULES.md#bounded-native-cunning-duke-vidal',
skills: 'docs/LEADER_SKILLS_RULES.md',
modules: 'docs/STRONGHOLD_CARDS.md',
publisher: 'https://www.gf9games.com/dune/wp-content/uploads/EcazMoritani-Rulebook-LOWRES.pdf',
face: 'https://boardgamegeek.com/image/7767032/dune-ecaz-and-moritani',
} as const;
export type EcazNexusSkillsModulesStep = StrongholdFactionsNativeStep;
export type EcazNexusSkillsModulesOptions = {
/** Only the actual undealt lobby or original unassigned skill-first setup. */
initial?: Game;
rules?: 'basic' | 'advanced';
tech?: boolean;
stronghold?: boolean;
skill?: LeaderSkillId;
/** A genuine eligible native disc; never the separate Duke. */
leader?: string;
kind?: 'duke' | 'suk';
band?: 'normal' | 'skilled';
strongholdKind?: Exclude<StrongholdId, 'hidden_mobile_stronghold'>;
poison?: boolean;
defense?: boolean;
ownerDial?: number;
opponentDial?: number;
};
export type EcazNexusSkillsModulesTransition = {
before: Game; step: EcazNexusSkillsModulesStep; after: Game;
};
export type EcazNexusSkillsModulesFixture = {
initial: Game; offered: Game; afterSetup: Game; afterFirstStorm: Game;
beforeFirstMentat: Game; firstMentatStep: EcazNexusSkillsModulesStep; afterFirstMentat: Game;
beforeAlliance: Game; allianceActions: EcazNexusSkillsModulesStep[];
closingDraw: EcazNexusSkillsModulesTransition;
beforeCunning: Game; cunning: EcazNexusSkillsModulesTransition;
beforeBattle: Game; posture: Game; game: Game;
owner: string; opponent: string; observer: string; trainer: string;
territory: Exclude<StrongholdId, 'hidden_mobile_stronghold'>; location: string;
plans: EcazNexusSkillsModulesStep[]; actions: EcazNexusSkillsModulesStep[]; staging: string[];
};
export {
ecazSkillsModulesAction as ecazNexusSkillsModulesAction,
ecazSkillsModulesPlayer as ecazNexusSkillsModulesPlayer,
ecazSkillsModulesTrainer as ecazNexusSkillsModulesTrainer,
reloadEcazSkillsModules as reloadEcazNexusSkillsModules,
};
export const ecazNexusSkillsModulesClean = (g: Game): boolean => !g.response && !g.phaseOpening && !g.decision;
export function assertEcazNexusSkillsModulesCustody(game: Game): void {
assertEcazSkillsModulesCustody(game);
const skills = game.leaderSkills!;
assert.deepEqual([...skills.deck, ...Object.values(skills.offers).flatMap(o => o.cards),
...skills.assignments.map(a => a.skill)].sort(), LEADER_SKILL_CARDS.map(c => c.id).sort());
validateNexusCards(game.nexusCards!.cards!, game.players);
const cards = game.nexusCards!.cards!;
assert.deepEqual([...cards.deck, ...cards.discard,
...Object.values(cards.hands).filter((c): c is FactionId => c !== null)].sort(), [...NEXUS_FACTIONS].sort());
}
export function nextEcazNexusSkillsModulesStep(game: Game): EcazNexusSkillsModulesStep | null {
if (ecazNexusSkillsModulesClean(game) && game.nexusCards?.phase?.stage === 'drawing') {
const phase = game.nexusCards.phase, actor = phase.eligible.find(id => !phase.done.includes(id));
if (actor) return { actor, action: { type: 'nexusCardChoice', turn: game.turn,
card: game.nexusCards.cards!.hands[actor], choice: 'keep', ownRedraws: 0 } };
}
if (!game.response && !game.phaseOpening && game.decision?.kind === 'wormRide')
return { actor: game.decision.player, action: { type: 'decision', accept: false } };
return nextEcazSkillsModulesStep(game);
}
export function stepEcazNexusSkillsModules(game: Game, next: EcazNexusSkillsModulesStep,
actions?: EcazNexusSkillsModulesStep[]): Game {
actions?.push(structuredClone(next));
return ecazSkillsModulesAction(game, next.actor, next.action);
}
export function advanceEcazNexusSkillsModules(state: Game, until: (g: Game) => boolean,
actions?: EcazNexusSkillsModulesStep[], firstDial = 0): Game {
let game = reloadEcazSkillsModules(state);
for (let count = 0; count < 1800; count++) {
if (until(game)) return game;
assert.equal(game.status, 'playing');
const next = nextEcazNexusSkillsModulesStep(game);
assert.ok(next, 'An actual private battle plan must be supplied by its owner.');
if (game.turn === 1 && next.action.type === 'stormDial') next.action.amount = firstDial;
game = stepEcazNexusSkillsModules(game, next, actions);
}
throw Error('Original Ecaz Nexus/Skills chronology did not reach its declared boundary.');
}
const CLASSIC_OPPONENTS: Partial<Record<FactionId, true>> = {
atreides: true, emperor: true, guild: true, fremen: true, beneGesserit: true, harkonnen: true,
};
function lobby(options: EcazNexusSkillsModulesOptions): Game {
const rules = options.rules ?? (options.initial?.advanced === false ? 'basic' : 'advanced');
let game = options.initial ? reloadEcazSkillsModules(options.initial)
: createGame('ECAZNEXUSSKILLSMODULES', newPlayer('ecaz', 'Ecaz', 'ecaz'), rules === 'advanced', ['ecaz']);
if (!options.initial) for (const faction of ['emperor', 'atreides'] as const) joinGame(game, newPlayer(faction, faction, faction));
const tech = options.tech ?? (options.initial ? !!game.techTokens : true);
const stronghold = options.stronghold ?? (options.initial ? !!game.strongholdCards : rules === 'advanced');
assert.equal(game.advanced, rules === 'advanced');
assert.deepEqual(game.expansions, ['ecaz']);
assert.ok(game.players.length >= (tech ? 3 : 2) && game.players.length <= 6);
assert.ok(!stronghold || game.advanced);
assert.equal(game.players.filter(p => p.faction === 'ecaz').length, 1);
assert.ok(game.players.every(p => p.faction === 'ecaz' ||
CLASSIC_OPPONENTS[p.faction] === true && (!game.advanced || p.faction !== 'harkonnen')));
assert.ok(game.status === 'lobby' || game.status === 'setup' && game.turn === 1 && game.phase === 0 &&
game.setupStage === 'leaderSkills' && game.leaderSkills?.assignments.length === 0,
'Continue only an original undealt lobby or unassigned skill-first setup.');
if (game.status === 'lobby') {
if (!!game.techTokens !== tech) game = applyAction(game, game.host, { type: 'techTokens', enabled: tech });
if (!!game.strongholdCards !== stronghold) game = applyAction(game, game.host, { type: 'strongholdCards', enabled: stronghold });
// Same unused lobby-module declaration as the original Nexus/Skills fixtures.
if (!game.nexusCards) game.nexusCards = { cards: null, phase: null };
} else {
assert.equal(!!game.techTokens, tech); assert.equal(!!game.strongholdCards, stronghold);
assert.ok(game.nexusCards, 'Do not convert a dealt setup into a Nexus game.');
}
return game;
}
export function initializeEcazNexusSkillsModules(options: EcazNexusSkillsModulesOptions = {}): Game {
const game = lobby(options);
return initializeAdvancedNativeSkillsSetup({ family: 'ecaz', initial: game,
rules: game.advanced ? 'advanced' : 'basic', requestedSkill: options.skill ?? (options.kind === 'suk' ? 'suk-graduate' : 'warmaster') });
}
/** Original own-seat offers/IDs are authoritative; a requested missing skill is
* an error, never a redeal, a fallback assignment or a forged selected effect. */
export function completeEcazNexusSkillsModulesSetup(state: Game, options: EcazNexusSkillsModulesOptions = {},
actions?: EcazNexusSkillsModulesStep[]): Game {
let game = reloadEcazSkillsModules(state);
const owner = game.players.find(p => p.faction === 'ecaz')!.id;
const requested = options.skill ?? (options.kind === 'suk' ? 'suk-graduate' : 'warmaster');
for (let count = 0; game.status === 'setup' && count < 200; count++) {
if (game.setupStage === 'leaderSkills') {
const actor = Object.keys(game.leaderSkills!.offers)[0], own = viewGame(game, actor).leaderSkills!;
const offer = game.leaderSkills!.offers[actor];
const skill = actor === owner ? requested : offer.cards.find(c => !own.unavailableSkills?.[c]);
const eligible = new Set(own.eligibleLeaders.map(l => l.id));
const discs = ecazSkillsModulesPlayer(game, actor).leaders.filter(l => eligible.has(l.id));
const leader = actor === owner && options.leader ? discs.find(l => l.id === options.leader)
: discs.sort((a, b) => b.strength - a.strength)[0];
assert.ok(skill && offer.cards.includes(skill) && !own.unavailableSkills?.[skill] && leader);
game = stepEcazNexusSkillsModules(game, { actor, action: { type: 'leaderSkill', event: offer.event, skill, leader: leader.id } }, actions);
} else {
const next = nextEcazNexusSkillsModulesStep(game); assert.ok(next);
game = stepEcazNexusSkillsModules(game, next, actions);
}
}
assert.equal(game.status, 'playing');
assertEcazNexusSkillsModulesCustody(game);
return game;
}
function reserveBoard(game: Game, staging: string[]): void {
for (const p of game.players) {
p.reserves += Object.values(p.forces).reduce((sum, n) => sum + n, 0); p.forces = {};
if (p.elites) { p.elites.reserves += Object.values(p.elites.forces).reduce((sum, n) => sum + n, 0); p.elites.forces = {}; }
if (p.advisors) p.advisors = {};
}
staging.push('Conserve all native board counters into their own reserves; no wallet, phase, turn, module owner, skill, receipt or Duke custody is assigned.');
}
export function placeEcazNexusSkillsModulesForce(game: Game, actor: string, location: string, amount: number, staging: string[]): void {
const p = ecazSkillsModulesPlayer(game, actor);
assert.ok(Number.isSafeInteger(amount) && amount >= 0 && p.reserves >= amount);
p.reserves -= amount; p.forces[location] = (p.forces[location] ?? 0) + amount;
staging.push(`Conserved ${amount} ordinary ${actor} counters from their own reserves to ${location}.`);
}
function orderSpice(game: Game, kinds: ('worm' | 'land')[], staging: string[]): void {
const front = kinds.map(kind => {
const index = game.spiceDeck.findIndex(c => kind === 'worm' ? 'worm' in c : 'territory' in c); assert.ok(index >= 0);
return game.spiceDeck.splice(index, 1)[0];
});
game.spiceDeck.unshift(...front);
staging.push(`Order only existing unplayed original Spice Cards: ${kinds.join(', ')}; played discards remain unchanged.`);
}
export function holdEcazNexusSkillsModulesCard(game: Game, actor: string, predicate: (c: Card) => boolean, staging: string[]): Card {
const p = ecazSkillsModulesPlayer(game, actor), held = p.hand.find(predicate);
if (held) return held;
const index = game.deck.findIndex(predicate); assert.ok(index >= 0 && p.hand.length < handLimit(p));
const card = game.deck.splice(index, 1)[0]; p.hand.push(card);
staging.push(`Conserved original Ecaz33 ${card.id} from the remaining physical deck to ${actor}'s hand.`);
return card;
}
export function ecazNexusSkillsModulesCunningRequest(game: Game, owner: string): EcazNexusSkillsModulesStep {
const offer = viewGame(game, owner).nexusEcazDuke; assert.ok(offer && offer.blocked === null);
return { actor: owner, action: { type: 'nexusEcazDuke', event: offer.event } };
}
/** Stop before the actual human posture choice, never manufacture visibility. */
export function openEcazNexusSkillsModulesPosture(game: Game, owner: string, opponent: string,
stronghold: string, actions?: EcazNexusSkillsModulesStep[]): Game {
assert.ok(game.phase === 6 && ecazNexusSkillsModulesClean(game) && !game.battle);
assert.ok(game.active === owner || game.active === opponent);
const opened = stepEcazNexusSkillsModules(game, { actor: game.active!, action: {
type: 'chooseBattle', territory: stronghold, target: game.active === owner ? opponent : owner } }, actions);
return advanceEcazNexusSkillsModules(opened, g => g.decision?.kind === 'leaderSkillVisibility', actions);
}
export function advanceEcazNexusSkillsModulesToPlans(game: Game, actions?: EcazNexusSkillsModulesStep[]): Game {
return advanceEcazNexusSkillsModules(game, g => !!g.battle && ecazNexusSkillsModulesClean(g) &&
!g.battle.preparation && g.battle.preLeader?.closed !== false, actions);
}
export function revealEcazNexusSkillsModulesBattle(game: Game, plans: readonly EcazNexusSkillsModulesStep[],
actions?: EcazNexusSkillsModulesStep[]): Game {
let state = reloadEcazSkillsModules(game);
for (const plan of plans) {
state = stepEcazNexusSkillsModules(state, plan, actions);
if (!state.battle?.revealed) state = advanceEcazNexusSkillsModulesToPlans(state, actions);
}
assert.ok(state.battle?.revealed);
return state;
}
export function settleEcazNexusSkillsModulesBattle(game: Game,
stop: 'suk' | 'cards' | 'tech' | 'complete' = 'complete', actions?: EcazNexusSkillsModulesStep[]): Game {
return advanceEcazNexusSkillsModules(game, g =>
stop === 'suk' && g.decision?.kind === 'sukRescue' ||
stop === 'cards' && g.decision?.kind === 'battleCards' ||
stop === 'tech' && g.decision?.kind === 'techToken' ||
!g.battle && ecazNexusSkillsModulesClean(g) && !g.pendingTreacheryDiscard, actions);
}
/** Native immutable source facts: no computed/fabricated matching stock. */
export function ecazNexusSkillsModulesSourceFacts(game: Game, owner: string, opponent: string) {
const p = ecazSkillsModulesPlayer(game, owner), enemy = ecazSkillsModulesPlayer(game, opponent);
return structuredClone({
turn: game.turn, phase: game.phase, advanced: game.advanced,
duke: game.dukeVidal, assignment: ecazSkillsModulesTrainer(game, owner),
nativeLeaders: p.leaders, ownWallet: p.spice, enemyWallet: enemy.spice,
ownForces: p.forces, enemyForces: enemy.forces, ownReserves: p.reserves,
ownTanks: p.tanks, enemyTanks: enemy.tanks, ownHand: p.hand, enemyHand: enemy.hand,
heldCards: game.strongholdCards?.owners, controls: strongholdControllers(game.players, false),
enemyTech: ownedTech(game.techTokens, opponent), plans: game.battle?.plans,
losses: game.pendingSukRescue?.losses, battle: game.lastBattleContext,
});
}
export const ecazNexusSkillsModulesCurrentControl = (game: Game) => strongholdControllers(game.players, false);
/** Genuine turn-end readiness, including ordinary set-aside clearing and unused
* Nexus tenure expiry. No phase/turn or custody jump is permitted. */
export function expireEcazNexusSkillsModulesDuke(game: Game, actions?: EcazNexusSkillsModulesStep[]): Game {
const turn = game.turn;
return advanceEcazNexusSkillsModules(game, g => g.turn > turn, actions);
}
/** Native setup -> first Storm -> actual END Mentat -> actual worm/alliance ->
* closing Ecaz draw -> quiet Battle -> living/unclaimed Cunning. Conserved
* staging is labeled below, not claimed to be natural random play history.
* Three seats are needed for a real alliance plus one unallied draw; two-seat
* original setup is supported by initialize/complete, never by a forged draw. */
export function createEcazNexusSkillsModulesFixture(options: EcazNexusSkillsModulesOptions = {}): EcazNexusSkillsModulesFixture {
const actions: EcazNexusSkillsModulesStep[] = [], staging: string[] = [];
const initial = lobby(options), offered = initializeEcazNexusSkillsModules({ ...options, initial });
let game = completeEcazNexusSkillsModulesSetup(offered, options, actions);
const afterSetup = reloadEcazSkillsModules(game);
const owner = game.players.find(p => p.faction === 'ecaz')!.id;
const opponents = game.players.filter(p => p.id !== owner);
const opponent = (opponents.find(p => p.faction === 'emperor') ?? opponents[0]).id;
const observer = opponents.find(p => p.id !== opponent)?.id;
assert.ok(observer, 'A genuine alliance and unallied Ecaz closing draw require three real seats.');
const trainer = ecazSkillsModulesTrainer(game, owner).leader;
const strongholdKind = options.strongholdKind ?? 'arrakeen';
const location = `${strongholdKind}:${territory(strongholdKind).sectors[0]}`;
const firstDial = ['tueks_sietch', 'habbanya_ridge_sietch'].includes(strongholdKind) ? 4 : 0;
reserveBoard(game, staging);
game = advanceEcazNexusSkillsModules(game, g => g.phase === 1 && ecazNexusSkillsModulesClean(g), actions, firstDial);
const afterFirstStorm = reloadEcazSkillsModules(game);
orderSpice(game, ['land', 'land'], staging);
game = advanceEcazNexusSkillsModules(game, g => g.phase === 8 && ecazNexusSkillsModulesClean(g), actions);
reserveBoard(game, staging); placeEcazNexusSkillsModulesForce(game, owner, location, 1, staging);
let beforeFirstMentat: Game | undefined, firstMentatStep: EcazNexusSkillsModulesStep | undefined;
for (let count = 0; game.turn === 1 && count < 200; count++) {
const before = reloadEcazSkillsModules(game), next = nextEcazNexusSkillsModulesStep(game); assert.ok(next);
game = stepEcazNexusSkillsModules(game, next, actions);
if (game.turn === 2) { beforeFirstMentat = before; firstMentatStep = next; }
}
assert.ok(beforeFirstMentat && firstMentatStep);
if (game.strongholdCards) assert.equal(game.strongholdCards.owners[strongholdKind], owner);
const afterFirstMentat = reloadEcazSkillsModules(game);
game = advanceEcazNexusSkillsModules(game, g => g.phase === 1 && ecazNexusSkillsModulesClean(g), actions);
orderSpice(game, ['worm', 'land', 'land'], staging);
game = advanceEcazNexusSkillsModules(game, g => g.nexus === true && !g.spiceWindow && !g.spiceResolution && ecazNexusSkillsModulesClean(g), actions);
const beforeAlliance = reloadEcazSkillsModules(game), allianceActions: EcazNexusSkillsModulesStep[] = [
{ actor: opponent, action: { type: 'alliance', target: observer } },
{ actor: observer, action: { type: 'alliance', target: opponent } },
];
for (const next of allianceActions) game = stepEcazNexusSkillsModules(game, next, actions);
game = advanceEcazNexusSkillsModules(game, g => g.nexusCards?.phase?.stage === 'drawing' && ecazNexusSkillsModulesClean(g), actions);
const cards = game.nexusCards!.cards!, index = cards.deck.indexOf('ecaz'); assert.ok(index >= 0);
cards.deck.unshift(cards.deck.splice(index, 1)[0]);
staging.push('Order the unused original Ecaz Nexus singleton first immediately before its genuine closing draw; no Nexus hand is assigned.');
const drawBefore = reloadEcazSkillsModules(game), drawStep: EcazNexusSkillsModulesStep = { actor: owner,
action: { type: 'nexusCardChoice', turn: game.turn, card: null, choice: 'draw', ownRedraws: 0 } };
game = stepEcazNexusSkillsModules(game, drawStep, actions);
const closingDraw = { before: drawBefore, step: drawStep, after: reloadEcazSkillsModules(game) };
game = advanceEcazNexusSkillsModules(game, g => g.phase === 5 && ecazNexusSkillsModulesClean(g), actions);
reserveBoard(game, staging);
for (const actor of [owner, opponent]) placeEcazNexusSkillsModulesForce(game, actor, location, 8, staging);
// Only controlled battle positions move native cards; fresh CLI deals and
// own-seat offers above remain authoritative and are exported unchanged.
for (const p of game.players) { game.deck.push(...p.hand); p.hand = []; }
staging.push('Before the real Movement closes, return original hands to the same Ecaz33 physical deck, then transfer only the requested native battle cards.');
const worthless = holdEcazNexusSkillsModulesCard(game, owner, c => c.kind === 'worthless', staging);
const shield = options.defense ? holdEcazNexusSkillsModulesCard(game, owner, c => c.kind === 'shield', staging) : undefined;
const poison = options.poison ? holdEcazNexusSkillsModulesCard(game, opponent, c => c.kind === 'poison', staging) : undefined;
game = advanceEcazNexusSkillsModules(game, g => g.phase === 6 && ecazNexusSkillsModulesClean(g), actions);
const beforeCunning = reloadEcazSkillsModules(game), cunningStep = ecazNexusSkillsModulesCunningRequest(game, owner);
assert.equal(game.dukeVidal!.controller, null);
game = stepEcazNexusSkillsModules(game, cunningStep, actions);
const cunning = { before: beforeCunning, step: cunningStep, after: reloadEcazSkillsModules(game) };
const beforeBattle = reloadEcazSkillsModules(game);
game = openEcazNexusSkillsModulesPosture(game, owner, opponent, strongholdKind, actions);
const posture = reloadEcazSkillsModules(game), band = options.band ?? (options.kind === 'suk' ? 'skilled' : 'normal');
for (let count = 0; count < 200; count++) {
if (game.battle && ecazNexusSkillsModulesClean(game) && !game.battle.preparation && game.battle.preLeader?.closed !== false) break;
const next = nextEcazNexusSkillsModulesStep(game); assert.ok(next);
if (next.action.type === 'leaderSkillVisibility' && next.actor === owner) next.action.hide = band === 'skilled';
game = stepEcazNexusSkillsModules(game, next, actions);
}
assert.ok(game.battle && ecazNexusSkillsModulesClean(game) && !game.battle.preparation && game.battle.preLeader?.closed !== false);
const enemy = ecazSkillsModulesPlayer(game, opponent), enemyTrainer = ecazSkillsModulesTrainer(game, opponent).leader;
const defender = enemy.leaders.filter(l => !l.dead && l.id !== enemyTrainer).sort((a, b) =>
options.kind === 'suk' ? a.strength - b.strength : b.strength - a.strength)[0]; assert.ok(defender);
const ownLeader = options.kind === 'suk' ? band === 'skilled' ? trainer
: ecazSkillsModulesPlayer(game, owner).leaders.find(l => l.strength === 4 && l.id !== trainer)!.id : DUKE_VIDAL_ID;
const ownerDial = options.ownerDial ?? (options.kind === 'suk' ? 5 : 0);
const opponentDial = options.opponentDial ?? (options.kind === 'suk' ? 0 : game.battle.attacker === owner ? 2 : 1);
const plans: EcazNexusSkillsModulesStep[] = [
{ actor: owner, action: { type: 'battlePlan', leader: ownLeader, dial: ownerDial,
support: game.advanced ? ownerDial : 0, weapon: worthless.id, defense: shield?.id } },
{ actor: opponent, action: { type: 'battlePlan', leader: defender.id, dial: opponentDial,
support: game.advanced ? opponentDial : 0, weapon: poison?.id } },
];
assertEcazNexusSkillsModulesCustody(game);
return { initial, offered, afterSetup, afterFirstStorm, beforeFirstMentat, firstMentatStep, afterFirstMentat,
beforeAlliance, allianceActions, closingDraw, beforeCunning, cunning, beforeBattle, posture, game,
owner, opponent, observer, trainer, territory: strongholdKind, location, plans, actions, staging };
}