Files
Dune/tests/fixture-basic-native-mirror.ts
T
Quintine 19e554666e Admit Mirror and Stone in Basic native E3 battles
Reuse original copied-weapon and typed coalition accounting for bare Basic
Ecaz/Moritani and selected Ix. Preserve optional-module guards and physical
card identity, actual force-owner commitments and native continuations.
2026-10-24 16:34:19 +11:00

125 lines
7.7 KiB
TypeScript

import assert from 'node:assert/strict';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { type Card } from '../game/cards';
import { basicEcazNativeFixture, nextBasicEcazNativeStep } from './fixture-basic-ecaz-native';
import { stageEcazOccupyCard } from './fixture-ecaz-occupy';
export type BasicNativeMirrorOptions = {
owner?: 'ecaz' | 'ally' | 'moritani';
weapon?: 'poisonTooth' | 'stoneBurner' | 'projectile' | 'artillery';
};
export type BasicNativeMirrorStep = { actor: string; action: Action };
export const BASIC_NATIVE_MIRROR = 'richese-mirror-weapon';
export const basicNativeMirrorPlayer = (game: Game, actor: string) => {
const player = game.players.find(player => player.id === actor);
assert.ok(player); return player;
};
export const basicNativeMirrorClean = (game: Game) =>
!game.response && !game.phaseOpening && !game.decision &&
!game.truthtrance && !game.pendingTreacheryDiscard;
export const basicNativeMirrorPlansOpen = (game: Game) =>
!!game.battle && !game.battle.preparation &&
(!game.battle.preLeader || game.battle.preLeader.closed) && basicNativeMirrorClean(game);
/** Physical inventory, including native cache/retired cards. No duplicated
* original identity may be introduced by the explicitly controlled staging. */
export function basicNativeMirrorInventory(game: Game): string[] {
const ids = [...game.deck, ...game.discard, ...(game.ixSetupCards ?? []),
...(game.richeseCache ?? []), ...(game.richeseRemoved ?? []),
...(game.auction?.cards.slice(game.auction.index) ?? []),
...game.players.flatMap(player => player.hand)].map(card => card.id).sort();
assert.equal(new Set(ids).size, ids.length, 'Every original physical card has exactly one current location');
return ids;
}
export function actBasicNativeMirror(game: Game, actor: string, action: Action,
actions: BasicNativeMirrorStep[] = []): Game {
const next = applyAction(game, actor, action);
actions.push({ actor, action: structuredClone(action) }); return next;
}
/** Only actual native continuations. A plan, copied/original weapon choice,
* typed physical allocation or casualty remains an explicit caller action. */
export function advanceBasicNativeMirror(game: Game, until: (state: Game) => boolean,
actions: BasicNativeMirrorStep[] = []): Game {
for (let count = 0; count < 250; count++) {
if (until(game)) return game;
const decision = game.decision;
assert.ok(!decision || !['poisonTooth', 'stoneBurner', 'stoneAllocation', 'battleLosses']
.includes(decision.kind), 'Choose the actual weapon/allocation/loss explicitly');
const step = nextBasicEcazNativeStep(game);
assert.ok(step, 'The native battle continuation must retain a real control');
assert.ok(!['battlePlan', 'chooseBattle'].includes(step.action.type),
'The caller owns real battle admission and sealed plans');
game = actBasicNativeMirror(game, step.actor, step.action, actions);
}
throw Error('The native Mirror continuation did not reach its finite boundary');
}
function stagedPhysicalCard(game: Game, owner: string, predicate: (card: Card) => boolean): string {
const before = basicNativeMirrorInventory(game), player = basicNativeMirrorPlayer(game, owner);
const cache = game.richeseCache?.findIndex(predicate) ?? -1;
let id: string;
if (cache >= 0) {
const card = game.richeseCache!.splice(cache, 1)[0];
if (player.hand.length >= 4) game.deck.push(player.hand.shift()!);
player.hand.push(card); id = card.id;
} else id = stageEcazOccupyCard(game, owner, predicate);
assert.ok(player.hand.length <= 4, 'Controlled rule hands retain an actual legal capacity');
assert.deepEqual(basicNativeMirrorInventory(game), before);
return id;
}
/** Original public Basic native setup/alliance and actual battle admission.
* The shared fixture explicitly stages a conserved board, thirty-spice bank
* budget and original cards. This adds named physical weapon transfers, not
* an unstaged acquisition or complete-game certificate. */
export function createBasicNativeMirrorFixture(options: BasicNativeMirrorOptions = {}) {
const role = options.owner ?? 'ecaz', weapon = options.weapon ?? 'poisonTooth';
const fixture = basicEcazNativeFixture({ allyFaction: 'ixians', opponentFaction: 'choam', mixedIxians: true });
const actions: BasicNativeMirrorStep[] = [], staging = [...fixture.staging];
const originalInventory = basicNativeMirrorInventory(fixture.source);
let game = structuredClone(role === 'moritani' ? fixture.beforeBattle : fixture.game);
let beforeBattle = structuredClone(fixture.beforeBattle);
if (role === 'moritani') {
const previous = basicNativeMirrorPlayer(game, fixture.opponent);
previous.reserves += previous.forces[fixture.location] ?? 0;
delete previous.forces[fixture.location];
if (previous.elites) {
previous.elites.reserves += previous.elites.forces[fixture.location] ?? 0;
delete previous.elites.forces[fixture.location];
}
const moritani = basicNativeMirrorPlayer(game, 'moritani');
assert.equal(moritani.forces[fixture.location] ?? 0, 0);
assert.ok(moritani.reserves >= 5);
moritani.reserves -= 5; moritani.forces[fixture.location] = 5;
staging.push('Before actual admission, return the original CHOAM Carthag counters to their own reserves and place five conserved Moritani reserves there; preserve the actual Ecaz/Ixian Nexus alliance, native phase and battle order.');
beforeBattle = structuredClone(game);
const choice = viewGame(game, game.active!).battleChoices.find(choice =>
choice.territory === fixture.territory && [choice.attacker, choice.defender].includes('moritani'));
assert.ok(choice, 'The conserved Moritani army creates a real native battle choice');
game = actBasicNativeMirror(game, choice.chooser, { type: 'chooseBattle', territory: fixture.territory,
target: choice.attacker === choice.chooser ? choice.defender : choice.attacker }, actions);
}
assert.equal(game.decision?.kind, 'ecazBattleLead');
const lead = role === 'ecaz' ? fixture.ecaz : fixture.ally;
game = actBasicNativeMirror(game, fixture.ecaz,
{ type: 'decision', event: game.decision!.event, lead }, actions);
game = advanceBasicNativeMirror(game, basicNativeMirrorPlansOpen, actions);
assert.ok(game.battle && !game.battle.revealed && Object.keys(game.battle.plans).length === 0);
const owner = role === 'moritani' ? 'moritani' : lead;
const opponent = game.battle.attacker === owner ? game.battle.defender : game.battle.attacker;
assert.ok([game.battle.attacker, game.battle.defender].includes(owner));
const mirror = stagedPhysicalCard(game, owner, card => card.id === BASIC_NATIVE_MIRROR);
const original = stagedPhysicalCard(game, opponent,
card => weapon === 'stoneBurner' ? card.id === 'richese-stone-burner' : card.kind === weapon);
staging.push(`At the actual clean unsealed-plan boundary, conserve the original physical Mirror into ${owner}'s hand and the original ${weapon} into ${opponent}'s hand, exchanging an original held card with the deck only when capacity requires it. These are labelled rule-case acquisitions, not a played cache purchase or newly manufactured cards.`);
assert.equal(game.advanced, false);
assert.deepEqual(game.players.map(player => player.faction), ['ecaz', 'ixians', 'choam', 'richese', 'moritani', 'tleilaxu']);
assert.deepEqual(basicNativeMirrorInventory(game), originalInventory);
return { source: fixture.source, afterSetup: fixture.afterSetup, beforeBattle, game,
owner, opponent, ecaz: fixture.ecaz, ally: fixture.ally, richese: fixture.richese,
territory: fixture.territory, location: fixture.location, mirror, original, weapon, role,
originalInventory, actions, staging };
}
export type BasicNativeMirrorFixture = ReturnType<typeof createBasicNativeMirrorFixture>;