Files
Dune/tests/nexus-duke-capture-runtime.test.ts
T

235 lines
13 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame } from '../game/engine';
import { DUKE_VIDAL_ID, dukeController } from '../game/duke-vidal';
import {
advanceNexusDuke, assertNexusDukeCustody, createNexusDukeCaptureFixture,
createNexusDukeDeathFixture, nexusDukeClean, nexusDukePlayer, quietNexusDukeWindow,
reclaimCapturedNexusDuke, releaseCapturedNexusDukeByAllHarkDead,
releaseCapturedNexusDukeByUse, openNexusDukeBattle, sealNexusDukeBattle, stepNexusDuke,
} from './fixture-nexus-duke-capture';
void test('a real Harkonnen Battle win captures the unused shared Duke', () => {
const fixture = createNexusDukeCaptureFixture({ variant: 'unused' });
const { owner, captor, game } = fixture;
assert.ok(fixture.candidates.some((leader) => leader.id === DUKE_VIDAL_ID),
'The shared disc must be in the actual original capture pool.');
const duke = game.dukeVidal!;
assert.equal(dukeController(duke), captor);
assert.equal(duke.controller, owner, 'An unused disc keeps its underlying Ecaz tenure.');
assert.equal(duke.source, 'ecaz');
assert.equal(duke.leader.capturedBy, captor);
assert.equal(duke.leader.dead, false);
assert.equal(duke.leader.deaths, 0);
assert.equal(duke.leader.usedAt, undefined);
assert.equal(duke.leader.gholaBy, undefined);
// The just-used native losing leader keeps its own one-battle release.
assert.equal(nexusDukePlayer(fixture.game, owner).leaders
.filter((leader) => leader.usedAt === 'carthag').length, 1);
assertNexusDukeCustody(game);
});
void test('a just-used Duke is captured from its original battle release through the witness', () => {
const fixture = createNexusDukeCaptureFixture({ variant: 'used' });
const { captor, game } = fixture;
const duke = game.dukeVidal!;
assert.equal(dukeController(duke), captor);
assert.equal(duke.controller, null, 'A released battle disc keeps no underlying control.');
assert.equal(duke.acquiredTurn, null);
assert.equal(duke.source, null);
assert.equal(duke.leader.capturedBy, captor);
assert.equal(duke.leader.usedAt, 'carthag');
assert.equal(duke.leader.dead, false);
assert.equal(duke.leader.deaths, 0);
assertNexusDukeCustody(game);
});
void test('executing the captive disc pays the original bounty and hides the death from unrelated seats', () => {
const fixture = createNexusDukeCaptureFixture({ variant: 'used', mode: 'execute' });
const { owner, captor } = fixture;
const before = fixture.decision!.before, after = fixture.decision!.after;
const prior = before.dukeVidal!;
const duke = after.dukeVidal!;
assert.equal(prior.leader.deaths, 0);
assert.equal(duke.leader.dead, true);
assert.equal(duke.leader.deaths, Math.max(2, prior.leader.deaths + 1));
assert.equal(duke.leader.capturedBy, undefined);
assert.equal(dukeController(duke), null, 'An executed disc is set aside in the Tanks.');
assert.equal(duke.controller, null);
assert.equal(duke.source, null);
assert.equal(duke.acquiredTurn, null);
assert.equal(duke.leader.faction, 'ecaz');
assert.equal(duke.leader.strength, 6);
assert.equal(nexusDukePlayer(after, captor).spice,
nexusDukePlayer(before, captor).spice + 2, 'The captor is paid the printed 2 spice.');
assert.equal(nexusDukePlayer(after, owner).spice, nexusDukePlayer(before, owner).spice);
assert.equal(nexusDukePlayer(after, owner).leaders.length, 5);
const unrelated = fixture.others.find((id) => id !== owner && id !== captor)!;
assert.deepEqual(viewGame(after, unrelated).dukeVidal!.leader,
viewGame(before, unrelated).dukeVidal!.leader,
'A private execution never discloses the secret selection or death to unrelated seats.');
assert.equal(viewGame(after, owner).dukeVidal!.leader.dead, true);
assert.equal(viewGame(after, captor).dukeVidal!.leader.dead, true);
assertNexusDukeCustody(after);
});
void test('genuine Ecaz Nexus Cunning liberates the actual captured disc through the real closing draw', () => {
const fixture = createNexusDukeCaptureFixture({ variant: 'unused' });
const { owner, captor, plan } = fixture;
assert.equal(dukeController(fixture.game.dukeVidal!), captor);
const reclaim = reclaimCapturedNexusDuke(fixture.game, plan);
const holder = nexusDukePlayer(reclaim.before, owner);
const spice = holder.spice, forces = structuredClone(holder.forces), tanks = holder.tanks;
const revived = holder.leaderRevived;
const duke = reclaim.after.dukeVidal!;
assert.equal(dukeController(duke), owner);
assert.equal(duke.controller, owner);
assert.equal(duke.source, 'ecazNexus');
assert.equal(duke.leader.capturedBy, undefined);
assert.equal(duke.leader.concealed, undefined);
assert.equal(duke.leader.dead, false);
assert.equal(duke.leader.deaths, fixture.duke.leader.deaths, 'Liberation preserves death history.');
assert.equal(duke.leader.gholaBy, undefined);
assert.ok(reclaim.after.nexusCards!.cards!.discard.includes('ecaz'),
'The physical Ecaz Nexus card is spent by the play.');
const liberator = nexusDukePlayer(reclaim.after, owner);
assert.equal(liberator.spice, spice, 'Liberation costs no spice.');
assert.deepEqual(liberator.forces, forces);
assert.equal(liberator.tanks, tanks);
assert.equal(liberator.leaderRevived, revived, 'Liberation spends no ordinary leader slot.');
assert.equal(liberator.leaders.length, 5);
assert.equal(nexusDukePlayer(reclaim.after, captor).spice, nexusDukePlayer(reclaim.before, captor).spice);
assertNexusDukeCustody(reclaim.after);
});
void test('a liberated Duke fights once through the native battle path', () => {
const fixture = createNexusDukeCaptureFixture({ variant: 'unused' });
const { owner, captor, plan, actions } = fixture;
const reclaim = reclaimCapturedNexusDuke(fixture.game, plan, actions, { battle: true });
const battle = openNexusDukeBattle(reclaim.after, plan, actions,
{ territory: 'carthag', first: owner, second: captor });
const enemy = nexusDukePlayer(battle.game, captor).leaders
.filter((leader) => !leader.dead).sort((a, b) => a.strength - b.strength)[0];
assert.ok(enemy);
const revealed = sealNexusDukeBattle(battle.game, plan, actions, [
{ actor: owner, leader: DUKE_VIDAL_ID, dial: 0, support: 0 },
{ actor: captor, leader: enemy.id, dial: 0, support: 0 },
]);
const after = advanceNexusDuke(revealed, (game) => !game.battle && nexusDukeClean(game), plan, actions);
assert.equal(after.dukeVidal!.leader.dead, false);
assert.equal(after.dukeVidal!.leader.deaths, fixture.duke.leader.deaths);
assert.equal(after.dukeVidal!.leader.usedAt, 'carthag');
assert.equal(dukeController(after.dukeVidal!), null);
assert.equal(after.dukeVidal!.leader.capturedBy, undefined);
assert.equal(after.dukeVidal!.leader.concealed, undefined);
assertNexusDukeCustody(after);
});
void test('unused liberated Nexus tenure expires at the original turn end without restoring captivity', () => {
const fixture = createNexusDukeCaptureFixture({ variant: 'unused' });
const { plan, actions } = fixture;
const reclaim = reclaimCapturedNexusDuke(fixture.game, plan, actions);
const battle = openNexusDukeBattle(reclaim.after, plan, actions,
{ territory: 'carthag', first: fixture.owner, second: fixture.captor });
const own = nexusDukePlayer(battle.game, fixture.owner).leaders
.filter(leader => !leader.dead).sort((a, b) => b.strength - a.strength)[0];
const enemy = nexusDukePlayer(battle.game, fixture.captor).leaders
.filter(leader => !leader.dead).sort((a, b) => a.strength - b.strength)[0];
assert.ok(own && enemy);
const revealed = sealNexusDukeBattle(battle.game, plan, actions, [
{ actor: fixture.owner, leader: own.id, dial: 0, support: 0 },
{ actor: fixture.captor, leader: enemy.id, dial: 0, support: 0 },
]);
const after = advanceNexusDuke(revealed, (game) => game.turn > reclaim.after.turn, plan, actions);
assert.equal(dukeController(after.dukeVidal!), null);
assert.equal(after.dukeVidal!.controller, null);
assert.equal(after.dukeVidal!.source, null);
assert.equal(after.dukeVidal!.acquiredTurn, null);
assert.equal(after.dukeVidal!.leader.capturedBy, undefined);
assert.equal(after.dukeVidal!.leader.concealed, undefined);
assert.equal(after.dukeVidal!.leader.dead, false);
assert.equal(after.dukeVidal!.leader.deaths, fixture.duke.leader.deaths);
assertNexusDukeCustody(after);
});
void test('a later real Battle use releases the retained captive disc', () => {
const fixture = createNexusDukeCaptureFixture({ variant: 'unused' });
const { owner, plan } = fixture;
const release = releaseCapturedNexusDukeByUse(fixture.game, plan);
const duke = release.game.dukeVidal!;
assert.equal(duke.leader.capturedBy, undefined);
assert.equal(duke.leader.concealed, undefined);
assert.equal(dukeController(duke), null);
assert.equal(duke.controller, null);
assert.equal(duke.acquiredTurn, null);
assert.equal(duke.source, null);
assert.equal(duke.leader.dead, false);
assert.equal(duke.leader.deaths, fixture.duke.leader.deaths);
assert.equal(duke.leader.usedAt, 'imperial_basin',
'The one-battle use stamps the shared disc at the real Battle territory.');
assert.equal(nexusDukePlayer(release.game, owner).leaders.every((leader) => !leader.dead), true);
assertNexusDukeCustody(release.game);
});
void test('all native Harkonnen leaders dead returns the captive disc to its underlying Ecaz custody', () => {
const fixture = createNexusDukeCaptureFixture({ variant: 'unused' });
const { owner, captor, plan } = fixture;
const returned = releaseCapturedNexusDukeByAllHarkDead(fixture.game, plan);
assert.equal(returned.deaths.length, 5);
assert.deepEqual(returned.deaths.map((entry) => entry.leader).sort(),
nexusDukePlayer(fixture.game, captor).leaders.map((leader) => leader.id).sort());
assert.deepEqual(nexusDukePlayer(returned.game, captor).leaders.filter((leader) => !leader.dead), []);
const duke = returned.game.dukeVidal!;
assert.equal(duke.leader.capturedBy, undefined);
assert.equal(duke.leader.concealed, undefined);
assert.equal(dukeController(duke), owner,
'The early return restores the still-valid original Ecaz tenure.');
assert.equal(duke.controller, owner);
assert.equal(duke.source, 'ecaz');
assert.equal(duke.acquiredTurn, 1);
assert.equal(duke.leader.dead, false);
assert.equal(duke.leader.deaths, fixture.duke.leader.deaths);
assertNexusDukeCustody(returned.game);
});
void test('a genuine dead Duke is dealt the printed Ecaz Ghola and then taken by quiet Cunning', () => {
const death = createNexusDukeDeathFixture({
deal: (faction) => faction === 'ecaz' ? ['ghola']
: faction === 'harkonnen' ? ['worthless', 'poison'] : ['worthless'],
});
const { owner, plan } = death;
assert.equal(death.game.dukeVidal!.leader.dead, true);
assert.ok(death.deaths >= 1);
assert.equal(death.game.dukeVidal!.leader.capturedBy, undefined);
assert.equal(death.game.dukeVidal!.leader.gholaBy, undefined);
assert.equal(death.window.nexusCards!.cards!.hands[owner], 'ecaz');
const ghola = nexusDukePlayer(death.window, owner).hand.find((card) => card.effect === 'ghola');
assert.ok(ghola, 'The real setup deal must leave Ecaz holding the printed Ghola card.');
const slot = nexusDukePlayer(death.window, owner).leaderRevived;
const revived = stepNexusDuke(death.window, plan,
{ actor: owner, action: { type: 'card', card: ghola.id, leader: DUKE_VIDAL_ID } }, death.actions);
const disc = revived.dukeVidal!;
assert.equal(disc.leader.dead, false);
assert.equal(disc.leader.deaths, death.deaths, 'The Ghola return preserves death history.');
assert.equal(disc.leader.usedAt, undefined);
assert.equal(disc.leader.gholaBy, undefined, 'No foreign-Ghola custody is ever produced.');
assert.equal(disc.controller, null);
assert.equal(disc.source, null);
assert.equal(dukeController(disc), null);
assert.equal(nexusDukePlayer(revived, owner).leaderRevived, slot,
'The printed Ghola spends no ordinary leader-revival allowance.');
assert.equal(nexusDukePlayer(revived, owner).leaders.length, 5);
assertNexusDukeCustody(revived);
const window = quietNexusDukeWindow(revived, plan);
const taken = stepNexusDuke(window.game, plan, { actor: owner, action: window.action }, death.actions);
assert.equal(dukeController(taken.dukeVidal!), owner);
assert.equal(taken.dukeVidal!.source, 'ecazNexus');
assert.equal(taken.dukeVidal!.leader.capturedBy, undefined);
assert.equal(taken.dukeVidal!.leader.gholaBy, undefined);
assert.equal(taken.dukeVidal!.leader.deaths, death.deaths);
assert.equal(nexusDukePlayer(taken, owner).leaders.length, 5);
assert.ok(!taken.players.some((player) => player.leaders.some((leader) => leader.id === DUKE_VIDAL_ID)));
assert.equal(taken.players.flatMap((player) => player.traitors).includes(DUKE_VIDAL_ID), false);
assertNexusDukeCustody(taken);
});