Files
Dune/tests/homeworld-ecaz-duke-engine.test.ts

115 lines
8.2 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { quoteEcazDukeRevival } from '../game/ecaz-duke-revival';
import {
createHomeworldEcazDukeFixture as fixture, actHomeworldEcazDuke as act,
advanceHomeworldEcazDuke as advance, homeworldEcazDukeClean as clean,
homeworldEcazDukePlayer as player, homeworldEcazDukeInventory as inventory,
homeworldEcazDukePhysical as physical, playHomeworldEcazDukeBattle as playBattle,
type HomeworldEcazDukeFixture,
} from './fixture-homeworld-ecaz-duke';
function reject(game: Game, actor: string, action: Action, pattern?: RegExp) {
const before = structuredClone(game);
if (pattern) assert.throws(() => applyAction(game, actor, action), pattern);
else assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before);
}
function nativeSource(f: HomeworldEcazDukeFixture) {
assert.equal(f.setup.status, 'setup'); assert.equal(f.setup.advanced, f.advanced); assert.ok(f.setup.homeworlds);
assert.deepEqual(f.setup.expansions, ['ecaz']); assert.ok(f.beforeStartingDeal.players.every(player => !player.hand.length));
assert.equal(f.originalInventory.length, 33); assert.deepEqual(inventory(f.game), f.originalInventory);
assert.equal(f.funding.steps[0].actor, f.guild); assert.equal(f.funding.steps[0].action.type, 'bribe');
assert.equal(player(f.funding.after, f.occupier).spice, player(f.funding.before, f.occupier).spice);
assert.equal(player(f.funding.after, f.occupier).bribes, player(f.funding.before, f.occupier).bribes + 1);
assert.equal(player(f.duneShipment.after, f.occupier).forces['carthag:11'], 2);
assert.equal(f.nexus.nexus, true); assert.equal(player(f.game, f.occupier).ally, f.ally);
assert.equal(f.alliance.steps.length, f.allied ? 2 : 0);
assert.ok(f.invasion && f.secondEntrant);
assert.equal(f.invasion.steps[0].actor, f.occupier); assert.equal(f.invasion.steps[0].action.type, 'homeworldShip');
assert.equal(f.secondEntrant.steps[0].actor, f.opponent); assert.equal(f.secondEntrant.steps[0].action.type, 'ship');
assert.equal(f.qualification.world, f.world); assert.equal(f.qualification.player, f.occupier);
assert.equal(f.qualification.turn, 2); assert.equal(f.qualification.cause, 'sole');
assert.equal(f.game.turn, 2); assert.equal(f.game.phase, 6); assert.ok(clean(f.game));
assert.equal(f.game.battle?.territory, f.secondTerritory); assert.deepEqual(f.game.battle?.plans, {});
assert.equal(f.game.dukeVidal?.leader.dead, false); assert.equal(f.game.dukeVidal?.leader.id, f.dukeId);
assert.equal(f.sourceActionCount, f.actions.length);
}
function grant(f: HomeworldEcazDukeFixture, recipient: string) {
const game = structuredClone(f.game), option = viewGame(game, f.occupier).homeworldEcazDuke;
assert.ok(option); assert.equal(option.blocked, null); assert.ok(option.recipients.some(player => player.id === recipient));
const action: Action = { type: 'homeworldDuke', event: option.event, recipient };
const before = structuredClone(physical(game)), granted = act(game, f.occupier, action);
assert.deepEqual(physical(granted), before); assert.equal(granted.dukeVidal?.controller, recipient);
assert.equal(granted.dukeVidal?.source, 'homeworldEcaz'); assert.equal(granted.dukeVidal?.acquiredTurn, granted.turn);
return { game: granted, action, option };
}
for (const advanced of [false, true]) void test(
`${advanced ? 'Advanced' : 'Basic'} native sole Ecaz World occupation automatically grants the single recipient Duke for one actual battle`, () => {
const f = fixture({ advanced }); nativeSource(f);
assert.equal(f.game.dukeVidal?.controller, f.occupier); assert.equal(f.game.dukeVidal?.source, 'homeworldEcaz');
assert.equal(f.game.homeworldEcazDukeGrants?.length, 1);
const battle = playBattle(f, structuredClone(f.game));
assert.equal(battle.game.dukeVidal?.controller, null); assert.equal(battle.game.dukeVidal?.leader.dead, false);
assert.equal(battle.game.homeworldEcazDukeGrants?.length, 1);
assert.deepEqual(inventory(battle.game), f.originalInventory);
});
for (const advanced of [false, true]) void test(
`${advanced ? 'Advanced' : 'Basic'} native allied Ecaz World occupation retains both real Duke choices and the ally uses one battle`, () => {
const f = fixture({ advanced, allied: true }); nativeSource(f); assert.ok(f.ally);
assert.equal(f.game.dukeVidal?.controller, null);
const option = viewGame(f.game, f.occupier).homeworldEcazDuke; assert.ok(option);
assert.deepEqual(option.recipients.map(player => player.id).sort(), [f.occupier, f.ally].sort());
const self = grant(f, f.occupier);
assert.equal(self.game.dukeVidal?.controller, f.occupier);
const allied = grant(f, f.ally); assert.equal(allied.game.homeworldEcazDukeGrants?.length, 1);
reject(allied.game, f.occupier, allied.action);
const battle = playBattle(f, allied.game);
assert.equal(battle.game.dukeVidal?.controller, null); assert.equal(battle.game.dukeVidal?.leader.dead, false);
assert.equal(battle.game.homeworldEcazDukeGrants?.length, 1);
reject(battle.game, f.occupier, allied.action);
assert.deepEqual(inventory(battle.game), f.originalInventory);
});
void test('native occupied Ecaz Duke choice rejects stale events, foreign actors and unrelated recipients before any control changes', () => {
const f = fixture({ allied: true }); nativeSource(f);
const option = viewGame(f.game, f.occupier).homeworldEcazDuke; assert.ok(option);
assert.equal(viewGame(f.game, f.opponent).homeworldEcazDuke ?? null, null);
const action: Action = { type: 'homeworldDuke', event: option.event, recipient: f.occupier };
reject(f.game, f.occupier, { ...action, event: `${option.event}:stale` });
reject(f.game, f.opponent, action); reject(f.game, f.occupier, { ...action, recipient: f.opponent });
reject(f.game, f.occupier, { ...action, recipient: f.ecaz });
assert.equal(f.game.dukeVidal?.controller, null); assert.deepEqual(inventory(f.game), f.originalInventory);
});
for (const advanced of [false, true]) void test(
`${advanced ? 'Advanced' : 'Basic'} occupied Ecaz Duke use can die natively without granting its foreign ally revival permission`, () => {
const f = fixture({ advanced, allied: true }); nativeSource(f); assert.ok(f.ally);
const granted = grant(f, f.ally), battle = playBattle(f, granted.game, { killDuke: true });
assert.equal(battle.game.dukeVidal?.leader.dead, true); assert.equal(battle.game.dukeVidal?.controller, null);
let game = advance(battle.game, state => state.turn === 3 && state.phase === 4 && clean(state));
assert.equal(game.dukeVidal?.leader.dead, true);
reject(game, f.ally, { type: 'reviveLeader', leader: f.dukeId }, /Ecaz/i);
assert.equal(game.dukeVidal?.leader.dead, true);
// Preserve the foreign-revival denial above, then wait for Ecaz's actual
// ordered Advanced opportunity before making its authorized revival.
game = advance(game, state => state.phase === 4 && clean(state) &&
(!state.revivalOrder || state.revivalOrder.remaining[0] === f.ecaz));
const dead = structuredClone(game.dukeVidal!.leader), cash = player(game, f.ecaz).spice;
const quote = quoteEcazDukeRevival(game, f.ecaz, false);
assert.equal(quote.cost, 5); assert.ok(cash >= quote.cost, 'Original native Ecaz cash must fund its actual five-spice revival');
const revived = act(game, f.ecaz, { type: 'reviveLeader', leader: f.dukeId });
assert.equal(player(revived, f.ecaz).spice, cash - quote.cost);
assert.equal(revived.dukeVidal?.leader.id, dead.id); assert.equal(revived.dukeVidal?.leader.deaths, dead.deaths);
assert.equal(revived.dukeVidal?.leader.dead, false); assert.equal(revived.dukeVidal?.controller, null);
assert.equal(revived.dukeVidal?.source, null); assert.equal(revived.dukeVidal?.acquiredTurn, null);
assert.equal(player(revived, f.ecaz).leaders.some(leader => leader.id === f.dukeId), false);
const renewed = viewGame(revived, f.occupier).homeworldEcazDuke; assert.ok(renewed);
assert.equal(renewed.blocked, null);
assert.deepEqual(renewed.recipients.map(recipient => recipient.id).sort(), [f.occupier, f.ally].sort());
assert.deepEqual(inventory(revived), f.originalInventory);
});