Compose independent Skills with original Tech and Stronghold modules

Unify fresh/live classic and native phase admission, retaining original counts, earned custody, separate income ledgers and physical card/skill continuations. Plain paired E3 phases no longer require cards or Nexus. Coupled44-file471/471, types/lint/build, genuine programmes and390px normal entry pass. Controlled card inputs explicit; no save retrofit or deployment.
This commit is contained in:
Quintine committed 2026-10-17 22:12:44 +11:00
1 parent 17adffa6a7
commit b9330d489e
31 files changed
+797 -104

No files matched your search

+55 -7
View File
@@ -1,12 +1,14 @@
import assert from 'node:assert/strict';
import {
applyAction, createGame, initializeEcazTreacheryGameForAudit, initializeFactionExpansionsGameForAudit,
initializeStrongholdFactionsGameForAudit, joinGame, newPlayer, type Action, type Game,
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 = {
@@ -15,13 +17,37 @@ export type IndependentCardModulesOptions = {
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;
};
/** Select original modules and independent cards before the undealt native setup.
* No armies, wallets, module owners, phase or history are assigned afterward. */
/** Select only the original undealt fourteen-card lottery, never assignments. */
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;
@@ -38,22 +64,44 @@ export function createIndependentCardModulesFixture(options: IndependentCardModu
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);
new Uint8Array(array.buffer, array.byteOffset, array.byteLength).fill(255);
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 {
game = options.independent !== false ? initializeEcazTreacheryGameForAudit(game) :
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');
}
game = advanceIndependentCardModules(game, state => state.status === 'playing', steps);
return { initial, game, steps };
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. */