Files
Dune/tests/fixture-fremen-cunning-modules.ts
T
Quintine b4157aaa9b Connect native Fremen Nexus effects across selected modules
Retain actual Cunning appearances, paid Karama and typed remote riding with optional Sandmaster collection and source-parent HMS entry. Preserve Betrayal movement sources and apply its declared worm restriction to Great Maker reserve riding without suppressing votes or casualties. Completed Maker history no longer blocks a later ordinary offer.

Validation: frozen 14-file86/86, types/lint/build, nine finite original-action/rendered-control programmes, four minimal legal policies. Discovery destination proof remains focused quotes pending actual arrival; full combinations, assurance and deployed acceptance remain required. No saved-game, restart or deployment operation.
2026-10-18 11:35:59 +11:00

262 lines
18 KiB
TypeScript

import assert from 'node:assert/strict';
import { applyAction, createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
import type { FactionId } from '../game/catalog';
import { territory } from '../game/board';
import { nativeShipmentSources } from '../game/homeworld-options';
import { sandmasterCunningCollection } from '../game/sandmaster-worm';
import type { LeaderSkillId } from '../game/leader-skill-cards';
import { skillOfferRolls } from './fixture-independent-card-modules';
import { nextPairedChoamSkillsStep } from './fixture-paired-choam-nexus-skills';
export type FremenCunningModulesStep = { actor: string; action: Action };
export type FremenCunningModulesOptions = {
advanced?: boolean;
expansions?: Game['expansions'];
factions?: readonly FactionId[];
skills?: boolean;
homeworlds?: boolean;
tech?: boolean;
strongholds?: boolean;
hmsAtSource?: boolean;
};
export const fremenCunningModulesPlayer = (game: Game, actor: string) => {
const player = game.players.find(player => player.id === actor); assert.ok(player); return player;
};
export const fremenCunningModulesClean = (game: Game) => !game.phaseOpening && !game.response &&
!game.decision && !game.truthtrance && !game.pendingTreacheryDiscard && !game.pendingGuildBetrayal &&
!game.pendingNexusIxianReplacement && !game.pendingNexusIxianBetrayal && !game.pendingNexusHarkonnenBetrayal;
/** Scope the real undealt skill lottery; all remaining scalar shuffles retain
* their original order. UUIDs and all other random byte arrays stay native. */
function originalLottery<T>(rolls: readonly number[], operation: () => T): T {
const native = crypto.getRandomValues.bind(crypto), descriptor = Object.getOwnPropertyDescriptor(crypto, 'getRandomValues');
let cursor = 0;
crypto.getRandomValues = <V extends ArrayBufferView | null>(array: V): V => {
if (array instanceof Uint32Array && array.length === 1) array[0] = rolls[cursor++] ?? 0xffffffff;
else Reflect.apply(native, crypto, [array]);
return array;
};
try { return operation(); }
finally {
if (descriptor) Object.defineProperty(crypto, 'getRandomValues', descriptor);
else Reflect.deleteProperty(crypto, 'getRandomValues');
}
}
export function actFremenCunningModules(game: Game, actor: string, action: Action, actions?: FremenCunningModulesStep[]): Game {
actions?.push({ actor, action: structuredClone(action) });
return originalLottery([], () => applyAction(game, actor, action));
}
/** Original no-spend/no-movement progression stops at owned Terror, Ambassador
* and the remote ride. The real first empty appearance has no held Fremen card
* and is declined; no private Nexus entitlement is manufactured for it. */
export function nextFremenCunningModulesStep(game: Game): FremenCunningModulesStep | null {
if (game.pendingGuildBetrayal) {
const frame = game.pendingGuildBetrayal, actor = frame.required.find(actor => !frame.passed.includes(actor)); assert.ok(actor);
return { actor, action: { type: 'guildBetrayalPass', event: frame.invoice.event } };
}
if (game.pendingNexusIxianReplacement) return { actor: game.pendingNexusIxianReplacement.buyer,
action: { type: 'nexusIxianReplacementPass', event: game.pendingNexusIxianReplacement.event } };
if (!game.response && !game.phaseOpening && !game.pendingTreacheryDiscard) {
const decision = game.decision;
if (decision?.kind === 'nexusFremenCunningRide' || decision?.kind === 'moritaniTerror' || decision?.kind === 'ecazAmbassador') return null;
if (decision?.kind === 'nexusFremenCunningOffer') return { actor: decision.player,
action: { type: 'decision', event: decision.event, accept: false } };
if (decision?.kind === 'homeworldShipmentGuild') return { actor: decision.player,
action: { type: 'decision', event: decision.event, allow: true } };
if (decision?.kind === 'homeworldDefense') return { actor: decision.player,
action: { type: 'decision', event: decision.event, use: false } };
if (decision?.kind === 'moritaniPlacement' || decision?.kind === 'ecazPlacement' ||
decision?.kind === 'homeworldRevivalDeployment' || decision?.kind === 'caladanReinforcement' || decision?.kind === 'grummanCollection')
return { actor: decision.player, action: { type: 'decision', event: 'event' in decision ? decision.event : undefined, decline: true } };
if (decision?.kind === 'guildTiming') return { actor: decision.player, action: { type: 'decision', take: false } };
if (decision?.kind === 'wormRide' || decision?.kind === 'wormPlacement' || decision?.kind === 'advisor' ||
decision?.kind === 'intrusion' || decision?.kind === 'advisorBattle')
return { actor: decision.player, action: { type: 'decision', accept: false } };
if (decision?.kind === 'leaderSkillVisibility') return { actor: decision.player,
action: { type: 'leaderSkillVisibility', event: decision.event, hide: false } };
if (decision?.kind === 'mentatQuestion') return { actor: decision.player,
action: { type: 'decision', event: decision.event, decline: true } };
if (decision?.kind === 'moritaniSetup') return { actor: decision.player,
action: { type: 'decision', territory: 'pasty_mesa', sector: 5 } };
}
return nextPairedChoamSkillsStep(game);
}
export function advanceFremenCunningModules(game: Game, until: (state: Game) => boolean,
actions: FremenCunningModulesStep[] = []): Game {
for (let count = 0; count < 2400; count++) {
if (until(game)) return game;
assert.notEqual(game.status, 'finished', 'The genuine Fremen Cunning chronology ended early');
const next = nextFremenCunningModulesStep(game);
assert.ok(next, `Choose the original ${game.decision?.kind ?? 'Battle Plan'} at ${game.turn}/${game.phase}`);
game = actFremenCunningModules(game, next.actor, next.action, actions);
}
throw Error('The genuine Fremen Cunning chronology did not reach its boundary');
}
/** Move original unopened physical Spice Cards only. Never assign a discard
* target, board pile, appearance identity or historical worm record. */
function orderOriginalSpice(game: Game, entries: readonly string[]): void {
const front: Game['spiceDeck'] = [];
for (const entry of entries) {
const index = game.spiceDeck.findIndex(card => entry === 'worm'
? 'worm' in card && !card.greatMaker && !card.suppressed && !card.thumper
: 'territory' in card && card.territory === entry && !card.discovery);
assert.ok(index >= 0, `Use original unopened Spice Card ${entry}`);
front.push(game.spiceDeck.splice(index, 1)[0]);
}
game.spiceDeck.unshift(...front);
}
const quietTraining: Partial<Record<FactionId, LeaderSkillId>> = {
fremen: 'sandmaster', guild: 'killer-medic', emperor: 'master-of-assassins',
ixians: 'prana-bindu-adept', choam: 'swordmaster-of-ginaz', ecaz: 'warmaster',
};
/** Ordinary host module selections and private setup choices. The all14 case
* chooses a genuinely offered Sandmaster and eligible living Fremen trainer.
* Only unopened lotteries are ordered; no post-deal inventory is replaced. */
export function createFremenCunningModulesFixture(options: FremenCunningModulesOptions = {}) {
const factions = options.factions ?? ['fremen', 'guild', 'emperor'];
assert.equal(factions[0], 'fremen');
assert.ok(factions.includes('guild') && factions.includes('emperor'));
let game = createGame('FREMENCUNNINGMODULES', newPlayer('fremen', 'Fremen', 'fremen'),
options.advanced ?? false, options.expansions ?? ['ecaz']);
for (const faction of factions.slice(1)) joinGame(game, newPlayer(faction, faction, faction));
const initial = structuredClone(game), actions: FremenCunningModulesStep[] = [], staging: string[] = [];
const act = (actor: string, action: Action) => { game = actFremenCunningModules(game, actor, action, actions); };
const reach = (until: (state: Game) => boolean) => { game = advanceFremenCunningModules(game, until, actions); };
for (const selection of ['ecazTreachery', 'nexusCards', ...(options.skills ? ['leaderSkills'] : [])])
act(game.host, { type: 'moduleOption', module: selection, enabled: true });
for (const [type, enabled] of [['homeworlds', options.homeworlds], ['techTokens', options.tech], ['strongholdCards', options.strongholds]] as const)
if (enabled) act(game.host, { type, enabled: true });
for (const player of game.players) act(player.id, { type: 'ready' });
const lobby = structuredClone(game), start: FremenCunningModulesStep = { actor: game.host,
action: game.advanced ? { type: 'start', advancedPreview: true } : { type: 'start' } };
actions.push(structuredClone(start));
game = originalLottery(options.skills ? skillOfferRolls(game, quietTraining) : [], () => applyAction(game, start.actor, start.action));
const offered = structuredClone(game);
for (let count = 0; game.status === 'setup' && count < 300; count++) {
if (game.setupStage === 'leaderSkills' && !game.decision) {
const actor = Object.keys(game.leaderSkills!.offers)[0], own = viewGame(game, actor).leaderSkills!;
const wanted = quietTraining[fremenCunningModulesPlayer(game, actor).faction];
const skill = wanted && own.offer!.cards.includes(wanted) ? wanted : own.offer!.cards.find(card => !own.unavailableSkills?.[card]);
const leader = own.eligibleLeaders[0];
assert.ok(skill && !own.unavailableSkills?.[skill] && leader);
act(actor, { type: 'leaderSkill', event: own.offer!.event, skill, leader: leader.id });
continue;
}
if (game.setupStage === 'forces' && fremenCunningModulesPlayer(game, 'fremen').reserves === 20) {
act('fremen', { type: 'fremenSetup', placements: { [`sietch_tabr:${territory('sietch_tabr').sectors[0]}`]: 10 } });
continue;
}
const next = nextFremenCunningModulesStep(game); assert.ok(next); act(next.actor, next.action);
}
assert.equal(game.status, 'playing');
const afterSetup = structuredClone(game);
const trainer = game.leaderSkills?.assignments.find(assignment => assignment.owner === 'fremen' && assignment.skill === 'sandmaster')?.leader ?? null;
assert.ok(!options.skills || trainer);
orderOriginalSpice(game, game.advanced ? ['broken_land', 'rock_outcroppings'] : ['broken_land']);
const counterIndex = game.deck.findIndex(card => card.effect === 'karama'); assert.ok(counterIndex >= 0);
const counterCard = game.deck[counterIndex].id;
game.deck.unshift(game.deck.splice(counterIndex, 1)[0]);
let counterBid: { before: Game; step: FremenCunningModulesStep } | null = null;
let hmsPlacement: { before: Game; step: FremenCunningModulesStep; after: Game } | null = null;
for (let count = 0; count < 700; count++) {
if (fremenCunningModulesPlayer(game, 'guild').hand.some(card => card.id === counterCard)) break;
let next: FremenCunningModulesStep | null;
if (options.hmsAtSource && !game.response && !game.phaseOpening &&
game.decision?.kind === 'mobileStronghold' && game.decision.placement) {
const before = structuredClone(game), step: FremenCunningModulesStep = {
actor: game.decision.player, action: { type: 'decision', location: 'wind_pass:14' } };
act(step.actor, step.action);
hmsPlacement = { before, step, after: structuredClone(game) };
continue;
}
if (!game.response && !game.phaseOpening && game.decision?.kind === 'ixAuction' &&
game.ixAuction?.cards.some(card => card.id === counterCard)) {
const returned = game.ixAuction.cards.find(card => card.id !== counterCard); assert.ok(returned);
act(game.decision.player, { type: 'decision', card: returned.id, position: 'bottom' });
continue;
}
if (fremenCunningModulesClean(game) && game.auction?.cards[game.auction.index]?.id === counterCard) {
next = { actor: game.auction.active, action: game.auction.active === 'guild' && game.auction.bidder !== 'guild'
? { type: 'bid', amount: 1, allyPayment: 0 } : { type: 'passBid' } };
if (next.action.type === 'bid') counterBid = { before: structuredClone(game), step: structuredClone(next) };
} else next = nextFremenCunningModulesStep(game);
assert.ok(next, 'The original paid Karama must pass through its native bank auction');
act(next.actor, next.action);
}
assert.ok(counterBid && fremenCunningModulesPlayer(game, 'guild').hand.some(card => card.id === counterCard));
assert.ok(!options.hmsAtSource || hmsPlacement);
const counterPurchase = { ...counterBid, after: structuredClone(game) };
reach(state => state.turn === 1 && state.phase === 5 && state.active === 'fremen' && fremenCunningModulesClean(state));
const elite = game.advanced ? 2 : 0, homeworldSources = game.homeworlds ? nativeShipmentSources(viewGame(game, 'fremen'), 3, elite) : undefined;
assert.ok(!game.homeworlds || homeworldSources);
const shipmentBefore = structuredClone(game), shipmentStep: FremenCunningModulesStep = { actor: 'fremen', action: {
type: 'ship', territory: 'wind_pass', sector: 14, amount: 3, elite, allyPayment: 0,
...(homeworldSources ? { homeworldSources } : {}) } };
act(shipmentStep.actor, shipmentStep.action); reach(fremenCunningModulesClean);
const shipmentAfter = structuredClone(game);
assert.equal(fremenCunningModulesPlayer(game, 'fremen').forces['wind_pass:14'], 3);
reach(state => state.turn === 2 && state.phase === 1 && fremenCunningModulesClean(state));
orderOriginalSpice(game, game.advanced ? ['worm', 'red_chasm', 'south_mesa'] : ['worm', 'red_chasm']);
reach(state => state.nexus && !state.spiceWindow && !state.spiceResolution && fremenCunningModulesClean(state));
const beforeAlliance = structuredClone(game);
act('guild', { type: 'alliance', target: 'emperor' });
act('emperor', { type: 'alliance', target: 'guild' });
reach(state => state.nexusCards?.phase?.stage === 'drawing' && fremenCunningModulesClean(state));
const deck = game.nexusCards!.cards!.deck, index = deck.indexOf('fremen'); assert.ok(index >= 0);
deck.unshift(deck.splice(index, 1)[0]);
const drawBefore = structuredClone(game), drawStep: FremenCunningModulesStep = { actor: 'fremen', action: {
type: 'nexusCardChoice', turn: game.turn, card: null, choice: 'draw', ownRedraws: 0 } };
act(drawStep.actor, drawStep.action);
const drawAfter = structuredClone(game);
reach(state => state.turn === 3 && state.phase === 1 && fremenCunningModulesClean(state));
orderOriginalSpice(game, game.advanced ? ['worm', 'hagga_basin', 'habbanya_erg'] : ['worm', 'hagga_basin']);
let beforeWorm = structuredClone(game), producer: FremenCunningModulesStep | null = null;
for (let count = 0; count < 150; count++) {
if (!game.response && !game.phaseOpening && game.decision?.kind === 'nexusFremenCunningOffer') break;
beforeWorm = structuredClone(game); producer = nextFremenCunningModulesStep(game); assert.ok(producer);
act(producer.actor, producer.action);
}
assert.ok(producer && game.decision?.kind === 'nexusFremenCunningOffer' && game.nexusFremenCunningOffer);
assert.equal(game.nexusFremenCunningOffer.occurrence.territory, 'red_chasm');
staging.push('Original host/module/setup actions, undealt skill and scalar shuffles, actual one-spice Guild Karama purchase, native free typed Fremen reinforcement, real empty worm and Guild/Emperor alliance, actual own Fremen closing draw, then a distinct real empty worm before devouring. Only unopened physical Treachery, Spice and Nexus order is selected.');
return { initial, lobby, offered, afterSetup, game, actions, staging, owner: 'fremen', trainer,
source: 'wind_pass', sourceKey: 'wind_pass:14', destination: 'hagga_basin', destinationSector: 12,
shipment: { before: shipmentBefore, step: shipmentStep, after: shipmentAfter },
counterCard, counterPurchase, hmsPlacement,
beforeAlliance, draw: { before: drawBefore, step: drawStep, after: drawAfter },
beforeWorm, producer, occurrence: structuredClone(game.nexusFremenCunningOffer.occurrence) };
}
export type FremenCunningModulesFixture = ReturnType<typeof createFremenCunningModulesFixture>;
/** Actual spend/response/devouring/Nexus/remote selection, then its native
* arrival continuation. Optional collection debits only the newly earned
* replacement-Blow destination pile before any arrival reaction. */
export function completeFremenCunningModules(fixture: FremenCunningModulesFixture,
options: { collect?: boolean; pile?: string; destination?: { territory: string; sector: number } } = {}) {
const actions = fixture.actions, owner = fixture.owner, event = fixture.occurrence.event;
let game = fixture.game;
const beforeSpend = structuredClone(game), spend: FremenCunningModulesStep = { actor: owner,
action: { type: 'decision', event, accept: true } };
game = actFremenCunningModules(game, owner, spend.action, actions);
const spentResponse = structuredClone(game);
game = advanceFremenCunningModules(game, state => !state.response && !state.phaseOpening && state.decision?.kind === 'nexusFremenCunningRide', actions);
const beforeRide = structuredClone(game), player = fremenCunningModulesPlayer(game, owner);
const total = player.forces[fixture.sourceKey], elite = player.elites?.forces[fixture.sourceKey] ?? 0;
assert.equal(total, 3);
const destination = options.destination ?? { territory: fixture.destination, sector: fixture.destinationSector };
const collection = options.collect ? sandmasterCunningCollection(game, owner, fixture.source,
destination.territory, destination.sector, options.pile) : null;
if (options.collect) assert.ok(collection && !collection.blocked && collection.key);
const ride: FremenCunningModulesStep = { actor: owner, action: { type: 'decision', event, accept: true,
source: fixture.source, forces: { [fixture.sourceKey]: { normal: total - elite, elite } },
territory: destination.territory, sector: destination.sector,
...(options.collect !== undefined ? { sandmasterCollect: options.collect } : {}),
...(options.collect ? { sandmasterPile: collection!.key! } : {}) } };
game = actFremenCunningModules(game, owner, ride.action, actions);
const afterRide = structuredClone(game);
game = advanceFremenCunningModules(game, fremenCunningModulesClean, actions);
return { game, actions, beforeSpend, spend, spentResponse, beforeRide, ride, afterRide, collection };
}