Compose World Skills cards with selected ordinary decks

This commit is contained in:
Quintine committed 2026-10-17 23:07:39 +11:00
1 parent b9330d489e
commit 01985af36b
36 files changed
+1201 -283

No files matched your search

+38 -5
View File
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict';
import {
applyAction, createGame, initializeEcazTreacheryGameForAudit, joinGame,
applyAction, createGame, initializeEcazTreacheryGameForAudit, initializeLeaderSkillsGameForAudit, joinGame,
newPlayer, viewGame, type Action, type Game, type Player,
} from '../game/engine';
import { treacheryDeck } from '../game/cards';
@@ -13,11 +13,18 @@ import { nativeShipmentSources } from '../game/homeworld-options';
import { homeworldShipmentChoice } from '../game/homeworld-shipment-options';
import { finishHarassPreparation, takeHarassCard } from './fixture-harass-withdraw';
import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions';
import { skillOfferRolls } from './fixture-independent-card-modules';
import type { FactionId } from '../game/catalog';
import type { LeaderSkillId } from '../game/leader-skill-cards';
export const HARASS_HOMEWORLD_CARD = 'ecaz-harass-withdraw';
export type HarassHomeworldStep = { actor: string; action: Action };
export type HarassHomeworldOptions = {
advanced?: boolean;
skills?: boolean;
training?: Partial<Record<FactionId, LeaderSkillId>>;
tech?: boolean;
strongholds?: boolean;
slot?: 'weapon' | 'defense';
role?: 'visitor' | 'native';
/** Salusa is an Advanced own-World rejection case, not a second faction. */
@@ -84,7 +91,18 @@ export function actHarassHomeworld(game: Game, step: HarassHomeworldStep, action
/** Never invent a plan, native vote or late-defense permission. */
export function nextHarassHomeworldStep(game: Game): HarassHomeworldStep | null {
if (!game.response && !game.phaseOpening) {
if (game.status === 'setup' && game.setupStage === 'leaderSkills') {
const actor = Object.keys(game.leaderSkills!.offers)[0], offer = game.leaderSkills!.offers[actor];
const own = viewGame(game, actor).leaderSkills!;
const skill = offer.cards.find(card => !own.unavailableSkills?.[card]), leader = own.eligibleLeaders[0];
assert.ok(skill && leader, 'Choose an original offered skill and eligible native disc');
return { actor, action: { type: 'leaderSkill', event: offer.event, skill, leader: leader.id } };
}
const d = game.decision;
if (d?.kind === 'mentatQuestion' && d.stage === 'name')
return { actor: d.player, action: { type: 'decision', event: d.event, decline: true } };
if (d?.kind === 'leaderSkillVisibility')
return { actor: d.player, action: { type: 'leaderSkillVisibility', event: d.event, hide: true } };
if (d?.kind === 'harassWithdraw') return null;
if (d?.kind === 'homeworldDefense') return { actor: d.player, action: { type: 'decision', event: d.event, use: false } };
if (d?.kind === 'homeworldExplosion') return { actor: d.player, action: { type: 'decision', event: d.event, choice: 0 } };
@@ -105,12 +123,23 @@ export function advanceHarassHomeworld(game: Game, until: (game: Game) => boolea
}
throw Error('Original Harass Homeworld programme did not reach its boundary');
}
function initialize(game: Game): Game {
function initialize(game: Game, options: Pick<HarassHomeworldOptions, 'skills' | 'training'>): 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); return array;
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 { return initializeEcazTreacheryGameForAudit(game); }
try {
if (options.skills) {
game.ecazTreachery = true;
return initializeLeaderSkillsGameForAudit(game);
}
return initializeEcazTreacheryGameForAudit(game);
}
finally {
if (descriptor) Object.defineProperty(crypto, 'getRandomValues', descriptor);
else Reflect.deleteProperty(crypto, 'getRandomValues');
@@ -134,11 +163,15 @@ export function createHarassHomeworldSetup(options: HarassHomeworldOptions = {})
!game.discoveryEnabled && !game.discoveries && !game.greatMaker);
if (!game.homeworlds) game = applyAction(game, game.host, { type: 'homeworlds', enabled: true });
assert.deepEqual(game.homeworlds, { custody: null });
if (options.tech !== undefined && !!game.techTokens !== options.tech)
game = applyAction(game, game.host, { type: 'techTokens', enabled: options.tech });
if (options.strongholds !== undefined && !!game.strongholdCards !== options.strongholds)
game = applyAction(game, game.host, { type: 'strongholdCards', enabled: options.strongholds });
const nativeSteps: HarassHomeworldStep[] = [];
for (const p of game.players) if (!p.ready)
game = actHarassHomeworld(game, { actor: p.id, action: { type: 'ready' } }, nativeSteps);
const initial = structuredClone(game);
game = initialize(game);
game = initialize(game, options);
const initialized = structuredClone(game);
game = advanceHarassHomeworld(game, g => g.status === 'playing', nativeSteps);
const owner = game.players.find(p => p.faction === 'emperor')!.id;