Files
Dune/tests/revival-terror-runtime.test.ts

183 lines
12 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { homeworldGameIntegrity, homeworldTable } from '../game/homeworld-game';
import {
createRevivalTerrorFixture as fixture,
actRevivalTerror as act,
commitRevivalTerror as commit,
finishRevivalTerror as finish,
settleRevivalTerror as settle,
revivalTerrorPlayer as player,
revivalTerrorInventory as inventory,
type RevivalTerrorFixture,
} from './fixture-revival-terror';
function entered(f: RevivalTerrorFixture) {
const game = commit(f.game, f).game;
assert.equal(game.decision?.kind, 'moritaniTerror'); assert.equal(game.decision?.player, f.owner);
assert.equal(game.pendingTerrorEntry?.stage, 'offer');
assert.equal(game.pendingTerrorEntry.entrant, f.entrant); assert.equal(game.pendingTerrorEntry.token, f.token);
assert.equal(game.pendingTerrorEntry.territory, f.territory); assert.equal(game.pendingTerrorEntry.sector, f.sector);
assert.equal(game.pendingTerrorEntry.amount, 1); assert.equal(game.pendingTerrorEntry.cause, 'homeworldRevival');
assert.equal(game.homeworldRevivalReturn?.stage, 'arrival');
assert.equal(game.homeworldRevivalReturn.event, f.event);
assert.equal(player(game, f.entrant).forces[f.location], 1); assert.equal(player(game, f.entrant).reserves, 19);
assert.equal(player(game, f.entrant).spice, player(f.game, f.entrant).spice,
'Original revival income waits until the entry effects are complete');
assert.equal(viewGame(game, f.owner).terrorEntry?.canReveal, true);
return game;
}
function completed(game: Game, f: RevivalTerrorFixture, surviving = 1) {
assert.equal(game.turn, 2); assert.equal(game.phase, 4);
assert.ok(!game.pendingTerrorEntry); assert.ok(!game.pendingTreacheryDiscard);
assert.equal(game.homeworldRevivalReturn?.stage, 'complete');
assert.equal(game.decision, null); assert.equal(game.response, null);
const returned = player(game, f.entrant), source = player(f.game, f.entrant);
assert.equal(returned.reserves, 19); assert.equal(returned.forces[f.location] ?? 0, surviving);
assert.equal(returned.tanks, 1 - surviving); assert.equal(returned.revived, 1); assert.equal(returned.freeForcesRevived, 1);
assert.equal(returned.shipped, source.shipped); assert.equal(returned.moved, source.moved);
for (const p of game.players)
assert.equal(p.reserves + p.tanks + Object.values(p.forces).reduce((sum, amount) => sum + amount, 0), 20);
assert.deepEqual(inventory(game), f.originalInventory); homeworldGameIntegrity(game);
}
for (const advanced of [false, true]) {
const mode = advanced ? 'Advanced' : 'Basic';
void test(`${mode} native revived entry permits Moritani to decline its genuinely Mentat-placed Terror`, () => {
const f = fixture({ advanced });
assert.deepEqual(f.game.expansions, ['ix', 'ecaz']); assert.ok(f.game.homeworlds);
assert.equal(f.originalInventory.length, 47); assert.equal(f.sourceActionCount, f.actions.length);
assert.equal(player(f.afterSetup, f.entrant).reserves, 20);
assert.equal(player(f.beforeRequest, f.entrant).tanks, 1);
assert.equal(player(f.beforeRequest, f.entrant).reserves, 19);
assert.equal(f.beforePlacement.turn, 1); assert.equal(f.beforePlacement.phase, 8);
assert.equal(f.beforePlacement.decision?.kind, 'moritaniPlacement');
assert.deepEqual(f.placement.steps[0], { actor: f.owner, action: f.placementAction });
assert.equal(player(f.shipment.before, f.entrant).spice - player(f.shipment.after, f.entrant).spice, 2);
assert.equal(player(f.storm.after, f.entrant).tanks, 1); assert.equal(f.storm.after.storm, 13);
assert.equal(player(f.game, f.entrant).tanks, 0); assert.equal(player(f.game, f.entrant).reserves, 20);
assert.equal(homeworldTable(f.game)!.find(home => home.native === f.entrant)!.side, 'high');
assert.equal(homeworldTable(f.game)!.find(home => home.native === f.owner)!.side, 'high');
const done = finish(act(entered(f), f.owner, f.declineAction));
assert.equal(done.moritaniTerror!.tokens.find(token => token.id === f.token)!.status, 'placed');
assert.equal(player(done, f.entrant).spice, player(f.game, f.entrant).spice + 1);
assert.ok(player(done, f.holder).hand.some(card => card.id === f.karama)); completed(done, f);
});
void test(`${mode} native revival Robbery resolves either original spice theft or top-card draw before free-revival income`, () => {
const f = fixture({ advanced, kind: 'robbery' });
for (const choice of ['spice', 'card']) {
const offered = entered(f), cash = player(offered, f.entrant).spice;
const moritaniCash = player(offered, f.owner).spice, deckTop = offered.deck[0].id;
const revealed = act(offered, f.owner, f.revealAction);
assert.equal(revealed.pendingTerrorEntry?.stage, 'robbery');
assert.equal(revealed.homeworldRevivalReturn?.stage, 'arrival');
const done = finish(act(revealed, f.owner, { type: 'decision', choice }));
assert.equal(player(done, f.entrant).spice, cash - (choice === 'spice' ? Math.ceil(cash / 2) : 0) + 1);
assert.equal(player(done, f.owner).spice, moritaniCash + (choice === 'spice' ? Math.ceil(cash / 2) : 0));
assert.equal(player(done, f.owner).hand.some(card => card.id === deckTop), choice === 'card');
assert.equal(done.deck.length, offered.deck.length - Number(choice === 'card'));
assert.equal(done.moritaniTerror!.tokens.find(token => token.id === f.token)!.status, 'removed');
completed(done, f);
}
});
void test(`${mode} native revival Sabotage completes its actual discard and optional original-card gift`, () => {
const f = fixture({ advanced, kind: 'sabotage' });
for (const give of [false, true]) {
const offered = entered(f), cash = player(offered, f.entrant).spice;
const pending = settle(act(offered, f.owner, f.revealAction));
assert.equal(pending.pendingTerrorEntry?.stage, 'gift');
assert.equal(pending.homeworldRevivalReturn?.stage, 'arrival');
assert.equal(player(pending, f.entrant).spice, cash);
assert.equal(pending.discard.filter(card => card.id === f.karama).length, 1);
const done = finish(act(pending, f.owner, give
? { type: 'decision', card: f.gift } : f.declineAction));
assert.equal(player(done, f.entrant).hand.some(card => card.id === f.gift), give);
assert.equal(player(done, f.owner).hand.some(card => card.id === f.gift), !give);
assert.equal(player(done, f.entrant).spice, cash + 1);
assert.equal(done.moritaniTerror!.tokens.find(token => token.id === f.token)!.status, 'removed');
completed(done, f);
}
});
void test(`${mode} native revival Assassination uses its real living-disc lottery and original bounty`, () => {
const f = fixture({ advanced, kind: 'assassination' }), offered = entered(f);
const before = structuredClone(player(offered, f.entrant).leaders), ownerCash = player(offered, f.owner).spice;
const done = finish(act(offered, f.owner, f.revealAction));
const killed = player(done, f.entrant).leaders.filter(leader => leader.dead && !before.find(old => old.id === leader.id)!.dead);
assert.equal(killed.length, 1);
assert.equal(killed[0].deaths, before.find(leader => leader.id === killed[0].id)!.deaths + 1);
assert.equal(player(done, f.owner).spice, ownerCash + (killed[0].id === 'tleilaxu-0' ? 3 : killed[0].strength));
assert.equal(player(done, f.entrant).spice, player(offered, f.entrant).spice + 1);
assert.equal(done.moritaniTerror!.tokens.find(token => token.id === f.token)!.status, 'removed'); completed(done, f);
});
void test(`${mode} native revival Extortion reserves its original bank five without prematurely paying it`, () => {
const f = fixture({ advanced, kind: 'extortion' }), offered = entered(f), before = player(offered, f.owner).spice;
const done = finish(act(offered, f.owner, f.revealAction));
assert.equal(done.moritaniExtortion?.stage, 'reserved');
assert.deepEqual(done.moritaniExtortion?.bank, { reserved: 5, collected: 0 });
assert.equal(done.moritaniExtortion?.owner, f.owner); assert.equal(done.moritaniExtortion?.token, f.token);
assert.equal(player(done, f.owner).spice, before);
assert.equal(player(done, f.entrant).spice, player(offered, f.entrant).spice + 1);
assert.equal(done.moritaniTerror!.tokens.find(token => token.id === f.token)!.status, 'extortion'); completed(done, f);
});
void test(`${mode} native revival Sneak Attack moves an actual Moritani reserve counter without another shipment fee`, () => {
const f = fixture({ advanced, kind: 'sneakAttack' }), offered = entered(f), before = structuredClone(player(offered, f.owner));
const pending = act(offered, f.owner, f.revealAction);
assert.equal(pending.pendingTerrorEntry?.stage, 'sneakAttack');
const done = finish(act(pending, f.owner, { type: 'decision', amount: 1, sector: f.sector }));
assert.equal(player(done, f.owner).reserves, before.reserves - 1);
assert.equal(player(done, f.owner).forces[f.location], 1); assert.equal(player(done, f.owner).spice, before.spice);
assert.equal(player(done, f.owner).shipped, before.shipped); assert.equal(player(done, f.owner).moved, before.moved);
assert.equal(done.moritaniTerror!.tokens.find(token => token.id === f.token)!.status, 'removed'); completed(done, f);
});
void test(`${mode} native revival Atomics destroys the already revived entrant and finishes the original return`, () => {
const f = fixture({ advanced, kind: 'atomics' }), offered = entered(f);
const done = finish(act(offered, f.owner, f.revealAction));
assert.equal(done.moritaniAtomics?.territory, f.territory);
assert.equal(done.moritaniAtomics?.turn, 2); assert.equal(done.moritaniAtomics?.moritaniId, f.owner);
assert.equal(player(done, f.owner).atomicsHandLimitPenalty, true);
assert.equal(player(done, f.entrant).spice, player(offered, f.entrant).spice + 1);
assert.equal(done.moritaniTerror!.tokens.find(token => token.id === f.token)!.status, 'removed'); completed(done, f, 0);
});
void test(`${mode} all four minimal policies complete legal native revival Terror choices`, () => {
const f = fixture({ advanced });
for (const difficulty of DIFFICULTIES) {
let game = entered(f);
for (let count = 0; count < 100 && (game.homeworldRevivalReturn?.stage !== 'complete' || game.response || game.decision); count++) {
let next;
for (const seat of game.players) {
const own = viewGame(game, seat.id); own.players.find(player => player.id === seat.id)!.bot = difficulty;
const action = botActions(own)[0];
if (action) { next = { actor: seat.id, action }; break; }
}
assert.ok(next, `${difficulty} must retain a legal current Terror/response action`);
game = act(game, next.actor, next.action);
}
completed(game, f);
}
});
}
void test('native original Tleilaxu Karama can cancel Enemy of My Enemy while keeping the revived entry committed', () => {
const f = fixture(), offered = entered(f), pending = act(offered, f.owner, { type: 'decision', alliance: true });
assert.equal(pending.response?.kind, 'moritaniAlliance');
const prevented = act(pending, f.holder, f.cancelAction);
assert.equal(prevented.pendingTerrorEntry?.stage, 'offer'); assert.equal(prevented.decision?.player, f.owner);
assert.equal(player(prevented, f.entrant).ally, null); assert.equal(player(prevented, f.owner).ally, null);
assert.equal(player(prevented, f.entrant).forces[f.location], 1);
assert.equal(prevented.homeworldRevivalReturn?.stage, 'arrival');
const done = finish(act(prevented, f.owner, f.declineAction));
assert.equal(done.discard.filter(card => card.id === f.karama).length, 1);
assert.equal(player(done, f.entrant).spice, player(offered, f.entrant).spice + 1); completed(done, f);
});