import assert from 'node:assert/strict'; import { applyAction, createGame, initializeFactionExpansionsGameForAudit, initializeStrongholdFactionsGameForAudit, joinGame, newPlayer, viewGame, type Game, } from '../game/engine'; import { territory } from '../game/board'; import { createTechTokens } from '../game/tech-tokens'; import { finishEcazOccupySetup, stageEcazOccupyCard } from './fixture-ecaz-occupy'; import { advanceEcazStronghold, ecazStrongholdFixture, ecazStrongholdSeat as player, openEcazStronghold, type EcazStrongholdFixture, } from './fixture-ecaz-strongholds'; import { advanceMoritaniStrongholds, createMoritaniStrongholdsFixture, holdMoritaniStrongholdsTraitor, nextMoritaniStrongholdsStep, resolveMoritaniStrongholdsBattle, stageMoritaniStrongholdsBattle, type MoritaniStrongholdsFixture, } from './fixture-moritani-strongholds'; import { nextStrongholdFactionsNativeStep, type StrongholdFactionsNativeStep } from './fixture-stronghold-factions'; export type E3NativeTechOptions = { /** Original authenticated lobby or its undealt native setup; identities are retained. */ initial?: Game; native?: 'ecaz' | 'moritani'; advanced?: boolean; strongholds?: boolean; }; const clean = (game: Game) => !game.response && !game.phaseOpening && !game.decision; /** Scope deterministic entropy to the genuine first initializer, never a later deal. */ export function initializeE3NativeTechSetup(options: E3NativeTechOptions = {}): Game { const native = options.native ?? 'ecaz'; const advanced = options.advanced ?? options.initial?.advanced ?? true; const strongholds = options.strongholds ?? false; let game: Game; if (options.initial) game = structuredClone(options.initial); else { game = createGame('E3NATIVETECH', newPlayer(native, native, native), advanced, ['ecaz']); joinGame(game, newPlayer('guild', 'Guild', 'guild')); joinGame(game, newPlayer('emperor', 'Emperor', 'emperor')); if (native === 'ecaz') { for (const [actor, position] of [['ecaz', 6], ['guild', 4], ['emperor', 5]] as const) if (viewGame(game, actor).playerPositions[actor] !== position) game = applyAction(game, actor, { type: 'seatPosition', position }); } } assert.equal(game.advanced, advanced); assert.deepEqual(game.expansions, ['ecaz']); assert.ok(game.players.some(p => p.faction === native)); if (game.status === 'setup') { assert.equal(game.turn, 1); assert.equal(game.phase, 0); assert.ok(game.players.every(p => p.hand.length === 0)); assert.ok(game.techTokens); assert.equal(!!game.strongholdCards, strongholds); return game; } assert.equal(game.status, 'lobby', 'Started games cannot be retrofitted with Tech.'); if (!game.techTokens) game.techTokens = createTechTokens(); for (const p of game.players) if (!p.ready) game = applyAction(game, p.id, { type: 'ready' }); const descriptor = Object.getOwnPropertyDescriptor(crypto, 'getRandomValues'); crypto.getRandomValues = (array: T): T => { assert.ok(array); new Uint8Array(array.buffer, array.byteOffset, array.byteLength).fill(255); return array; }; try { return strongholds ? initializeStrongholdFactionsGameForAudit(game) : initializeFactionExpansionsGameForAudit(game); } finally { if (descriptor) Object.defineProperty(crypto, 'getRandomValues', descriptor); else Reflect.deleteProperty(crypto, 'getRandomValues'); } } function reserveBoard(game: Game): 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 = {}; } } function place(game: Game, actor: string, location: string, count: number): void { const p = player(game, actor); assert.ok(Number.isSafeInteger(count) && count >= 0 && p.reserves >= count); p.reserves -= count; p.forces[location] = count; } function orderBlow(game: Game, worm: boolean, position: number): void { const index = game.spiceDeck.findIndex((card, i) => i >= position && (worm ? 'worm' in card && !card.greatMaker && !card.suppressed : 'territory' in card)); assert.ok(index >= position, 'Only unplayed original Spice Cards are ordered.'); game.spiceDeck.splice(position, 0, game.spiceDeck.splice(index, 1)[0]); } export type EcazNativeTechOptions = Omit & { ecazForces?: number; /** Expose original chooseBattle for the preserved Basic odd-force rejection. */ chooseBattle?: boolean; }; export type EcazNativeTechFixture = EcazStrongholdFixture & { battleAction: StrongholdFactionsNativeStep }; function battleChoice(game: Game, kind: string): StrongholdFactionsNativeStep { const choice = viewGame(game, game.active!).battleChoices.find(b => b.territory === kind); assert.ok(choice, 'The actual conserved armies must quote an original battle.'); return { actor: choice.chooser, action: { type: 'chooseBattle', territory: kind, target: choice.attacker === choice.chooser ? choice.defender : choice.attacker } }; } /** Original setup, worm Nexus, reciprocal alliance and (when selected) real * Stronghold claim. Controlled board/card custody follows setup; wallets, phases, * Tech owners and aftermath receipts are never assigned. */ export function createEcazNativeTechFixture(options: EcazNativeTechOptions = {}): EcazNativeTechFixture { const setup = initializeE3NativeTechSetup({ ...options, native: 'ecaz' }); if (options.strongholds) { const f = ecazStrongholdFixture({ initial: setup, kind: 'sietch_tabr', ecazForces: options.ecazForces }); return { ...f, battleAction: battleChoice(f.beforeBattle, f.territory) }; } const initial = structuredClone(setup), staging: string[] = []; let game = finishEcazOccupySetup(setup); const afterSetup = structuredClone(game); const ecaz = game.players.find(p => p.faction === 'ecaz')!.id; const ally = game.players.find(p => p.faction === 'guild')!.id; const opponent = game.players.find(p => p.faction === 'emperor')!.id; assert.equal(game.players.length, 3); assert.ok(ecaz && ally && opponent); const kind = 'sietch_tabr', location = `${kind}:${territory(kind).sectors[0]}`; reserveBoard(game); orderBlow(game, false, 0); orderBlow(game, false, 1); staging.push('After genuine setup: return physical counters to their own reserves; retain native wallets, cards, leaders and identities.'); game = advanceEcazStronghold(game, g => g.turn === 2 && g.phase === 0 && clean(g)); orderBlow(game, true, 0); orderBlow(game, false, 1); orderBlow(game, false, 2); game = advanceEcazStronghold(game, g => g.phase === 1 && !!g.nexus && clean(g) && !g.spiceWindow && !g.spiceResolution); game = applyAction(game, ecaz, { type: 'alliance', target: ally }); game = applyAction(game, ally, { type: 'alliance', target: ecaz }); game = advanceEcazStronghold(game, g => g.phase === 8 && clean(g)); reserveBoard(game); place(game, ecaz, location, 1); place(game, ally, location, 1); let beforeMentat: Game | undefined, mentatStep: StrongholdFactionsNativeStep | undefined; for (let count = 0; game.turn === 2 && count < 200; count++) { const next = nextStrongholdFactionsNativeStep(game); assert.ok(next); const before = structuredClone(game); game = applyAction(game, next.actor, next.action); if (game.turn === 3) { beforeMentat = before; mentatStep = next; } } assert.ok(beforeMentat && mentatStep); const afterMentat = structuredClone(game); orderBlow(game, false, 0); orderBlow(game, false, 1); game = advanceEcazStronghold(game, g => g.phase === 5 && clean(g)); const ecazForces = options.ecazForces ?? (game.advanced ? 5 : 4), allyForces = 4; reserveBoard(game); place(game, ecaz, location, ecazForces); place(game, ally, location, allyForces); place(game, opponent, location, 8); stageEcazOccupyCard(game, opponent, card => card.effect === 'karama'); staging.push(`Before original movement completion: conserve ${ecazForces} Ecaz, four Guild and eight Emperor counters in Tabr; conserved Karama custody exposes original Occupy response.`); game = advanceEcazStronghold(game, g => g.phase === 6 && clean(g)); const beforeBattle = structuredClone(game), battleAction = battleChoice(game, kind); if (options.chooseBattle !== false) game = applyAction(game, battleAction.actor, battleAction.action); return { initial, afterSetup, beforeMentat, mentatStep, afterMentat, beforeBattle, game, ecaz, ally, opponent, territory: kind, location, ecazForces, allyForces, staging, battleAction }; } export function revealEcazNativeTechBattle(f: EcazNativeTechFixture, lead: 'ecaz' | 'ally', victoryPosition = false) { const actor = lead === 'ecaz' ? f.ecaz : f.ally; const staged = { ...f, game: structuredClone(f.game) }; if (victoryPosition) { const holds = ['arrakeen', 'carthag', 'habbanya_ridge_sietch', 'tueks_sietch'] .filter(id => territory(id).sectors[0] !== staged.game.storm); for (const [index, owner] of [f.ecaz, f.ally].entries()) { const hold = holds[index]; assert.ok(hold); place(staged.game, owner, `${hold}:${territory(hold).sectors[0]}`, 1); f.staging.push(`Before selected-lead choice: conserve one reserve ${owner} counter alone in ${hold}; three alliance holds but only one jointly occupied hold.`); } } const shield = stageEcazOccupyCard(staged.game, actor, c => c.kind === 'shield'); let game = openEcazStronghold(staged, lead); const before = structuredClone(game), profile = viewGame(game, actor).battle!.ecazOccupy!.profile!; const own = player(game, actor).leaders.filter(l => !l.dead && !l.usedAt).sort((a, b) => b.strength - a.strength)[0]; const enemy = player(game, f.opponent).leaders.filter(l => !l.dead && !l.usedAt).sort((a, b) => a.strength - b.strength)[0]; const support = game.advanced ? Math.min(3, player(game, actor).spice) : 0; const variableDial = game.advanced ? (3 + support) / 2 : 3; assert.ok(own && enemy); game = applyAction(game, actor, { type: 'battlePlan', leader: own.id, dial: profile.fixedEcazDial + variableDial, support, defense: shield }); game = applyAction(game, f.opponent, { type: 'battlePlan', leader: enemy.id, dial: game.advanced ? 2 : 0, support: 0 }); assert.ok(game.battle?.revealed); const revealed = structuredClone(game); for (const voter of viewGame(game, actor).battle!.traitorVoters) game = applyAction(game, voter, { type: 'traitorCall', call: false }); assert.equal(game.decision?.kind, 'battleCards', 'The original winner-card window precedes Tech transfer.'); return { before, revealed, pending: game, actor, shield }; } export type MoritaniNativeTechOptions = Omit & { normalCall?: boolean }; /** Genuine six-disc Terror supply: the first printed Mentat placement leaves * five supply discs and one placed disc. Advanced losses use original assassination. */ export function createMoritaniNativeTechFixture(options: MoritaniNativeTechOptions = {}): MoritaniStrongholdsFixture { const setup = initializeE3NativeTechSetup({ ...options, native: 'moritani' }); if (options.strongholds) return createMoritaniStrongholdsFixture({ initial: setup, normalCall: options.normalCall }); let game = setup; const initial = structuredClone(game), sourceActions: StrongholdFactionsNativeStep[] = [], staging: string[] = []; const run = (step: StrongholdFactionsNativeStep) => { sourceActions.push(structuredClone(step)); game = applyAction(game, step.actor, step.action); }; for (let count = 0; game.status === 'setup' && count < 200; count++) { const next = nextMoritaniStrongholdsStep(game); assert.ok(next); run(next); } assert.equal(game.status, 'playing', 'Genuine native Moritani setup must complete.'); const afterSetup = structuredClone(game); const moritani = game.players.find(p => p.faction === 'moritani')!.id; const opponent = game.players.find(p => p.faction === 'guild')!.id; assert.equal(game.players.length, 3); assert.ok(moritani && opponent); orderBlow(game, false, 0); orderBlow(game, false, 1); for (let count = 0; !(game.phase === 8 && game.decision?.kind === 'moritaniPlacement') && count < 1800; count++) { const next = nextMoritaniStrongholdsStep(game); assert.ok(next); if (next.action.type === 'stormDial' && game.turn === 1) next.action.amount = 4; run(next); } assert.equal(game.decision?.kind, 'moritaniPlacement'); const beforePlacement = structuredClone(game), token = game.moritaniTerror!.tokens.find(t => t.status === 'available'); assert.ok(token); run({ actor: moritani, action: { type: 'decision', token: token.id, territory: 'arrakeen' } }); game = advanceMoritaniStrongholds(game, g => g.phase === 8 && clean(g), sourceActions); const afterPlacement = structuredClone(game), kind = 'tueks_sietch'; reserveBoard(game); place(game, moritani, `${kind}:${territory(kind).sectors[0]}`, 1); place(game, opponent, 'carthag:11', 1); let beforeFirstMentat: Game | undefined, firstMentatStep: StrongholdFactionsNativeStep | undefined; for (let count = 0; game.turn === 1 && count < 200; count++) { const next = nextMoritaniStrongholdsStep(game); assert.ok(next); const before = structuredClone(game); sourceActions.push(structuredClone(next)); game = applyAction(game, next.actor, next.action); if (game.turn === 2) { beforeFirstMentat = before; firstMentatStep = next; } } assert.ok(beforeFirstMentat && firstMentatStep); const afterFirstMentat = structuredClone(game); orderBlow(game, false, 0); orderBlow(game, false, 1); game = advanceMoritaniStrongholds(game, g => g.phase === 5 && clean(g), sourceActions); stageMoritaniStrongholdsBattle(game, moritani, opponent, kind, staging); const own = player(game, moritani).leaders.filter(l => !l.dead).sort((a, b) => a.strength - b.strength)[0]; const enemy = player(game, opponent).leaders.filter(l => !l.dead).sort((a, b) => b.strength - a.strength)[0]; const target = player(game, opponent).leaders.find(l => !l.dead && l.id !== enemy.id)!; assert.ok(own && enemy && target); holdMoritaniStrongholdsTraitor(game, moritani, options.normalCall ? enemy.id : target.id, staging); const moritaniCards = [stageEcazOccupyCard(game, moritani, c => c.kind === 'worthless')]; const opponentCards = [stageEcazOccupyCard(game, opponent, c => c.kind === 'worthless')]; game = advanceMoritaniStrongholds(game, g => g.phase === 6 && clean(g), sourceActions); const beforeBattle = structuredClone(game), battleAction = battleChoice(game, kind); run(battleAction); game = advanceMoritaniStrongholds(game, g => !!g.battle && !g.battle.revealed && !nextMoritaniStrongholdsStep(g), sourceActions); const planActions: StrongholdFactionsNativeStep[] = [ { actor: moritani, action: { type: 'battlePlan', leader: own.id, dial: 1, support: game.advanced ? 1 : 0, weapon: moritaniCards[0] } }, { actor: opponent, action: { type: 'battlePlan', leader: enemy.id, dial: 3, support: game.advanced ? 3 : 0, weapon: opponentCards[0] } }, ]; const prepared = structuredClone(game); for (const plan of planActions) run(plan); assert.ok(game.battle?.revealed); const revealed = structuredClone(game); const pending = resolveMoritaniStrongholdsBattle(game, moritani, options.normalCall, sourceActions); return { initial, afterSetup, beforePlacement, afterPlacement, beforeFirstMentat, firstMentatStep, afterFirstMentat, beforeBattle, game: prepared, revealed, pending, moritani, opponent, holder: moritani, kind, target: target.id, moritaniLeader: own.id, opponentLeader: enemy.id, moritaniCards, opponentCards, battleAction, planActions, sourceActions, staging }; }