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:
1 parent
17adffa6a7
commit
b9330d489e
31 files changed
+797
-104
No files matched your search
@@ -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. */
|
||||
|
||||
Reference in new issue
Block a user