Files
Dune/tests/revival-arrival-reactions-runtime.test.ts
T

180 lines
11 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Game } from '../game/engine';
import { isAdvisor } from '../game/advisors';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { homeworldGameIntegrity, homeworldTable } from '../game/homeworld-game';
import {
createRevivalArrivalReactionsFixture as fixture,
actRevivalArrivalReactions as act,
openRevivalArrivalReactions as open,
commitRevivalArrivalReactions as commit,
passRevivalArrivalReactions as pass,
finishRevivalArrivalReactions as finish,
revivalArrivalReactionsPlayer as player,
revivalArrivalReactionsInventory as inventory,
type RevivalArrivalReactionsFixture,
} from './fixture-revival-arrival-reactions';
function ownEvent(game: Game, owner: string) {
const p = player(game, owner);
return {
reserves: p.reserves, tanks: p.tanks, forces: structuredClone(p.forces), spice: p.spice,
shipped: p.shipped, moved: p.moved, revived: p.revived, freeForcesRevived: p.freeForcesRevived,
};
}
function bgOffer(f: RevivalArrivalReactionsFixture) {
const returned = open(f.game, f);
assert.equal(returned.homeworldRevivalReturn?.stage, 'choice');
assert.equal(returned.decision?.kind, 'homeworldRevivalDeployment');
assert.equal(returned.homeworldRevivalReturn?.resumeResponse?.kind, 'revivalIncome');
assert.equal(returned.homeworldRevivalReturn?.resumeResponse?.amount, 1);
assert.equal(player(returned, f.owner).tanks, 0); assert.equal(player(returned, f.owner).reserves, 20);
assert.equal(player(returned, f.owner).revived, 1);
assert.equal(player(returned, f.owner).freeForcesRevived, 1);
const placed = commit(returned, f).game;
assert.equal(placed.homeworldRevivalReturn?.event, returned.homeworldRevivalReturn!.event);
assert.equal(placed.homeworldRevivalReturn?.stage, 'arrival');
assert.equal(placed.decision?.kind, 'advisor'); assert.equal(placed.decision?.player, f.bg);
assert.equal(player(placed, f.owner).forces[f.destination], 1);
assert.equal(player(placed, f.owner).reserves, 19);
assert.equal(player(placed, f.bg).reserves, player(f.game, f.bg).reserves);
return { returned, game: placed };
}
function originalAccounting(game: Game, f: RevivalArrivalReactionsFixture) {
const before = player(f.game, f.owner), after = player(game, f.owner);
assert.equal(game.turn, 2); assert.equal(game.phase, 4);
assert.equal(after.spice, before.spice + 1, 'The original free-revival bank income settles once after arrival choices');
assert.equal(after.shipped, before.shipped); assert.equal(after.moved, before.moved);
assert.equal(after.tanks, 0); assert.equal(after.revived, 1); assert.equal(after.freeForcesRevived, 1);
assert.equal(after.reserves, 19); assert.equal(after.forces[f.destination], 1);
assert.equal(after.reserves + after.tanks + Object.values(after.forces).reduce((sum, amount) => sum + amount, 0), 20);
assert.equal(game.homeworldRevivalReturn?.stage, 'complete');
assert.deepEqual(inventory(game), f.originalInventory); homeworldGameIntegrity(game);
}
for (const advanced of [false, true]) {
const mode = advanced ? 'Advanced' : 'Basic';
void test(`${mode} genuine High Tleilax revival arrival opens BG Spiritual choice and decline finishes the original free return`, () => {
const f = fixture({ advanced });
assert.deepEqual(f.game.expansions, ['ix']); assert.ok(f.game.homeworlds);
assert.equal(f.originalInventory.length, 47); assert.equal(f.sourceActionCount, f.actions.length);
assert.equal(player(f.afterSetup, f.owner).reserves, 20);
assert.equal(player(f.afterSetup, f.owner).tanks, 0);
assert.equal(player(f.shipment.after, f.owner).forces[f.casualtyLocation], 1);
assert.equal(player(f.beforeShipment, f.owner).spice - player(f.shipment.after, f.owner).spice, 2);
assert.equal(f.beforeStormMovement.storm, advanced ? 12 : 11);
assert.equal(f.beforeStormMovement.stormPending, advanced ? 1 : 2);
assert.equal(f.storm.after.storm, 13);
assert.equal(player(f.storm.after, f.owner).tanks, 1);
assert.equal(player(f.game, f.owner).tanks, 1); assert.equal(player(f.game, f.owner).revived, 0);
assert.equal(player(f.game, f.owner).reserves, 19); assert.equal(player(f.game, f.bg).reserves, 19);
assert.ok(homeworldTable(f.game)!.filter(home => home.native === f.owner || home.native === f.bg)
.every(home => home.side === 'high'));
const { game } = bgOffer(f), before = ownEvent(game, f.owner);
const done = finish(act(game, f.bg, f.declineAction));
assert.deepEqual(ownEvent(done, f.owner), { ...before, spice: before.spice + 1 });
assert.equal(player(done, f.bg).reserves, 19); assert.equal(player(done, f.bg).forces['polar_sink:0'], 1);
assert.ok(player(done, f.holder).hand.some(card => card.id === f.karama));
originalAccounting(done, f);
});
void test(`${mode} genuine revival-triggered BG can send one or High Wallach two to Polar without a second revival or shipment fee`, () => {
const f = fixture({ advanced }), { game } = bgOffer(f);
for (const amount of [1, 2]) {
const before = ownEvent(game, f.owner), bgBefore = ownEvent(game, f.bg);
const declared = act(game, f.bg, amount === 1 ? f.sinkAction : f.highSinkAction);
assert.equal(declared.response?.kind, 'advisor'); assert.equal(declared.response?.owner, f.bg);
assert.equal(player(declared, f.bg).reserves, bgBefore.reserves);
assert.deepEqual(ownEvent(declared, f.owner), before);
const done = finish(pass(declared));
assert.deepEqual(ownEvent(done, f.owner), { ...before, spice: before.spice + 1 });
assert.equal(player(done, f.bg).reserves, bgBefore.reserves - amount);
assert.equal(player(done, f.bg).forces['polar_sink:0'], 1 + amount);
assert.equal(player(done, f.bg).spice, bgBefore.spice);
assert.equal(player(done, f.bg).shipped, bgBefore.shipped);
assert.equal(player(done, f.bg).moved, bgBefore.moved);
assert.equal(player(done, f.bg).revived, bgBefore.revived);
assert.ok(player(done, f.holder).hand.some(card => card.id === f.karama));
originalAccounting(done, f);
}
});
void test(`${mode} original Tleilaxu Karama independently cancels BG accompaniment after the revived force is already placed`, () => {
const f = fixture({ advanced }), { game } = bgOffer(f);
const before = ownEvent(game, f.owner), bgBefore = ownEvent(game, f.bg);
const declared = act(game, f.bg, f.highSinkAction);
assert.equal(declared.response?.kind, 'advisor');
const done = finish(act(declared, f.holder, f.cancelAction));
assert.deepEqual(ownEvent(done, f.owner), { ...before, spice: before.spice + 1 }); assert.deepEqual(ownEvent(done, f.bg), bgBefore);
assert.equal(done.discard.filter(card => card.id === f.karama).length, 1);
assert.equal(player(done, f.holder).hand.some(card => card.id === f.karama), false);
originalAccounting(done, f);
});
void test(`${mode} all four minimal BG policies and original Karama responders act legally on the revival arrival`, () => {
const f = fixture({ advanced }), { game } = bgOffer(f);
for (const difficulty of DIFFICULTIES) {
const own = viewGame(game, f.bg); own.players.find(seat => seat.id === f.bg)!.bot = difficulty;
const actions = botActions(own); assert.ok(actions.length, difficulty);
const declared = act(game, f.bg, actions[0]);
if (declared.response) {
const counter = viewGame(declared, f.holder);
counter.players.find(seat => seat.id === f.holder)!.bot = difficulty;
const replies = botActions(counter); assert.ok(replies.length, difficulty);
assert.doesNotThrow(() => act(declared, f.holder, replies[0]), difficulty);
}
const done = finish(pass(declared)); originalAccounting(done, f);
assert.deepEqual(inventory(done), f.originalInventory);
}
});
}
void test('Advanced genuine revived Tleilaxu arrival permits one advisor to accompany into its original Carthag destination', () => {
const f = fixture(), { game } = bgOffer(f), before = ownEvent(game, f.owner);
const done = finish(pass(act(game, f.bg, f.accompanyAction)));
assert.deepEqual(ownEvent(done, f.owner), { ...before, spice: before.spice + 1 });
assert.equal(player(done, f.bg).reserves, 18);
assert.equal(player(done, f.bg).forces[f.destination], 1);
assert.equal(isAdvisor(player(done, f.bg), f.territory), true);
assert.equal(player(done, f.bg).forces['polar_sink:0'], 1);
assert.ok(player(done, f.holder).hand.some(card => card.id === f.karama));
originalAccounting(done, f);
});
void test('Advanced actual BG resident fighters receive Intrusion before the revived arrival Spiritual followup', () => {
const f = fixture({ resident: true }); assert.ok(f.residentShipment);
assert.equal(player(f.residentShipment.before, f.bg).spice - player(f.residentShipment.after, f.bg).spice, 1);
assert.equal(player(f.game, f.bg).reserves, 18); assert.equal(isAdvisor(player(f.game, f.bg), f.territory), false);
const returned = open(f.game, f), placed = commit(returned, f).game;
assert.equal(placed.decision?.kind, 'intrusion'); assert.equal(placed.decision?.player, f.bg);
assert.equal(player(placed, f.owner).forces[f.destination], 1);
const before = ownEvent(placed, f.owner);
const converting = act(placed, f.bg, { type: 'decision', accept: true });
assert.equal(converting.response?.kind, 'advisorFlip'); assert.equal(converting.response?.advisors, true);
const offered = pass(converting);
assert.equal(isAdvisor(player(offered, f.bg), f.territory), true);
assert.equal(offered.decision?.kind, 'advisor'); assert.equal(offered.decision?.player, f.bg);
const done = finish(pass(act(offered, f.bg, f.accompanyAction)));
assert.deepEqual(ownEvent(done, f.owner), { ...before, spice: before.spice + 1 });
assert.equal(player(done, f.bg).forces[f.destination], 2);
assert.equal(player(done, f.bg).reserves, 17); assert.equal(isAdvisor(player(done, f.bg), f.territory), true);
originalAccounting(done, f);
});
void test('Advanced original Karama can prevent Intrusion conversion while the revived force and later decline remain committed', () => {
const f = fixture({ resident: true }), returned = open(f.game, f), placed = commit(returned, f).game;
assert.equal(placed.decision?.kind, 'intrusion');
const before = ownEvent(placed, f.owner), bgBefore = ownEvent(placed, f.bg);
const converting = act(placed, f.bg, { type: 'decision', accept: true });
assert.equal(converting.response?.kind, 'advisorFlip');
const offered = act(converting, f.holder, f.cancelAction);
assert.equal(offered.decision?.kind, 'advisor'); assert.equal(isAdvisor(player(offered, f.bg), f.territory), false);
const done = finish(act(offered, f.bg, f.declineAction));
assert.deepEqual(ownEvent(done, f.owner), { ...before, spice: before.spice + 1 }); assert.deepEqual(ownEvent(done, f.bg), bgBefore);
assert.equal(done.discard.filter(card => card.id === f.karama).length, 1);
originalAccounting(done, f);
});