Retain selected decks and original optional modules through normal setup, distinguish native Ixian Betrayal from absent-faction replacement, route explicit CHOAM fuel before printed effects, and preserve native No-Field invoices and pair continuation. Connect canonical native cancellation stock and existing human/minimal AI consumers. Validation: 194 affected tests across 19 files, types/lint/build, bounded native invoice review and seven finite original-action/rendered-control programmes. Runtime ec5ad6c18996d8d958ec0d01132ef3e3ea6d23d36188902eca9a63d4db2efbf8; documentation-only proof overlay a5892004937434429916496497c07a3838a324b4a5d64d4a0eb804ab5212d5bd. Other envelopes, full combinations, comprehensive assurance and deployed acceptance remain open. No saved-room/provider changes.
133 lines
6.9 KiB
TypeScript
133 lines
6.9 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import {
|
|
applyAction, createGame, initializeEcazTreacheryGameForAudit, initializeFactionExpansionsGameForAudit,
|
|
initializeLeaderSkillsGameForAudit, initializeStrongholdFactionsGameForAudit, joinGame, newPlayer,
|
|
viewGame, type Action, type Game,
|
|
} from '../game/engine';
|
|
import type { FactionId } from '../game/catalog';
|
|
import { createTechTokens } from '../game/tech-tokens';
|
|
import { createStrongholdCards } from '../game/stronghold-cards';
|
|
import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions';
|
|
import { LEADER_SKILL_CARDS, type LeaderSkillId } from '../game/leader-skill-cards';
|
|
|
|
export type IndependentCardModulesStep = { actor: string; action: Action };
|
|
export type IndependentCardModulesOptions = {
|
|
advanced?: boolean;
|
|
factions?: readonly FactionId[];
|
|
expansions?: Game['expansions'];
|
|
tech?: boolean;
|
|
strongholds?: boolean;
|
|
skills?: boolean;
|
|
training?: Partial<Record<FactionId, LeaderSkillId>>;
|
|
/** False keeps the original native deck for revealed-card comparisons. */
|
|
independent?: boolean;
|
|
homeworlds?: boolean;
|
|
nexus?: boolean;
|
|
};
|
|
|
|
/** Select only the original undealt fourteen-card lottery, never assignments. */
|
|
export function skillOfferRolls(game: Game, training: IndependentCardModulesOptions['training']): number[] {
|
|
const source = LEADER_SKILL_CARDS.map(card => card.id), selected: LeaderSkillId[] = [];
|
|
for (const player of game.players) {
|
|
const skill = training?.[player.faction] ?? source.find(card => !selected.includes(card))!;
|
|
assert.ok(source.includes(skill) && !selected.includes(skill), 'Use distinct original skill offers');
|
|
selected.push(skill);
|
|
}
|
|
const rest = source.filter(card => !selected.includes(card));
|
|
const desired = selected.flatMap(card => [card, rest.shift()!]).concat(rest);
|
|
const working = [...source], rolls: number[] = [];
|
|
for (let index = working.length - 1; index > 0; index--) {
|
|
const from = working.indexOf(desired[index]);
|
|
assert.ok(from >= 0 && from <= index);
|
|
rolls.push(Math.floor((from + 0.5) * 0x100000000 / (index + 1)));
|
|
[working[index], working[from]] = [working[from], working[index]];
|
|
}
|
|
return rolls;
|
|
}
|
|
|
|
/** Original unused modules and independent cards are selected before the real
|
|
* development initializer. Keep the undealt `initial` input for existing native
|
|
* constructors; `offered` is the actual post-initializer setup. No armies,
|
|
* wallets, owners, phases or history are assigned afterward. */
|
|
export function createIndependentCardModulesFixture(options: IndependentCardModulesOptions = {}) {
|
|
const factions = options.factions ?? ['emperor', 'atreides', 'guild'];
|
|
const advanced = options.advanced ?? false;
|
|
assert.ok(factions.length >= 2);
|
|
let game = createGame('INDEPENDENTCARDMODULES', newPlayer(factions[0], factions[0], factions[0]),
|
|
advanced, options.expansions ?? []);
|
|
for (const faction of factions.slice(1)) joinGame(game, newPlayer(faction, faction, faction));
|
|
if (options.homeworlds) game = applyAction(game, game.host, { type: 'homeworlds', enabled: true });
|
|
if (options.nexus) game = applyAction(game, game.host, { type: 'moduleOption', module: 'nexusCards', enabled: true });
|
|
if (options.tech ?? true) game.techTokens = createTechTokens();
|
|
if (options.strongholds) game.strongholdCards = createStrongholdCards();
|
|
const steps: IndependentCardModulesStep[] = [];
|
|
for (const player of game.players) {
|
|
const step: IndependentCardModulesStep = { actor: player.id, action: { type: 'ready' } };
|
|
steps.push(step); game = applyAction(game, step.actor, step.action);
|
|
}
|
|
const initial = structuredClone(game);
|
|
const rolls = options.skills ? skillOfferRolls(game, options.training) : [];
|
|
let cursor = 0;
|
|
const descriptor = Object.getOwnPropertyDescriptor(crypto, 'getRandomValues');
|
|
crypto.getRandomValues = <T extends ArrayBufferView | null>(array: T): T => {
|
|
assert.ok(array);
|
|
if (array instanceof Uint32Array && array.length === 1 && cursor < rolls.length) array[0] = rolls[cursor++];
|
|
else new Uint8Array(array.buffer, array.byteOffset, array.byteLength).fill(255);
|
|
return array;
|
|
};
|
|
try {
|
|
if (options.skills) {
|
|
if (options.independent !== false) game.ecazTreachery = true;
|
|
game = initializeLeaderSkillsGameForAudit(game);
|
|
} else game = options.independent !== false ? initializeEcazTreacheryGameForAudit(game) :
|
|
options.strongholds ? initializeStrongholdFactionsGameForAudit(game) : initializeFactionExpansionsGameForAudit(game);
|
|
}
|
|
finally {
|
|
if (descriptor) Object.defineProperty(crypto, 'getRandomValues', descriptor);
|
|
else Reflect.deleteProperty(crypto, 'getRandomValues');
|
|
}
|
|
const offered = structuredClone(game);
|
|
for (let count = 0; game.status === 'setup' && count < 300; count++) {
|
|
let step: IndependentCardModulesStep;
|
|
if (game.setupStage === 'leaderSkills') {
|
|
const actor = Object.keys(game.leaderSkills!.offers)[0], offer = game.leaderSkills!.offers[actor];
|
|
const own = viewGame(game, actor).leaderSkills!, player = game.players.find(seat => seat.id === actor)!;
|
|
const skill = options.training?.[player.faction] ?? offer.cards.find(card => !own.unavailableSkills?.[card]);
|
|
const leader = own.eligibleLeaders[0];
|
|
assert.ok(skill && offer.cards.includes(skill) && !own.unavailableSkills?.[skill] && leader);
|
|
step = { actor, action: { type: 'leaderSkill', event: offer.event, skill, leader: leader.id } };
|
|
} else {
|
|
const next = nextIndependentCardModulesStep(game); assert.ok(next, 'Complete original native setup');
|
|
step = next;
|
|
}
|
|
steps.push(structuredClone(step)); game = applyAction(game, step.actor, step.action);
|
|
}
|
|
assert.equal(game.status, 'playing');
|
|
return { initial, offered, game, steps };
|
|
}
|
|
|
|
/** Reuse original native choices, including all phase/module windows. */
|
|
export function nextIndependentCardModulesStep(game: Game): IndependentCardModulesStep | null {
|
|
if (!game.response && !game.phaseOpening && !game.decision && game.nexusCards?.phase?.stage === 'drawing') {
|
|
const phase = game.nexusCards.phase;
|
|
const actor = phase.eligible.find(id => !phase.done.includes(id));
|
|
assert.ok(actor);
|
|
return { actor, action: { type: 'nexusCardChoice', turn: game.turn,
|
|
card: game.nexusCards.cards!.hands[actor] ?? null, choice: 'keep', ownRedraws: 0 } };
|
|
}
|
|
return nextStrongholdFactionsNativeStep(game);
|
|
}
|
|
|
|
export function advanceIndependentCardModules(game: Game, until: (state: Game) => boolean,
|
|
steps: IndependentCardModulesStep[] = []): Game {
|
|
let state = game;
|
|
for (let count = 0; count < 1800; count++) {
|
|
if (until(state)) return state;
|
|
assert.notEqual(state.status, 'finished', 'Programme ended before its requested native boundary');
|
|
const step = nextIndependentCardModulesStep(state);
|
|
assert.ok(step, 'Supply the actual human Battle Plans before continuing');
|
|
steps.push(structuredClone(step)); state = applyAction(state, step.actor, step.action);
|
|
}
|
|
throw Error('Independent module programme did not reach its requested boundary');
|
|
}
|