Initialize occupation benefits for registered World Skills starts
This commit is contained in:
1 parent
7f9daa10fa
commit
396f9e0ccb
21 files changed
+737
-26
No files matched your search
@@ -1,10 +1,12 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { applyAction, createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { TERRITORIES, MOBILE_STRONGHOLD, location, territory } from '../game/board';
|
||||
import type { FactionId } from '../game/catalog';
|
||||
import { withNativeShipmentSources } from '../game/homeworld-options';
|
||||
import { LEADER_SKILL_CARDS, type LeaderSkillId } from '../game/leader-skill-cards';
|
||||
import { actAdvisorPreparation } from './fixture-advisor-preparation';
|
||||
import { nextRicheseStrongholdsNativeStep } from './fixture-richese-strongholds';
|
||||
import { skillOfferRolls } from './fixture-independent-card-modules';
|
||||
|
||||
export type MixedPhaseModulesStep = { actor: string; action: Action };
|
||||
export type MixedPhaseModulesTransition = { before: Game; steps: MixedPhaseModulesStep[]; after: Game };
|
||||
@@ -16,6 +18,8 @@ export type MixedPhaseModulesOptions = {
|
||||
strongholds?: boolean;
|
||||
independent?: boolean;
|
||||
homeworlds?: boolean;
|
||||
skills?: boolean;
|
||||
training?: Partial<Record<FactionId, LeaderSkillId>>;
|
||||
};
|
||||
export const MIXED_PHASE_NATIVE_FACTIONS = ['ixians', 'tleilaxu', 'choam', 'richese', 'ecaz', 'moritani'] as const;
|
||||
export const actMixedPhaseModules = actAdvisorPreparation;
|
||||
@@ -68,6 +72,26 @@ export function finishMixedPhaseModules(game: Game, actions: MixedPhaseModulesSt
|
||||
return advanceMixedPhaseModules(game, state => (state.phase !== phase || state.turn !== turn) && mixedPhaseModulesClean(state), actions);
|
||||
}
|
||||
|
||||
/** Choose only the original undealt skill lottery. Native UUID/byte entropy
|
||||
* stays native, and later scalar draws retain the existing source policy. */
|
||||
function startWithSkillOffers(game: Game, actor: string, action: Action,
|
||||
training: Partial<Record<FactionId, LeaderSkillId>>, actions: MixedPhaseModulesStep[]): Game {
|
||||
const rolls = skillOfferRolls(game, training), descriptor = Object.getOwnPropertyDescriptor(crypto, 'getRandomValues');
|
||||
const original = crypto.getRandomValues.bind(crypto); let cursor = 0;
|
||||
crypto.getRandomValues = <T extends ArrayBufferView | null>(array: T): T => {
|
||||
if (array instanceof Uint32Array && array.length === 1) array[0] = rolls[cursor++] ?? 0xffffffff;
|
||||
else Reflect.apply(original, crypto, [array]);
|
||||
return array;
|
||||
};
|
||||
try {
|
||||
const next = applyAction(game, actor, action);
|
||||
actions.push({ actor, action: structuredClone(action) }); return next;
|
||||
} finally {
|
||||
if (descriptor) Object.defineProperty(crypto, 'getRandomValues', descriptor);
|
||||
else Reflect.deleteProperty(crypto, 'getRandomValues');
|
||||
}
|
||||
}
|
||||
|
||||
/** Public module selections, ready/start and every original setup decision.
|
||||
* Only scalar lotteries and unopened Spice positions are selected. No played
|
||||
* card, force, wallet, Tanks, phase, token or Stronghold owner is installed. */
|
||||
@@ -78,16 +102,49 @@ export function createMixedPhaseModulesFixture(options: MixedPhaseModulesOptions
|
||||
const initial = structuredClone(game), actions: MixedPhaseModulesStep[] = [];
|
||||
const act = (actor: string, action: Action) => { game = actMixedPhaseModules(game, actor, action, actions); };
|
||||
if (options.homeworlds) act(host, { type: 'homeworlds', enabled: true });
|
||||
if (options.skills) act(host, { type: 'moduleOption', module: 'leaderSkills', enabled: true });
|
||||
if (options.independent) act(host, { type: 'moduleOption', module: 'ecazTreachery', enabled: true });
|
||||
if (options.tech) act(host, { type: 'techTokens', enabled: true });
|
||||
if (options.strongholds) act(host, { type: 'strongholdCards', enabled: true });
|
||||
for (const player of game.players) act(player.id, { type: 'ready' });
|
||||
const lobby = structuredClone(game);
|
||||
act(host, options.advanced ? { type: 'start', advancedPreview: true } : { type: 'start' });
|
||||
// Fill unspecified seats without consuming a role requested for a later
|
||||
// seat. The resulting full map still selects only the original lottery.
|
||||
let training: Partial<Record<FactionId, LeaderSkillId>> | undefined;
|
||||
if (options.skills && options.training) {
|
||||
training = {};
|
||||
const requested = game.players.flatMap(player => options.training?.[player.faction] ?? []);
|
||||
const chosen = new Set<LeaderSkillId>();
|
||||
for (const player of game.players) {
|
||||
const skill = options.training[player.faction] ?? LEADER_SKILL_CARDS.find(card =>
|
||||
!requested.includes(card.id) && !chosen.has(card.id))?.id;
|
||||
assert.ok(skill && !chosen.has(skill), 'Choose distinct original skill offers');
|
||||
training[player.faction] = skill; chosen.add(skill);
|
||||
}
|
||||
}
|
||||
const start: Action = options.advanced ? { type: 'start', advancedPreview: true } : { type: 'start' };
|
||||
if (training) game = startWithSkillOffers(game, host, start, training, actions);
|
||||
else act(host, start);
|
||||
const setup = structuredClone(game); assert.equal(game.status, 'setup');
|
||||
assert.ok(game.players.every(player => !player.hand.length));
|
||||
if (!options.skills) assert.ok(game.players.every(player => !player.hand.length));
|
||||
const originalInventory = mixedPhaseModulesInventory(game);
|
||||
game = advanceMixedPhaseModules(game, state => state.status === 'playing', actions);
|
||||
if (options.skills) {
|
||||
for (let count = 0; game.status === 'setup' && count < 300; count++) {
|
||||
if (!game.decision && !game.response && !game.phaseOpening && game.setupStage === 'leaderSkills') {
|
||||
const actor = Object.keys(game.leaderSkills!.offers)[0]; assert.ok(actor);
|
||||
const own = viewGame(game, actor).leaderSkills!;
|
||||
const player = mixedPhaseModulesPlayer(game, actor);
|
||||
const skill = training?.[player.faction] ?? own.offer?.cards.find(card => !own.unavailableSkills?.[card]);
|
||||
const leader = own.eligibleLeaders[0];
|
||||
assert.ok(own.offer && skill && own.offer.cards.includes(skill) && !own.unavailableSkills?.[skill] && leader,
|
||||
'Choose one currently offered original skill and an eligible own leader');
|
||||
act(actor, { type: 'leaderSkill', event: own.offer.event, skill, leader: leader.id });
|
||||
} else {
|
||||
const next = nextMixedPhaseModules(game); assert.ok(next); act(next.actor, next.action);
|
||||
}
|
||||
}
|
||||
assert.equal(game.status, 'playing');
|
||||
} else game = advanceMixedPhaseModules(game, state => state.status === 'playing', actions);
|
||||
const afterSetup = structuredClone(game);
|
||||
const originalSpice = game.spiceDeck.map(card => JSON.stringify(card)).sort();
|
||||
const spiceOrder = [0, 1, 2, 3].map(position => {
|
||||
@@ -102,6 +159,7 @@ export function createMixedPhaseModulesFixture(options: MixedPhaseModulesOptions
|
||||
assert.deepEqual(mixedPhaseModulesInventory(game), originalInventory);
|
||||
return { game, sourceGame: structuredClone(game), independent: options.independent ?? false,
|
||||
homeworlds: options.homeworlds ?? false,
|
||||
skills: options.skills ?? false,
|
||||
initial, lobby, setup, afterSetup, firstStorm,
|
||||
originalInventory, originalSpice, spiceOrder, actions, sourceActions: structuredClone(actions),
|
||||
sourceActionCount: actions.length, staging: [] as string[] };
|
||||
@@ -113,11 +171,13 @@ export function createMixedPhaseModulesFixture(options: MixedPhaseModulesOptions
|
||||
* entry plus original native choices, with no played-state installation. */
|
||||
export function createMixedPhaseModulesProgramme(options: {
|
||||
advanced?: boolean; strongholds?: boolean; independent?: boolean; homeworlds?: boolean;
|
||||
skills?: boolean; training?: Partial<Record<FactionId, LeaderSkillId>>;
|
||||
} = {}) {
|
||||
const advanced = options.advanced ?? false, strongholds = options.strongholds ?? advanced;
|
||||
const source = createMixedPhaseModulesFixture({ factions: MIXED_PHASE_NATIVE_FACTIONS,
|
||||
expansions: ['ix', 'choam', 'ecaz'], advanced, tech: true, strongholds,
|
||||
independent: options.independent, homeworlds: options.homeworlds });
|
||||
independent: options.independent, homeworlds: options.homeworlds,
|
||||
skills: options.skills, training: options.training });
|
||||
let game = source.game;
|
||||
const actions = [...source.actions];
|
||||
const act = (actor: string, action: Action) => { game = actMixedPhaseModules(game, actor, action, actions); };
|
||||
|
||||
Reference in new issue
Block a user