105 lines
4.7 KiB
TypeScript
105 lines
4.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { viewGame, type Game } from '../game/engine';
|
|
import { forceInventory } from '../game/force-inventory';
|
|
import type { NativeReserveSelections } from '../game/homeworld-native-reserves';
|
|
import {
|
|
createIndependentCardModulesFixture, type IndependentCardModulesStep,
|
|
} from './fixture-independent-card-modules';
|
|
import {
|
|
createRicheseReinforcementsFixture, RICHESE_HARASS, RICHESE_REINFORCEMENTS,
|
|
type RicheseReinforcementsFixture, type RicheseReinforcementsOptions,
|
|
type RicheseReinforcementsStep,
|
|
} from './fixture-richese-reinforcements';
|
|
|
|
export type NativeWorldMarkerOptions = Omit<RicheseReinforcementsOptions, 'initial' | 'seatIds'> & {
|
|
tech?: boolean;
|
|
strongholds?: boolean;
|
|
};
|
|
export type NativeWorldMarkerFixture = RicheseReinforcementsFixture & {
|
|
/** Ready but undealt: the original initializer seam for controls/policies. */
|
|
readyLobby: Game;
|
|
readySteps: IndependentCardModulesStep[];
|
|
actors: { owner: string; opponent: string; observer: string };
|
|
source: {
|
|
homeworld: string;
|
|
reservesBeforeReveal: number;
|
|
materializes: number;
|
|
reservesAfterReveal: number;
|
|
feeChoices: NativeReserveSelections[] | undefined;
|
|
};
|
|
};
|
|
|
|
/** Genuine selected-family World programme, with optional all14 training and
|
|
* original Tech/Advanced Strongholds selected before the original initializer.
|
|
* Only that initializer's randomness is scoped. Basic low budgets come from
|
|
* two reveals, real Spice Blow collection and an ordinary paid shipment over
|
|
* four turns. Advanced uses its funded turn-one route, never a staged low budget
|
|
* or frozen later Storm.
|
|
* The inherited staging log explicitly labels conserved physical card moves;
|
|
* none of those moves is represented as a natural purchase. */
|
|
export function createNativeWorldMarkerFixture(
|
|
options: NativeWorldMarkerOptions = {},
|
|
): NativeWorldMarkerFixture {
|
|
const family = options.family ?? 'choam';
|
|
const selected = createIndependentCardModulesFixture({
|
|
advanced: options.advanced ?? false,
|
|
homeworlds: true,
|
|
tech: options.tech ?? false,
|
|
strongholds: options.strongholds ?? false,
|
|
skills: options.skills,
|
|
training: options.training,
|
|
expansions: family === 'choam' ? ['choam'] : ['ix', 'choam'],
|
|
factions: family === 'choam' ? ['richese', 'guild', 'atreides'] : ['richese', 'ixians', 'atreides'],
|
|
});
|
|
const readyLobby = selected.initial;
|
|
const fixture = createRicheseReinforcementsFixture({ ...options, family, initial: readyLobby });
|
|
const view = viewGame(fixture.game, fixture.richese);
|
|
const homeworld = view.homeworlds?.worlds?.find(home => home.native === fixture.richese && !home.secondary);
|
|
const preview = view.battle!.reinforcements!;
|
|
assert.ok(homeworld && preview.noField);
|
|
return {
|
|
...fixture,
|
|
readyLobby,
|
|
readySteps: selected.steps.filter(step => step.action.type === 'ready'),
|
|
actors: { owner: fixture.richese, opponent: fixture.opponent, observer: fixture.observer },
|
|
source: {
|
|
homeworld: homeworld.id,
|
|
reservesBeforeReveal: homeworld.forces[fixture.richese].normal,
|
|
materializes: preview.noField.materializes,
|
|
reservesAfterReveal: preview.noField.reservesAfterReveal,
|
|
feeChoices: structuredClone(preview.homeworldSources),
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Original public inventory counts native Worlds within reserves exactly once,
|
|
* keeps foreign visitors separate and preserves Ixian elite identity. */
|
|
export function nativeWorldMarkerInventory(game: Game, actor: string) {
|
|
const view = viewGame(game, actor);
|
|
const player = view.players.find(seat => seat.id === actor);
|
|
const worlds = view.homeworlds?.worlds;
|
|
assert.ok(player && worlds);
|
|
const inventory = forceInventory(player, worlds);
|
|
assert.ok(inventory, `Original native force inventory is inconsistent for ${actor}`);
|
|
return inventory;
|
|
}
|
|
|
|
/** Both independent machines may occupy either actual battle-plan slot. For a
|
|
* combined plan slot selects R's slot; H occupies the other. The fee allocation
|
|
* is the owner's real preview quote, never an inferred native source split. */
|
|
export function nativeWorldMarkerPlan(fixture: NativeWorldMarkerFixture,
|
|
mode: 'reinforcements' | 'both' | 'harass' | 'plain' = 'reinforcements',
|
|
slot: 'weapon' | 'defense' = 'weapon'): RicheseReinforcementsStep {
|
|
const step = structuredClone(fixture.plans.plain);
|
|
step.action.weapon = null;
|
|
step.action.defense = null;
|
|
if (mode === 'reinforcements' || mode === 'both') {
|
|
step.action[slot] = RICHESE_REINFORCEMENTS;
|
|
const sources = fixture.source.feeChoices?.[0];
|
|
if (sources) step.action.reinforcementsSources = structuredClone(sources);
|
|
}
|
|
if (mode === 'harass') step.action[slot] = RICHESE_HARASS;
|
|
if (mode === 'both') step.action[slot === 'weapon' ? 'defense' : 'weapon'] = RICHESE_HARASS;
|
|
return step;
|
|
}
|