Files
Dune/tests/fixture-nexus-moritani-atomics.ts
Quintine bb9d292c4c Compose native Ecaz Betrayal and Moritani Atomics
Retain original selected modules, typed native returns, advisors and hidden No-Field recall. Preserve a full unspent Guild/Sapho opening, paid ordered arrivals, capped provisional No-Field destruction and native World Sneak withdrawal. Bind desert Aftermath to its original Moritani Cunning placement.

Validation: frozen 16-file115/115, types/lint/build, eight finite original-action and GameTable SSR programmes, four minimal legal policies. Document provisional overlaps and corrected genuine recipes. No stored-game change or deployment; full combinations, comprehensive assurance and deployed acceptance remain required.
2026-10-18 11:21:28 +11:00

133 lines
9.2 KiB
TypeScript

import assert from 'node:assert/strict';
import { viewGame, type Action, type Game } from '../game/engine';
import { territory } from '../game/board';
import { nativeShipmentSources } from '../game/homeworld-options';
import { nexusMoritaniAction } from '../game/nexus-moritani-options';
import { validateNexusMoritani } from '../game/nexus-moritani';
import { nexusMoritaniRecordSignature } from '../game/terror-location';
import {
createE3IndependentNexusCardSetup, e3IndependentNexusCardPlayer as player,
type E3IndependentNexusCardOptions,
} from './fixture-e3-independent-nexus-cards';
import {
actAtomicsModules, advanceAtomicsModules, atomicsModulesClean,
finishAtomicsModulesReveal, type AtomicsModulesStep,
} from './fixture-moritani-atomics-modules';
export type NexusMoritaniAtomicsOptions = E3IndependentNexusCardOptions;
export const NEXUS_ATOMICS_TERRITORY = 'pasty_mesa';
/** Only unopened original Spice Cards are ordered. In particular, the real
* six-counter Moritani start is never exposed to a selected worm or sand blow. */
function orderOriginalBlow(game: Game, worm = false): void {
const front: Game['spiceDeck'] = [];
if (worm) {
const index = game.spiceDeck.findIndex(card => 'worm' in card && !card.greatMaker && !card.suppressed && !card.thumper);
assert.ok(index >= 0); front.push(game.spiceDeck.splice(index, 1)[0]);
}
for (let count = 0; count < (game.advanced ? 2 : 1); count++) {
const index = game.spiceDeck.findIndex(card => 'territory' in card && !card.discovery &&
!['pasty_mesa', 'tueks_sietch'].includes(card.territory));
assert.ok(index >= 0); front.push(game.spiceDeck.splice(index, 1)[0]);
}
game.spiceDeck.unshift(...front);
}
function nativeShipment(game: Game, destination: string, sector: number, amount: number): Action {
const sources = game.homeworlds ? nativeShipmentSources(viewGame(game, 'guild'), amount, 0) : undefined;
assert.ok(!game.homeworlds || sources);
return { type: 'ship', territory: destination, sector, amount, elite: 0, allyPayment: 0,
...(sources ? { homeworldSources: sources } : {}) };
}
/** Fresh selected E3/independent/Nexus setup, real closing draw and own
* Moritani Cunning. No dealt hand, spice, army, token location or receipt is
* assigned. The desert is Moritani's original physical starting territory. */
export function createNexusMoritaniAtomicsFixture(options: NexusMoritaniAtomicsOptions = {}) {
const source = createE3IndependentNexusCardSetup(options), actions = source.actions;
let game = structuredClone(source.game);
const reach = (until: (state: Game) => boolean) => { game = advanceAtomicsModules(game, until, actions); };
const act = (actor: string, action: Action) => { game = actAtomicsModules(game, actor, action, actions); };
orderOriginalBlow(game);
reach(state => state.turn === 2 && state.phase === 1 && atomicsModulesClean(state));
orderOriginalBlow(game, true);
reach(state => state.nexus && !state.spiceWindow && !state.spiceResolution && atomicsModulesClean(state));
const beforeAlliance = structuredClone(game);
act('guild', { type: 'alliance', target: 'emperor' });
act('emperor', { type: 'alliance', target: 'guild' });
reach(state => state.nexusCards?.phase?.stage === 'drawing' && atomicsModulesClean(state));
const deck = game.nexusCards!.cards!.deck, index = deck.indexOf('moritani'); assert.ok(index >= 0);
deck.unshift(deck.splice(index, 1)[0]);
const drawBefore = structuredClone(game), drawStep: AtomicsModulesStep = { actor: 'moritani', action: {
type: 'nexusCardChoice', turn: game.turn, card: null, choice: 'draw', ownRedraws: 0 } };
act(drawStep.actor, drawStep.action);
const drawAfter = structuredClone(game);
// Basic cannot use the Advanced three-territory flight from the original
// Tuek's army. Earn a separate adjacent counter with an earlier paid native
// shipment; the original five remain at Tuek's throughout the Basic scene.
let movementSupply: { before: Game; step: AtomicsModulesStep; after: Game } | null = null;
if (!game.advanced) {
reach(state => state.turn === 2 && state.phase === 5 && state.active === 'guild' && atomicsModulesClean(state));
const before = structuredClone(game), step: AtomicsModulesStep = {
actor: 'guild', action: nativeShipment(game, 'south_mesa', 6, 1) };
act(step.actor, step.action); reach(atomicsModulesClean);
assert.equal(player(game, 'guild').forces['south_mesa:6'], 1);
assert.equal(player(game, 'guild').forces['tueks_sietch:5'], 5);
movementSupply = { before, step, after: structuredClone(game) };
}
reach(state => state.turn === 2 && !state.response && !state.phaseOpening && state.decision?.kind === 'moritaniPlacement');
const atomics = game.moritaniTerror!.tokens.find(token => token.kind === 'atomics' && token.status === 'available'); assert.ok(atomics);
const own = viewGame(game, 'moritani'), offer = own.nexusMoritani; assert.ok(offer);
const cunning = nexusMoritaniAction(own, offer.event, atomics.id, NEXUS_ATOMICS_TERRITORY); assert.ok(cunning);
const placementBefore = structuredClone(game), placementStep: AtomicsModulesStep = { actor: 'moritani', action: cunning };
act(placementStep.actor, placementStep.action);
const placementPending = structuredClone(game);
reach(state => state.nexusMoritaniLast?.stage === 'complete' && atomicsModulesClean(state));
const placementAfter = structuredClone(game), record = game.nexusMoritaniHistory?.find(record =>
record.receipt.event === game.nexusMoritaniLast!.event && record.stage === 'complete');
assert.ok(record, 'The completed Cunning pointer must identify its original committed placement');
assert.equal(record.signature, nexusMoritaniRecordSignature(record));
validateNexusMoritani(game, record.receipt);
const receipt = structuredClone(record.receipt);
reach(state => state.turn === 3 && state.phase === 1 && atomicsModulesClean(state));
orderOriginalBlow(game);
reach(state => state.turn === 3 && state.phase === 5 && state.active === 'guild' && atomicsModulesClean(state));
assert.equal(player(game, 'moritani').forces['pasty_mesa:5'], 6);
assert.equal(player(game, 'guild').forces['tueks_sietch:5'], 5);
const sector = territory(NEXUS_ATOMICS_TERRITORY).sectors.find(sector => sector !== game.storm); assert.ok(sector !== undefined);
const beforeEntry = structuredClone(game), entry: AtomicsModulesStep = {
actor: 'guild', action: nativeShipment(game, NEXUS_ATOMICS_TERRITORY, sector, 3) };
act(entry.actor, entry.action);
reach(state => !state.response && !state.phaseOpening && state.decision?.kind === 'moritaniTerror');
source.staging.push('Keep original armies; use an actual worm and reciprocal Guild/Emperor alliance to draw the unopened Moritani Nexus, then spend it on original Mentat supply Atomics in Pasty Mesa and enter with three paid Guild counters.');
return { ...source, game, actions, beforeAlliance, atomics: atomics.id, receipt, movementSupply,
draw: { before: drawBefore, step: drawStep, after: drawAfter },
placement: { before: placementBefore, step: placementStep, pending: placementPending, after: placementAfter },
beforeEntry, entry };
}
/** Finite native runtime smoke: activate desert Atomics, retain the unused
* Guild movement, then reach a later fresh shipment and provide the real
* forbidden Aftermath request. A separate original paid shipment remains legal. */
export function continueNexusMoritaniAtomics(game: Game, actions: AtomicsModulesStep[] = []) {
const activation = finishAtomicsModulesReveal(game, actions); game = activation.game;
const beforeMove = structuredClone(game), sector = territory(NEXUS_ATOMICS_TERRITORY).sectors.find(sector => sector !== game.storm);
assert.ok(sector !== undefined);
const from = game.advanced ? 'tueks_sietch:5' : 'south_mesa:6', amount = game.advanced ? 5 : 1;
assert.equal(player(game, 'guild').forces[from], amount);
const movement: AtomicsModulesStep = { actor: 'guild', action: { type: 'move', from,
amount, territory: NEXUS_ATOMICS_TERRITORY, sector } };
game = actAtomicsModules(game, movement.actor, movement.action, actions);
game = advanceAtomicsModules(game, atomicsModulesClean, actions);
const afterMove = structuredClone(game), nextTurn = game.turn + 1;
game = advanceAtomicsModules(game, state => state.turn === nextTurn && state.phase === 1 && atomicsModulesClean(state), actions);
orderOriginalBlow(game);
game = advanceAtomicsModules(game, state => state.turn === nextTurn && state.phase === 5 && state.active === 'guild' && atomicsModulesClean(state), actions);
assert.equal(player(game, 'guild').shipped, false);
const forbiddenSector = territory(NEXUS_ATOMICS_TERRITORY).sectors.find(sector => sector !== game.storm); assert.ok(forbiddenSector !== undefined);
const beforeShipment = structuredClone(game), blockedShipment = nativeShipment(game, NEXUS_ATOMICS_TERRITORY, forbiddenSector, 1);
const allowedShipment: AtomicsModulesStep = { actor: 'guild', action: nativeShipment(game, 'polar_sink', 0, 1) };
game = actAtomicsModules(game, allowedShipment.actor, allowedShipment.action, actions);
game = advanceAtomicsModules(game, atomicsModulesClean, actions);
return { game, actions, activation, beforeMove, movement, afterMove, beforeShipment, blockedShipment, allowedShipment };
}