149 lines
8.0 KiB
TypeScript
149 lines
8.0 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { viewGame, type Game } from '../game/engine';
|
|
import { TERRITORIES } from '../game/board';
|
|
import {
|
|
advanceNexusDuke, assertNexusDukeCustody, closeNexusDukeDraw,
|
|
createNexusDukeCaptureFixture, createNexusDukeDeathFixture, nexusDukeClean, nexusDukePlayer,
|
|
openNexusDukeBattle, sealNexusDukeBattle, stepNexusDuke,
|
|
type NexusDukePlan, type NexusDukeStep,
|
|
} from './fixture-nexus-duke-capture';
|
|
|
|
type Transition = { before: Game; after: Game; leader: string };
|
|
export type NativeEcazCohortFixture = {
|
|
game: Game; owner: string; captor: string; plan: NexusDukePlan; actions: NexusDukeStep[];
|
|
deaths: Transition[]; funding: { before: Game; after: Game; amount: number };
|
|
first: Transition; repeat: Transition;
|
|
};
|
|
|
|
function poisonDeath(game: Game, context: Pick<NativeEcazCohortFixture, 'owner' | 'captor' | 'plan' | 'actions'>,
|
|
leader: string, territory = 'imperial_basin'): Transition {
|
|
const { owner, captor, plan, actions } = context;
|
|
// Choose after the real next Storm; a storm-separated target can otherwise
|
|
// skip that entire Battle phase and run this bounded chronology past turn ten.
|
|
if (game.phase > 6)
|
|
game = advanceNexusDuke(game, g => g.phase === 4 && nexusDukeClean(g), plan, actions);
|
|
if (territory === 'imperial_basin') {
|
|
const usable = (target: typeof TERRITORIES[number]) => target.type === 'sand' &&
|
|
!target.sectors.includes(game.storm) &&
|
|
!game.players.some(p => p.id !== owner && p.id !== captor &&
|
|
Object.entries(p.forces).some(([key, amount]) => amount > 0 && key.startsWith(`${target.id}:`)));
|
|
const target = TERRITORIES.find(t => t.id === territory && usable(t)) ?? TERRITORIES.find(usable);
|
|
assert.ok(target, 'The original board must offer an uncontested non-storm desert for both shipments.');
|
|
territory = target.id;
|
|
}
|
|
plan.ship = { [owner]: { territory, amount: 1 }, [captor]: { territory, amount: 1 } };
|
|
const battle = openNexusDukeBattle(game, plan, actions, { territory, first: owner, second: captor });
|
|
const poison = nexusDukePlayer(battle.game, captor).hand.find(c => c.kind === 'poison');
|
|
assert.ok(poison, 'The original winning Harkonnen hand retains its dealt poison.');
|
|
const enemy = nexusDukePlayer(battle.game, captor).leaders
|
|
.filter(l => !l.dead).sort((a, b) => a.strength - b.strength)[0];
|
|
assert.ok(enemy);
|
|
const before = structuredClone(battle.game);
|
|
const revealed = sealNexusDukeBattle(battle.game, plan, actions, [
|
|
{ actor: owner, leader, dial: 0, support: 0 },
|
|
{ actor: captor, leader: enemy.id, dial: 0, support: 0, weapon: poison.id },
|
|
]);
|
|
const after = advanceNexusDuke(revealed, g => !g.battle && nexusDukeClean(g), plan, actions);
|
|
assert.equal(nexusDukePlayer(after, owner).leaders.find(l => l.id === leader)!.dead, true);
|
|
assertNexusDukeCustody(after);
|
|
return { before, after, leader };
|
|
}
|
|
|
|
/** No death/history/hand/wallet assignment: original setup deal, five native
|
|
* poison deaths, a paid return and that same disc's real repeat death. */
|
|
export function createNativeEcazCohortFixture(): NativeEcazCohortFixture {
|
|
const f = createNexusDukeCaptureFixture({ variant: 'unused', deal: faction =>
|
|
faction === 'harkonnen' ? ['worthless', 'poison'] : ['worthless'] });
|
|
const { owner, captor, plan, actions } = f;
|
|
plan.capture = 'decline';
|
|
const closing = closeNexusDukeDraw(f.game, plan, actions);
|
|
const native = nexusDukePlayer(closing.game, owner).leaders
|
|
.slice().sort((a, b) => a.strength - b.strength).map(l => l.id);
|
|
let game = closing.game;
|
|
const kill = (leader: string): Transition => {
|
|
const result = poisonDeath(game, f, leader);
|
|
game = result.after;
|
|
return result;
|
|
};
|
|
const deaths = native.map(kill);
|
|
assert.ok(nexusDukePlayer(game, owner).leaders.every(l => l.dead && l.deaths === 1));
|
|
game = advanceNexusDuke(game, g => g.phase === 7 && nexusDukeClean(g), plan, actions);
|
|
const fundingBefore = structuredClone(game), amount = 15;
|
|
assert.ok(nexusDukePlayer(game, captor).spice >= amount);
|
|
game = stepNexusDuke(game, plan, { actor: captor,
|
|
action: { type: 'bribe', target: owner, amount } }, actions);
|
|
const funding = { before: fundingBefore, after: structuredClone(game), amount };
|
|
game = advanceNexusDuke(game, g => g.phase === 4 && nexusDukeClean(g), plan, actions);
|
|
const firstBefore = structuredClone(game);
|
|
const firstLeader = native[0];
|
|
assert.ok(viewGame(game, owner).revival.leaders.some(l => l.id === firstLeader));
|
|
game = stepNexusDuke(game, plan, { actor: owner,
|
|
action: { type: 'reviveLeader', leader: firstLeader } }, actions);
|
|
const first = { before: firstBefore, after: structuredClone(game), leader: firstLeader };
|
|
const repeat = kill(firstLeader);
|
|
game = advanceNexusDuke(game, g => g.phase === 4 && nexusDukeClean(g), plan, actions);
|
|
plan.ship = {};
|
|
assert.equal(nexusDukePlayer(game, owner).leaders.find(l => l.id === firstLeader)!.deaths, 2);
|
|
assertNexusDukeCustody(game);
|
|
return { game, owner, captor, plan, actions, deaths, funding, first, repeat };
|
|
}
|
|
|
|
export type NativeEcazSecondCohortFixture = Pick<NativeEcazCohortFixture, 'game' | 'owner' | 'captor' | 'plan' | 'actions'> & {
|
|
dukeDeath: Game; deaths: Transition[]; paid: Transition[]; repeats: Transition[];
|
|
ghola: Transition; funding: { before: Game; after: Game; amount: number };
|
|
};
|
|
|
|
/** The original Duke and four native discs die; three native paid returns and
|
|
* one actual Ghola complete the cohort. All four then really die again, within
|
|
* the original ten-turn limit. The never-killed fifth native remains alive. */
|
|
export function createNativeEcazSecondCohortFixture(): NativeEcazSecondCohortFixture {
|
|
const death = createNexusDukeDeathFixture({ deal: faction => faction === 'ecaz' ? ['ghola']
|
|
: faction === 'harkonnen' ? ['worthless', 'poison'] : ['worthless'] });
|
|
const { owner, captor, plan, actions } = death;
|
|
plan.capture = 'decline';
|
|
const native = nexusDukePlayer(death.window, owner).leaders
|
|
.slice().sort((a, b) => a.strength - b.strength).map(l => l.id);
|
|
let game = death.window;
|
|
const deaths: Transition[] = [];
|
|
for (let i = 0; i < 4; i++) {
|
|
const result = poisonDeath(game, death, native[i], i === 0 ? 'carthag' : 'imperial_basin');
|
|
deaths.push(result);
|
|
game = result.after;
|
|
}
|
|
game = advanceNexusDuke(game, g => g.phase === 7 && nexusDukeClean(g), plan, actions);
|
|
const fundingBefore = structuredClone(game), amount = 25;
|
|
assert.ok(nexusDukePlayer(game, captor).spice >= amount);
|
|
game = stepNexusDuke(game, plan, { actor: captor,
|
|
action: { type: 'bribe', target: owner, amount } }, actions);
|
|
const funding = { before: fundingBefore, after: structuredClone(game), amount };
|
|
const paid: Transition[] = [], repeats: Transition[] = [];
|
|
let ghola!: Transition;
|
|
for (let i = 0; i < 3; i++) {
|
|
game = advanceNexusDuke(game, g => g.phase === 4 && nexusDukeClean(g), plan, actions);
|
|
const before = structuredClone(game);
|
|
game = stepNexusDuke(game, plan, { actor: owner,
|
|
action: { type: 'reviveLeader', leader: native[i] } }, actions);
|
|
paid.push({ before, after: structuredClone(game), leader: native[i] });
|
|
if (i === 2) {
|
|
const card = nexusDukePlayer(game, owner).hand.find(c => c.effect === 'ghola');
|
|
assert.ok(card);
|
|
const before = structuredClone(game);
|
|
game = stepNexusDuke(game, plan, { actor: owner,
|
|
action: { type: 'card', card: card.id, leader: native[3] } }, actions);
|
|
ghola = { before, after: structuredClone(game), leader: native[3] };
|
|
}
|
|
const repeated = poisonDeath(game, death, native[i]);
|
|
repeats.push(repeated);
|
|
game = repeated.after;
|
|
}
|
|
const last = poisonDeath(game, death, native[3]);
|
|
repeats.push(last);
|
|
game = advanceNexusDuke(last.after, g => g.phase === 4 && nexusDukeClean(g), plan, actions);
|
|
plan.ship = {};
|
|
assert.equal(nexusDukePlayer(game, owner).leaders.find(l => l.id === native[4])!.dead, false);
|
|
assert.equal(game.dukeVidal!.leader.dead, true);
|
|
assert.equal(game.dukeVidal!.leader.deaths, 1);
|
|
assertNexusDukeCustody(game);
|
|
return { game, owner, captor, plan, actions, dukeDeath: death.game, deaths, paid, repeats, ghola, funding };
|
|
}
|