129 lines
7.8 KiB
TypeScript
129 lines
7.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { applyAction, createGame, handLimit, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
|
|
import type { Card } from '../game/cards';
|
|
import {
|
|
advanceIndependentSkillsBattle, assertIndependentSkillsBattleCustody,
|
|
createIndependentSkillsBattleFixture, createIndependentSkillsBattleSetup, finishIndependentSkillsBattle,
|
|
independentSkillsBattleClean, independentSkillsBattlePlayer,
|
|
type IndependentSkillsBattleStep,
|
|
} from './fixture-independent-skills-battle';
|
|
|
|
export type PlanetologistIndependentOptions = {
|
|
advanced?: boolean;
|
|
effect?: 'reinforcements' | 'harassWithdraw' | 'recruits';
|
|
alternate?: boolean;
|
|
poison?: boolean;
|
|
};
|
|
|
|
function conserve(game: Game, actor: string, matches: (card: Card) => boolean, staging: string[]): Card {
|
|
const player = independentSkillsBattlePlayer(game, actor);
|
|
const held = player.hand.find(matches);
|
|
if (held) return held;
|
|
assert.ok(player.hand.length < handLimit(player));
|
|
for (const [zone, label] of [[game.deck, 'undealt deck'], [game.discard, 'discard']] as const) {
|
|
const index = zone.findIndex(matches);
|
|
if (index < 0) continue;
|
|
const card = zone.splice(index, 1)[0];
|
|
player.hand.push(card);
|
|
staging.push(`Conserved original ${card.id} from ${label} to ${actor}; no purchase is claimed.`);
|
|
return card;
|
|
}
|
|
throw Error('The original selected card must remain in stock or already be held.');
|
|
}
|
|
|
|
export function createPlanetologistIndependentFixture(options: PlanetologistIndependentOptions = {}) {
|
|
const base = createIndependentSkillsBattleFixture({ advanced: options.advanced ?? true,
|
|
skills: { owner: 'planetologist' } });
|
|
let game = structuredClone(base.beforeBattle);
|
|
const staging = [...base.staging];
|
|
const before = base.actions.findIndex(step => step.action.type === 'chooseBattle');
|
|
assert.ok(before >= 0);
|
|
const actions = base.actions.slice(0, before);
|
|
const effect = options.effect ?? 'reinforcements';
|
|
const weapon = conserve(game, base.owner, card => card.effect === effect, staging);
|
|
const defense = conserve(game, base.owner, card => card.effect === (effect === 'harassWithdraw' ? 'reinforcements' : 'harassWithdraw'), staging);
|
|
const opposingWeapon = options.poison ? conserve(game, base.opponent, card => card.kind === 'poison', staging).id
|
|
: base.plans[1].action.weapon;
|
|
assertIndependentSkillsBattleCustody(game);
|
|
const beforeBattle = structuredClone(game);
|
|
actions.push(base.battleStep);
|
|
game = applyAction(game, base.battleStep.actor, base.battleStep.action);
|
|
game = advanceIndependentSkillsBattle(game,
|
|
state => !!state.battle && !state.battle.revealed && independentSkillsBattleClean(state),
|
|
actions, { hide: { [base.owner]: true, [base.opponent]: true } });
|
|
const plans: IndependentSkillsBattleStep[] = [
|
|
{ actor: base.owner, action: { type: 'battlePlan', dial: 2, support: game.advanced ? 2 : 0,
|
|
leader: base.trainers.owner, weapon: weapon.id, defense: defense.id,
|
|
...(effect === 'recruits' ? {} : { planetologistSpecial: options.alternate !== false }) } },
|
|
{ actor: base.opponent, action: { ...base.plans[1].action, weapon: opposingWeapon } },
|
|
];
|
|
staging.push('Branch from the original pre-battle position; make real hidden postures and select the trained Planetologist. No army, wallet, clock or skill assignment changes.');
|
|
return { initial: base.initial, offered: base.offered, beforeBattle, game,
|
|
owner: base.owner, opponent: base.opponent, observer: base.observer,
|
|
trainer: base.trainers.owner, location: base.location, first: base.first, second: base.second,
|
|
returns: base.returns, plans, actions, staging, effect, alternate: effect === 'recruits' || options.alternate !== false };
|
|
}
|
|
export type PlanetologistIndependentFixture = ReturnType<typeof createPlanetologistIndependentFixture>;
|
|
export function sealPlanetologistIndependent(fixture: PlanetologistIndependentFixture): Game {
|
|
let game = fixture.game;
|
|
for (const step of fixture.plans) game = applyAction(game, step.actor, step.action);
|
|
assert.ok(game.battle?.revealed);
|
|
return game;
|
|
}
|
|
export function finishPlanetologistIndependent(game: Game, fixture: PlanetologistIndependentFixture,
|
|
actions?: IndependentSkillsBattleStep[]): Game {
|
|
return finishIndependentSkillsBattle(game, { return: fixture.returns, actions });
|
|
}
|
|
|
|
/** Actual stronghold deployment empties the reserves before the later utility plan. */
|
|
export function createPlanetologistEmptyReserveFixture(): PlanetologistIndependentFixture {
|
|
const initial = createGame('PLANETEMPTY', newPlayer('h', 'Harkonnen', 'harkonnen'), true);
|
|
joinGame(initial, newPlayer('g', 'Guild', 'guild'));
|
|
joinGame(initial, newPlayer('e', 'Emperor', 'emperor'));
|
|
const setup = createIndependentSkillsBattleSetup({ initial, advanced: true,
|
|
skills: { owner: 'planetologist', opponent: 'rihani-decipherer', observer: 'prana-bindu-adept' } });
|
|
const actions = [...setup.actions], staging = [...setup.staging];
|
|
let game = setup.afterSetup;
|
|
const act = (actor: string, action: Action) => {
|
|
actions.push({ actor, action });
|
|
game = applyAction(game, actor, action);
|
|
};
|
|
game = advanceIndependentSkillsBattle(game,
|
|
state => state.phase === 5 && state.active === 'h' && independentSkillsBattleClean(state), actions);
|
|
act('h', { type: 'ship', territory: 'carthag', sector: 11, amount: 10 });
|
|
game = advanceIndependentSkillsBattle(game, state => state.phase === 5 && state.active === 'h' &&
|
|
independentSkillsBattleClean(state) && !state.pendingShipment, actions);
|
|
act('h', { type: 'move', from: 'carthag:11', territory: 'imperial_basin', sector: 10, amount: 5 });
|
|
act('h', { type: 'endMovement' });
|
|
game = advanceIndependentSkillsBattle(game,
|
|
state => state.phase === 5 && state.active === 'e' && independentSkillsBattleClean(state), actions);
|
|
act('e', { type: 'ship', territory: 'imperial_basin', sector: 10, amount: 2 });
|
|
game = advanceIndependentSkillsBattle(game,
|
|
state => state.phase === 6 && independentSkillsBattleClean(state), actions);
|
|
assert.equal(independentSkillsBattlePlayer(game, 'h').reserves, 0);
|
|
conserve(game, 'h', card => card.id === 'ecaz-reinforcements', staging);
|
|
conserve(game, 'h', card => card.id === 'ecaz-harass-withdraw', staging);
|
|
const beforeBattle = structuredClone(game);
|
|
const choice = viewGame(game, game.active!).battleChoices.find(candidate => candidate.territory === 'imperial_basin');
|
|
assert.ok(choice && choice.attacker === 'h');
|
|
act(choice.chooser, { type: 'chooseBattle', territory: choice.territory, target: choice.defender });
|
|
game = advanceIndependentSkillsBattle(game,
|
|
state => !!state.battle && !state.battle.revealed && independentSkillsBattleClean(state),
|
|
actions, { hide: { h: true, e: false } });
|
|
const other = independentSkillsBattlePlayer(game, 'e').leaders
|
|
.find(leader => !leader.dead && leader.id !== setup.trainers.observer)!;
|
|
const plans: IndependentSkillsBattleStep[] = [
|
|
{ actor: 'h', action: { type: 'battlePlan', leader: setup.trainers.owner,
|
|
weapon: 'ecaz-reinforcements', defense: 'ecaz-harass-withdraw',
|
|
dial: 0.5, support: 0, planetologistSpecial: true } },
|
|
{ actor: 'e', action: { type: 'battlePlan', leader: other.id, dial: 0, support: 0 } },
|
|
];
|
|
staging.push('Harkonnen paid its real ten-spice shipment into its own Carthag, then moved five original counters into Imperial Basin. Zero reserves and a half-strength unpaid dial are native outcomes, not staged resources.');
|
|
assertIndependentSkillsBattleCustody(game);
|
|
return { initial: setup.initial, offered: setup.offered, beforeBattle, game,
|
|
owner: 'h', opponent: 'e', observer: 'g', trainer: setup.trainers.owner,
|
|
location: 'imperial_basin', first: 'imperial_basin:10', second: 'imperial_basin:10',
|
|
returns: { 'imperial_basin:10': { normal: 4, elite: 0 } },
|
|
plans, actions, staging, effect: 'reinforcements', alternate: true };
|
|
}
|