168 lines
8.8 KiB
TypeScript
168 lines
8.8 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DUKE_VIDAL_ID, dukeController, retireHomeworldDukeTenure } from '../game/duke-vidal';
|
|
import { quoteHomeworldEcazDuke, releaseCapturedHomeworldDuke } from '../game/homeworld-ecaz-duke';
|
|
import {
|
|
createHomeworldEcazDukeCaptiveFixture, expireEcazDukeCaptive,
|
|
useExpiredEcazDukeCaptive, controlledEcazDukeCaptiveDeparture,
|
|
actEcazDukeCaptive as act, ecazDukeCaptivePlayer as p,
|
|
ecazDukeCaptiveInventory as inventory,
|
|
type HomeworldEcazDukeCaptiveFixture,
|
|
} from './fixture-homeworld-ecaz-duke-captive';
|
|
|
|
let earned: HomeworldEcazDukeCaptiveFixture | undefined;
|
|
const fixture = () => earned ??= createHomeworldEcazDukeCaptiveFixture();
|
|
const reload = (g: Game): Game => structuredClone(g);
|
|
function reject(game: Game, actor: string, action: Action) {
|
|
const before = reload(game);
|
|
assert.throws(() => applyAction(game,actor,action));
|
|
assert.deepEqual(game,before);
|
|
}
|
|
function disc(game: Game) {
|
|
assert.equal(game.dukeVidal!.leader.id,DUKE_VIDAL_ID);
|
|
assert.equal(game.players.flatMap(p => p.leaders).filter(l => l.id === DUKE_VIDAL_ID).length,0);
|
|
assert.deepEqual(inventory(game),fixture().source.originalInventory);
|
|
}
|
|
|
|
void test('native occupied-Ecaz capture starts with original Harkonnen, actual World lease and unused-Duke lottery', () => {
|
|
const f = fixture(), source = f.source;
|
|
assert.match(f.disclosure,/Original public Advanced.*no installed occupation, lease or capture receipt/);
|
|
assert.equal(source.initial.players.length,5);
|
|
assert.equal(source.initial.players.find(p => p.id === f.captor)!.faction,'harkonnen');
|
|
assert.equal(p(source.afterSetup,f.captor).forces['carthag:11'],10);
|
|
assert.equal(p(source.afterSetup,f.captor).hand.length,2);
|
|
assert.equal(source.originalInventory.length,33);
|
|
assert.equal(p(source.duneShipment.after,f.owner).forces['sietch_tabr:14'],2);
|
|
assert.equal(source.secondEntrant!.steps[0].actor,f.captor);
|
|
assert.equal(source.qualification.player,f.owner);
|
|
assert.equal(source.qualification.cause,'sole');
|
|
assert.equal(f.beforeBattle.dukeVidal!.controller,f.owner);
|
|
assert.equal(f.beforeBattle.dukeVidal!.source,'homeworldEcaz');
|
|
assert.equal(f.beforeBattle.dukeVidal!.leader.usedAt,undefined);
|
|
assert.ok(Object.values(f.plans).every(plan => plan.leader !== f.dukeId));
|
|
assert.ok(f.candidates.includes(f.dukeId));
|
|
assert.equal(f.selected.decision?.kind === 'capturedLeader' && f.selected.decision.leader,f.dukeId);
|
|
assert.equal(f.game.dukeVidal!.leader.capturedBy,f.captor);
|
|
assert.equal(dukeController(f.game.dukeVidal!),f.captor);
|
|
assert.deepEqual(f.game.dukeVidal!.captureVisibility,{controller:f.owner,acquiredTurn:2,source:'homeworldEcaz'});
|
|
assert.equal(f.game.dukeVidal!.leader.usedAt,undefined);
|
|
assert.equal(f.sourceCount,f.sourceActions.length);
|
|
assert.equal(f.captureActions.filter(step => step.action.type === 'battlePlan').length,2);
|
|
assert.equal(f.captureActions.filter(step => step.action.type === 'decision' && step.action.accept === true).length,1);
|
|
disc(f.game);
|
|
});
|
|
|
|
void test('actual next-turn expiry retires occupied-Ecaz underlay while Harkonnen keeps the same unused living captive', () => {
|
|
const f = fixture(), expired = expireEcazDukeCaptive(f), duke = expired.game.dukeVidal!;
|
|
assert.equal(expired.game.turn,3);
|
|
assert.ok(expired.actions.length > 0);
|
|
assert.equal(duke.controller,null);
|
|
assert.equal(duke.acquiredTurn,null);
|
|
assert.equal(duke.source,null);
|
|
assert.deepEqual(duke.captureVisibility,{controller:null,acquiredTurn:null,source:null});
|
|
assert.equal(duke.leader.capturedBy,f.captor);
|
|
assert.equal(duke.leader.concealed!.controller,f.owner);
|
|
assert.equal(duke.leader.dead,false);
|
|
assert.equal(duke.leader.deaths,f.game.dukeVidal!.leader.deaths);
|
|
assert.equal(duke.leader.usedAt,undefined);
|
|
assert.equal(dukeController(duke),f.captor);
|
|
assert.deepEqual(expired.game.homeworldEcazDukeGrants,f.game.homeworldEcazDukeGrants);
|
|
const offer = quoteHomeworldEcazDuke(expired.game);
|
|
assert.equal(offer.owner,f.owner);
|
|
assert.match(offer.blocked!,/captur|conceal|custody/i);
|
|
assert.deepEqual(retireHomeworldDukeTenure(duke,expired.game.turn),duke);
|
|
disc(expired.game);
|
|
});
|
|
|
|
void test('real later captive-Duke battle use returns the expired shared disc set aside after a paid native entry', () => {
|
|
const f = fixture(), expired = expireEcazDukeCaptive(f);
|
|
const result = useExpiredEcazDukeCaptive(f,expired.game), game = result.game, duke = game.dukeVidal!;
|
|
assert.ok(result.actions.some(step => step.actor === f.owner && step.action.type === 'ship' &&
|
|
step.action.territory === f.territory && step.action.amount === 1));
|
|
assert.ok(result.actions.some(step => step.actor === f.captor && step.action.type === 'battlePlan' &&
|
|
step.action.leader === f.dukeId));
|
|
assert.equal(result.beforeBattle.dukeVidal!.leader.capturedBy,f.captor);
|
|
assert.equal(game.lastBattleContext!.winner,f.captor);
|
|
assert.equal(duke.leader.id,f.dukeId);
|
|
assert.equal(duke.leader.dead,false);
|
|
assert.equal(duke.leader.deaths,f.game.dukeVidal!.leader.deaths);
|
|
assert.equal(duke.leader.usedAt,f.territory);
|
|
assert.equal(duke.leader.capturedBy,undefined);
|
|
assert.equal(duke.leader.concealed,undefined);
|
|
assert.equal(duke.captureVisibility,undefined);
|
|
assert.equal(duke.controller,null);
|
|
assert.equal(duke.source,null);
|
|
assert.equal(duke.acquiredTurn,null);
|
|
disc(game);
|
|
});
|
|
|
|
void test('earned captive helper release restores the still-current qualified original occupied-Ecaz tenure', () => {
|
|
const f = fixture(), game = reload(f.game), before = reload(game);
|
|
const released = releaseCapturedHomeworldDuke(game,f.captor);
|
|
assert.equal(game.turn,2);
|
|
assert.equal(released.controller,f.owner);
|
|
assert.equal(released.source,'homeworldEcaz');
|
|
assert.equal(released.acquiredTurn,game.turn);
|
|
assert.equal(released.leader.id,f.dukeId);
|
|
assert.equal(released.leader.dead,false);
|
|
assert.equal(released.leader.deaths,game.dukeVidal!.leader.deaths);
|
|
assert.equal(released.leader.capturedBy,undefined);
|
|
assert.equal(released.leader.concealed,undefined);
|
|
assert.equal(released.captureVisibility,undefined);
|
|
assert.deepEqual(game,before,'This focused helper exercise is not a claimed native early-release action.');
|
|
});
|
|
|
|
void test('controlled conserved same-turn departure helper context cannot restore an ended occupied-Ecaz entitlement', () => {
|
|
const f = fixture(), game = controlledEcazDukeCaptiveDeparture(f.game);
|
|
// Explicit unit supplement: only present physical custody changed. No played
|
|
// departure, occupation observation, lease/capture receipt or clock is added.
|
|
assert.equal(game.turn,f.game.turn);
|
|
assert.deepEqual(game.homeworldOccupationHistory,f.game.homeworldOccupationHistory);
|
|
assert.deepEqual(game.homeworldEcazDukeGrants,f.game.homeworldEcazDukeGrants);
|
|
assert.equal(quoteHomeworldEcazDuke(game).owner,null);
|
|
const before = reload(game), released = releaseCapturedHomeworldDuke(game,f.captor);
|
|
assert.equal(released.controller,null);
|
|
assert.equal(released.source,null);
|
|
assert.equal(released.acquiredTurn,null);
|
|
assert.equal(released.leader.id,f.dukeId);
|
|
assert.equal(released.leader.dead,false);
|
|
assert.equal(released.leader.capturedBy,undefined);
|
|
assert.equal(released.captureVisibility,undefined);
|
|
assert.deepEqual(game,before);
|
|
disc(game);
|
|
});
|
|
|
|
void test('actual occupied-Ecaz acquisition remains blocked on the captive and foreign helper release rejects before mutation', () => {
|
|
const f = fixture(), expired = expireEcazDukeCaptive(f), game = expired.game;
|
|
const quote = quoteHomeworldEcazDuke(game); assert.ok(quote.event);
|
|
reject(game,f.owner,{type:'homeworldDuke',event:quote.event,recipient:f.owner});
|
|
const before = reload(game);
|
|
assert.throws(() => releaseCapturedHomeworldDuke(game,f.owner));
|
|
assert.deepEqual(game,before);
|
|
assert.equal(dukeController(game.dukeVidal!),f.captor);
|
|
disc(game);
|
|
});
|
|
|
|
void test('all four minimal profiles answer the actual occupied-Duke captured-leader choice legally', () => {
|
|
const f = fixture();
|
|
for (const profile of ['Easy','Medium','Hard','Brutal'] as const) {
|
|
const game = reload(f.selected), view = viewGame(game,f.captor);
|
|
view.players.find(p => p.id === f.captor)!.bot = profile;
|
|
const action = botActions(view)[0];
|
|
assert.ok(action,`${profile} needs the actual retained/executed captive decision.`);
|
|
const next = act(game,f.captor,action);
|
|
if (action.mode === 'execute') {
|
|
assert.equal(next.dukeVidal!.leader.dead,true);
|
|
assert.equal(next.dukeVidal!.leader.capturedBy,undefined);
|
|
// The native Keep baseline includes the same automatic Carthag income.
|
|
assert.equal(p(next,f.captor).spice,p(f.game,f.captor).spice+2);
|
|
} else {
|
|
assert.equal(action.mode,'keep');
|
|
assert.equal(next.dukeVidal!.leader.capturedBy,f.captor);
|
|
}
|
|
disc(next);
|
|
}
|
|
});
|