Files
Dune/tests/fixture-nexus-fremen-free-quota.ts
T

200 lines
11 KiB
TypeScript

import assert from 'node:assert/strict';
import {
applyAction, createGame, initializeNexusGameForAudit, joinGame, newPlayer, viewGame,
type Action, type Game,
} from '../game/engine';
import { location, territory } from '../game/board';
import { treacheryDeck } from '../game/cards';
import { validateNexusCards } from '../game/nexus-cards';
import { shipmentAvailable } from '../game/shipment-opportunity';
import { traitorDeck } from '../game/traitors';
import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions';
import { nexusTleilaxuSecretAllyPlayer as player } from './fixture-nexus-tleilaxu-secret-ally';
export type NexusFremenFreeQuotaStep = { actor: string; action: Action };
export type NexusFremenFreeQuotaFixtureOptions = { advanced?: boolean; tleilaxu?: boolean };
export type NexusFremenFreeQuotaFixture = {
game: Game;
owner: string;
initial: Game;
scenario: {
advanced: boolean;
tleilaxu: boolean;
opponent: string;
partner: string;
event: string;
staging: string[];
};
actions: NexusFremenFreeQuotaStep[];
physicalfacts: {
shipmentCost: number;
deadLeader: string;
battleLosses: number;
beforeDraw: Game;
draw: NexusFremenFreeQuotaStep;
afterDraw: Game;
};
};
export const nexusFremenFreeQuotaQuiet = (game: Game): boolean =>
!game.phaseOpening && !game.response && !game.decision && !game.pendingRevival;
/** Continue only original public choices; private battle plans and closing draws
* remain explicit actions of the chronology rather than fabricated state. */
export function nextNexusFremenFreeQuotaNativeStep(game: Game): NexusFremenFreeQuotaStep | null {
if (game.nexusCards?.phase?.stage === 'drawing' && nexusFremenFreeQuotaQuiet(game)) return null;
if (game.decision?.kind === 'faceDance')
return { actor: game.decision.player, action: { type: 'decision', reveal: false } };
return nextStrongholdFactionsNativeStep(game);
}
export function advanceNexusFremenFreeQuota(
game: Game, until: (state: Game) => boolean, actions?: NexusFremenFreeQuotaStep[],
): Game {
for (let count = 0; count < 2000; count++) {
if (until(game)) return game;
assert.notEqual(game.status, 'finished');
const next = nextNexusFremenFreeQuotaNativeStep(game);
assert.ok(next, `Native Fremen chronology stopped at ${game.status}/${game.turn}/${game.phase}`);
actions?.push(structuredClone(next));
game = applyAction(game, next.actor, next.action);
}
throw Error('Native Fremen chronology did not reach its boundary');
}
/** Scalar producer entropy, scoped to the real initializer/actions. Each shuffle
* retains its original printed order; no candidate histories are retried. */
function identityRandom<T>(run: () => T): T {
const descriptor = Object.getOwnPropertyDescriptor(crypto, 'getRandomValues');
crypto.getRandomValues = <V extends ArrayBufferView | null>(array: V): V => {
assert.ok(array);
new Uint8Array(array.buffer, array.byteOffset, array.byteLength).fill(255);
return array;
};
try { return run(); }
finally {
if (descriptor) Object.defineProperty(crypto, 'getRandomValues', descriptor);
else Reflect.deleteProperty(crypto, 'getRandomValues');
}
}
export function assertNexusFremenFreeQuotaInventory(game: Game): void {
const cards = [...game.deck, ...game.discard, ...game.players.flatMap(p => p.hand),
...(game.auction?.cards.slice(game.auction.index + (game.currentAuctionSale ? 1 : 0)) ?? [])];
assert.deepEqual(cards.map(card => card.id).sort(), treacheryDeck(game.expansions).map(card => card.id).sort());
assert.equal(new Set(cards.map(card => card.id)).size, cards.length);
for (const p of game.players) {
assert.equal(p.reserves + p.tanks + Object.values(p.forces).reduce((sum, n) => sum + n, 0), 20);
if (p.elites) {
assert.equal(p.elites.reserves + p.elites.tanks +
Object.values(p.elites.forces).reduce((sum, n) => sum + n, 0), p.faction === 'emperor' ? 5 : 3);
assert.ok(p.elites.reserves <= p.reserves && p.elites.tanks <= p.tanks);
}
}
assert.deepEqual([...(game.traitorReserve ?? []), ...game.players.flatMap(p =>
[...p.traitors, ...p.traitorChoices, ...(p.faceDancers ?? []).map(card => card.leader)])].sort(),
traitorDeck(game.players, game.expansions.includes('ix')).sort());
assert.ok(game.nexusCards?.cards);
validateNexusCards(game.nexusCards.cards, game.players);
}
/** Genuine original Basic/Advanced setup, shipment, weapon death, eight-counter
* exchange, Shai-Hulud, reciprocal alliance, and physical Fremen closing draw.
* Only original undealt deck order and producer entropy are staged. */
export function createNexusFremenFreeQuotaFixture(
options: NexusFremenFreeQuotaFixtureOptions = {},
): NexusFremenFreeQuotaFixture {
return identityRandom(() => {
const advanced = options.advanced ?? false, tleilaxu = options.tleilaxu ?? false;
const owner = 'atreides', opponent = 'emperor', partner = tleilaxu ? 'tleilaxu' : 'guild';
let lobby = createGame('NEXUSFREMENFREEQUOTA', newPlayer(owner, 'Atreides', 'atreides'), advanced, tleilaxu ? ['ix'] : []);
joinGame(lobby, newPlayer(partner, tleilaxu ? 'Tleilaxu' : 'Guild', tleilaxu ? 'tleilaxu' : 'guild'));
joinGame(lobby, newPlayer(opponent, 'Emperor', 'emperor'));
lobby.nexusCards = { cards: null, phase: null };
for (const p of lobby.players) lobby = applyAction(lobby, p.id, { type: 'ready' });
const initial = initializeNexusGameForAudit(lobby);
const actions: NexusFremenFreeQuotaStep[] = [], staging: string[] = [];
const act = (game: Game, actor: string, action: Action): Game => {
actions.push({ actor, action: structuredClone(action) });
return applyAction(game, actor, action);
};
let game = advanceNexusFremenFreeQuota(initial, state => state.status === 'playing', actions);
assert.ok(!game.players.some(p => p.faction === 'fremen'));
assert.ok(game.players.every(p => p.hand.length > 0));
if (tleilaxu) assert.equal(player(game, partner).faceDancers?.length, 3);
const weapon = player(game, opponent).hand.find(card => card.kind === 'projectile' || card.kind === 'poison');
assert.ok(weapon, 'The original third-seat deal supplies the real leader-killing weapon');
assertNexusFremenFreeQuotaInventory(game);
game = advanceNexusFremenFreeQuota(game,
state => state.turn === 1 && state.phase === 5 && state.active === opponent && nexusFremenFreeQuotaQuiet(state), actions);
const sector = territory('arrakeen').sectors[0], target = location('arrakeen', sector);
assert.ok(shipmentAvailable(game, player(game, opponent)));
const shipmentSpice = player(game, opponent).spice;
game = act(game, opponent, { type: 'ship', territory: 'arrakeen', sector, amount: 1, elite: 0, allyPayment: 0 });
game = advanceNexusFremenFreeQuota(game, nexusFremenFreeQuotaQuiet, actions);
const shipmentCost = shipmentSpice - player(game, opponent).spice;
assert.ok(shipmentCost > 0);
assert.equal(player(game, opponent).forces[target], 1);
assert.equal(player(game, owner).forces[target], 10);
game = advanceNexusFremenFreeQuota(game,
state => state.turn === 1 && state.phase === 6 && nexusFremenFreeQuotaQuiet(state), actions);
if (!game.battle) {
assert.ok(game.active === owner || game.active === opponent);
game = act(game, game.active, { type: 'chooseBattle', territory: 'arrakeen',
target: game.active === owner ? opponent : owner });
}
game = advanceNexusFremenFreeQuota(game,
state => nexusFremenFreeQuotaQuiet(state) && !nextNexusFremenFreeQuotaNativeStep(state), actions);
assert.ok(game.battle && !game.battle.revealed);
const ownLeader = player(game, owner).leaders.filter(leader => !leader.dead).sort((a, b) => a.strength - b.strength)[0];
const otherLeader = player(game, opponent).leaders.filter(leader => !leader.dead).sort((a, b) => a.strength - b.strength)[0];
assert.ok(ownLeader && otherLeader);
game = act(game, owner, { type: 'battlePlan', dial: advanced ? 4 : 8, support: 0,
leader: ownLeader.id, weapon: null, defense: null });
game = act(game, opponent, { type: 'battlePlan', dial: 0, support: 0,
leader: otherLeader.id, weapon: weapon.id, defense: null });
game = advanceNexusFremenFreeQuota(game, state => state.turn === 2 && state.phase === 1 &&
nexusFremenFreeQuotaQuiet(state) && !state.spiceSequence && !state.spiceResolution &&
!state.spiceWindow && !state.nexus && state.ready.length === 0, actions);
assert.equal(player(game, owner).leaders.find(leader => leader.id === ownLeader.id)?.dead, true);
assert.equal(player(game, owner).tanks, 8);
const wormIndex = game.spiceDeck.findIndex(card => 'worm' in card && !card.greatMaker && !card.suppressed && !card.thumper);
assert.ok(wormIndex >= 0);
game.spiceDeck.unshift(game.spiceDeck.splice(wormIndex, 1)[0]);
staging.push('Ordered one original undealt Shai-Hulud singleton before the real turn-two Spice Blow.');
game = advanceNexusFremenFreeQuota(game,
state => state.nexus === true && !state.spiceWindow && nexusFremenFreeQuotaQuiet(state), actions);
game = act(game, opponent, { type: 'alliance', target: partner });
game = act(game, partner, { type: 'alliance', target: opponent });
assert.equal(player(game, owner).ally, null);
game = advanceNexusFremenFreeQuota(game, state => state.nexusCards?.phase?.stage === 'drawing', actions);
assert.deepEqual(game.nexusCards!.phase!.eligible, [owner]);
const cards = game.nexusCards!.cards!;
assert.equal(cards.hands[owner], null);
const fremenIndex = cards.deck.indexOf('fremen');
assert.ok(fremenIndex >= 0);
cards.deck.unshift(cards.deck.splice(fremenIndex, 1)[0]);
staging.push('Ordered the original undealt Fremen Nexus singleton before the real closing draw; no hand was replaced.');
const beforeDraw = structuredClone(game);
const draw: NexusFremenFreeQuotaStep = { actor: owner, action: { type: 'nexusCardChoice',
turn: game.turn, card: cards.hands[owner], choice: 'draw', ownRedraws: 0 } };
game = act(game, draw.actor, draw.action);
assert.equal(game.nexusCards!.cards!.hands[owner], 'fremen');
const afterDraw = structuredClone(game);
game = advanceNexusFremenFreeQuota(game,
state => state.turn === 2 && state.phase === 4 && nexusFremenFreeQuotaQuiet(state), actions);
assert.equal(player(game, owner).tanks, 8);
assert.equal(player(game, owner).freeForcesRevived, 0);
assert.equal(player(game, owner).ally, null);
const offer = viewGame(game, owner).nexusFremenRevival;
assert.ok(offer);
assert.equal(offer.blocked, null);
assertNexusFremenFreeQuotaInventory(game);
return { initial, game, owner, actions,
scenario: { advanced, tleilaxu, opponent, partner, event: offer.event, staging },
physicalfacts: { shipmentCost, deadLeader: ownLeader.id, battleLosses: 8, beforeDraw, draw, afterDraw } };
});
}