Files
Dune/tests/phase-module-moritani-runtime.test.ts

119 lines
7.9 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame } from '../game/engine';
import { ownedTech, TECH_TOKENS } from '../game/tech-tokens';
import { STRONGHOLD_CARDS } from '../game/stronghold-cards';
import {
actAssassinateTarget as act, advanceAssassinateTarget as advance,
assassinateTargetInventory as inventory, assassinateTargetPlayer as player,
assassinateTargetTraitors as traitors, createMoritaniAssassinateTargetFixture as fixture,
finishMoritaniAssassinateTarget as finish, type AssassinateTargetStep,
} from './fixture-moritani-assassinate-target';
const cases = [
{ label: 'Tech', techTokens: true, strongholds: false, execute: false },
{ label: 'Strongholds', techTokens: false, strongholds: true, execute: false },
{ label: 'Tech and Strongholds', techTokens: true, strongholds: true, execute: false },
{ label: 'Tech and Strongholds with actual Execute', techTokens: true, strongholds: true, execute: true },
] as const;
for (const selected of cases) {
void test(`native Advanced Harkonnen/Moritani ${selected.label} keeps original capture, assassination and phase-module rewards`, () => {
const f = fixture(selected);
assert.equal(f.initial.status, 'lobby');
assert.equal(!!f.initial.techTokens, false); assert.equal(!!f.initial.strongholdCards, false);
assert.equal(f.moduleSelections.length, Number(selected.techTokens) + Number(selected.strongholds));
for (const selection of f.moduleSelections) {
assert.equal(selection.before.status, 'lobby'); assert.equal(selection.after.status, 'lobby');
assert.ok(selection.before.players.every(seat => !seat.hand.length && !seat.ready));
assert.equal(selection.steps.length, 1); assert.equal(selection.steps[0].actor, f.initial.host);
assert.ok(['techTokens', 'strongholdCards'].includes(selection.steps[0].action.type));
assert.equal(selection.steps[0].action.enabled, true);
}
assert.ok(f.actions.some(step => step.action.type === 'start' && step.action.advancedPreview === true));
assert.equal(f.setup.status, 'setup'); assert.ok(f.setup.moritaniAssassinate);
assert.deepEqual(f.setup.players.map(seat => seat.faction), ['harkonnen', 'emperor', 'moritani']);
assert.equal(!!f.setup.techTokens, selected.techTokens);
assert.equal(!!f.setup.strongholdCards, selected.strongholds);
assert.ok(!f.game.homeworlds && !f.game.leaderSkills && !f.game.nexusCards &&
!f.game.discoveryEnabled && !f.game.semutaPreview && !f.game.ecazTreachery);
assert.equal(f.originalInventory.length, 33);
assert.deepEqual(inventory(f.game), f.originalInventory); assert.deepEqual(traitors(f.game), f.originalTraitors);
assert.equal(f.sourceActionCount, f.actions.length);
assert.equal(f.shipment.steps[0].actor, f.opponent);
assert.equal(f.shipment.steps[0].action.type, 'ship');
assert.equal(player(f.shipment.before, f.opponent).spice - player(f.shipment.after, f.opponent).spice, 2);
assert.equal(player(f.movement.after, f.opponent).forces['carthag:11'], 1);
assert.equal(player(f.movement.after, f.opponent).forces['imperial_basin:11'], 1);
assert.equal(f.beforeCaptureChoice.decision?.kind, 'capturedLeader');
assert.ok(f.capture.steps.some(step => step.action.type === 'decision' && step.action.accept === true));
assert.equal(f.captureChoiceStep.action.mode, selected.execute ? 'execute' : 'keep');
const captured = player(f.afterCapture, f.opponent).leaders.find(disc => disc.id === f.target)!;
assert.ok(captured.concealed);
assert.equal(captured.dead, selected.execute);
assert.equal(captured.capturedBy, selected.execute ? undefined : f.captor);
const emperorToken = selected.techTokens ? ownedTech(f.beforeFirstBattle.techTokens, f.opponent) : [];
const moritaniToken = selected.techTokens ? ownedTech(f.beforeSecondBattle.techTokens, f.owner) : [];
if (selected.techTokens) {
assert.equal(emperorToken.length, 1); assert.equal(moritaniToken.length, 1);
assert.equal(new Set(TECH_TOKENS.map(token => f.beforeFirstBattle.techTokens![token.id].owner)).size, 3,
'The real first Storm assigns one original token to each original seat');
assert.equal(f.afterCapture.techTokens![emperorToken[0]].owner, f.captor,
'The first real Harkonnen win earns the Emperor token through original cleanup');
assert.equal(f.game.techTokens![moritaniToken[0]].owner, f.owner,
'The later winner reward waits for this actual assassination opportunity');
}
if (selected.strongholds) {
assert.equal(f.setup.strongholdCards?.claimedTurn, 0);
assert.equal(Object.keys(f.setup.strongholdCards!.owners).length, STRONGHOLD_CARDS.length);
assert.ok(Object.values(f.game.strongholdCards!.owners).every(owner => owner === null),
'No Stronghold advantage is assigned before the first actual END Mentat');
}
const pending = viewGame(f.game, f.owner).moritaniAssassinate?.pending; assert.ok(pending);
assert.ok(pending.cards.some(choice => choice.card === f.target));
assert.equal(f.receipt.opposingLeader, 'emperor-0'); assert.notEqual(f.target, f.receipt.opposingLeader);
const targetBefore = structuredClone(player(f.game, f.opponent).leaders.find(disc => disc.id === f.target)!);
const money = player(f.game, f.owner).spice;
let game = act(f.game, f.owner, f.reveal);
const killed = player(game, f.opponent).leaders.find(disc => disc.id === f.target)!;
assert.equal(killed.dead, true);
assert.equal(killed.deaths, targetBefore.deaths + Number(!selected.execute));
assert.equal(killed.capturedBy, undefined); assert.equal(killed.concealed, undefined);
assert.equal(killed.faction, 'emperor'); assert.equal(killed.strength, targetBefore.strength);
assert.equal(killed.controller, targetBefore.controller); assert.equal(killed.gholaBy, targetBefore.gholaBy);
assert.equal(player(game, f.owner).spice, money + (selected.execute ? 0 : f.potentialBounty));
assert.equal(game.moritaniAssassinate!.opportunities.at(-1)!.bounty, selected.execute ? 0 : f.potentialBounty);
const continuation: AssassinateTargetStep[] = [];
game = finish(game, f, continuation);
const receipt = game.moritaniAssassinate!.opportunities.find(r => r.event === f.event)!;
assert.equal(receipt.stage, 'replaced'); assert.ok(receipt.replacement && receipt.replacement !== f.target);
assert.equal(player(game, f.owner).traitors.includes(f.target), false);
assert.ok(player(game, f.owner).traitors.includes(receipt.replacement));
assert.deepEqual(traitors(game), f.originalTraitors); assert.deepEqual(inventory(game), f.originalInventory);
if (selected.techTokens) {
assert.equal(game.techTokens![emperorToken[0]].owner, f.captor);
assert.equal(game.techTokens![moritaniToken[0]].owner, f.opponent,
'The surviving native Emperor winner receives the original Moritani token once');
assert.equal(ownedTech(game.techTokens, f.captor).length, 2);
assert.equal(ownedTech(game.techTokens, f.opponent).length, 1);
assert.equal(ownedTech(game.techTokens, f.owner).length, 0);
}
game = advance(game, state => state.turn === 2 && state.phase === 0, continuation);
if (selected.strongholds) {
assert.equal(game.strongholdCards!.claimedTurn, 1);
assert.equal(game.strongholdCards!.owners.carthag, f.captor,
'The actual first END Mentat awards the city still physically held after the native capture');
assert.ok(Object.entries(game.strongholdCards!.owners).every(([id, owner]) => id === 'carthag' || owner === null));
}
assert.ok(continuation.some(step => step.action.type === 'ready'));
assert.deepEqual(inventory(game), f.originalInventory); assert.deepEqual(traitors(game), f.originalTraitors);
const finalTarget = player(game, f.opponent).leaders.find(disc => disc.id === f.target)!;
assert.equal(finalTarget.deaths, killed.deaths);
assert.equal(finalTarget.capturedBy, undefined); assert.equal(finalTarget.concealed, undefined);
});
}