Files
Dune/tests/semuta-nexus-emperor-revival.test.ts
T
Quintine 5b66628399
Verify and publish checkpoint container / container (push) Successful in 10m35s
Enable Basic Richese Semuta in Nexus games
2026-10-30 18:18:45 +11:00

100 lines
6.3 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, createGame, initializeSemutaGameForAudit, joinGame, newPlayer, viewGame, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { emperorNexusPools } from '../game/nexus-emperor-secret-ally';
import { forceRevivalRemaining, freeRevivalRemaining } from '../game/revival';
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
import { advanceSemutaNexusTrade, semutaNexusTradeInventory, swapSemutaNexusTradeIdentity } from './fixture-semuta-nexus-trade';
const player = (g: Game, id: string) => { const p = g.players.find(seat => seat.id === id); assert.ok(p); return p; };
const quiet = (g: Game) => !g.phaseOpening && !g.response && !g.decision && !g.pendingRevival;
function front(g: Game, kinds: ('land' | 'worm')[]): void {
for (const [position, kind] of kinds.entries()) {
const index = g.spiceDeck.findIndex((card, i) => i >= position && (kind === 'land' ? 'territory' in card : 'worm' in card));
assert.ok(index >= position);
g.spiceDeck.splice(position, 0, g.spiceDeck.splice(index, 1)[0]);
}
}
/** Genuine Semuta/Nexus setup and an earned closing Nexus draw. Only original
* unopened Spice order, conserved board placement, Nexus identity and Tanks
* counters are staged; no phase, card receipt or money is invented. */
function fixture(native: 'classic' | 'choam' | 'tleilaxu') {
const tleilaxu = native === 'tleilaxu';
let g = createGame('SEMUTANEXUSEMPEROR', newPlayer('r', 'Richese', 'richese'), false,
tleilaxu ? ['choam', 'ix'] : ['choam']);
joinGame(g, newPlayer('a', 'Atreides', 'atreides'));
joinGame(g, newPlayer('g', 'Guild', 'guild'));
joinGame(g, newPlayer('p', native, native === 'classic' ? 'harkonnen' : native));
g.nexusCards = {cards:null, phase:null};
for (const p of g.players) g = applyAction(g, p.id, {type:'ready'});
g = initializeSemutaGameForAudit(g);
g = advanceSemutaNexusTrade(g, state => state.status === 'playing');
assert.equal(g.semutaPreview, true);
assert.ok(Object.values(g.nexusCards!.cards!.hands).every(card => card === null));
const cards = semutaNexusTradeInventory(g);
g = advanceSemutaNexusTrade(g, state => state.phase === 1 && quiet(state));
front(g, ['land', 'land']);
g = advanceSemutaNexusTrade(g, state => state.phase === 5 && quiet(state));
for (const p of g.players) {
p.reserves += Object.values(p.forces).reduce((sum, n) => sum + n, 0); p.forces = {};
if (p.elites) { p.elites.reserves += Object.values(p.elites.forces).reduce((sum, n) => sum + n, 0); p.elites.forces = {}; }
}
g = advanceSemutaNexusTrade(g, state => state.turn === 2 && state.phase === 1 && quiet(state));
front(g, ['worm', 'land', 'land']);
g = advanceSemutaNexusTrade(g, state => state.nexus && !state.spiceWindow && quiet(state));
g = applyAction(g, 'p', {type:'alliance', target:'g'});
g = applyAction(g, 'g', {type:'alliance', target:'p'});
g = advanceSemutaNexusTrade(g, state => state.nexusCards?.phase?.stage === 'drawing');
assert.ok(g.nexusCards!.phase!.eligible.includes('a'));
g = applyAction(g, 'a', {type:'nexusCardChoice', turn:g.turn, card:null, choice:'draw', ownRedraws:0});
assert.ok(g.nexusCards!.cards!.hands.a);
swapSemutaNexusTradeIdentity(g, 'a', 'emperor');
g = advanceSemutaNexusTrade(g, state => state.phase === 4 && quiet(state));
const owner = player(g, 'a'); assert.ok(owner.reserves >= 6); owner.reserves -= 6; owner.tanks += 6;
assert.deepEqual(semutaNexusTradeInventory(g), cards);
return {game:g, cards};
}
for (const native of ['classic', 'choam', 'tleilaxu'] as const) void test(`Basic Semuta/Nexus preserves Emperor's extra revival with ${native}`, () => {
const tleilaxu = native === 'tleilaxu';
const f = fixture(native), g = f.game, owner = player(g, 'a');
const before = emperorNexusPools(owner), quota = forceRevivalRemaining(g, owner), free = freeRevivalRemaining(g, owner);
const semutaHeld = g.players.flatMap(p => p.hand).some(card => card.id === SEMUTA_DRUG_ID);
const offer = viewGame(g, 'a').nexusEmperorSecretAlly; assert.ok(offer);
assert.equal(offer.revival.blocked, null); assert.equal(offer.revival.amount, 3);
for (const difficulty of DIFFICULTIES) {
const view = viewGame(g, 'a'); view.players.find(p => p.id === 'a')!.bot = difficulty;
const action = botActions(view).find(candidate => candidate.type === 'nexusEmperorRevive');
assert.ok(action, `${difficulty} must retain a legal Emperor revival`);
assert.doesNotThrow(() => applyAction(g, 'a', action));
}
const otherSpice = tleilaxu ? player(g, 'p').spice : 0;
let result = applyAction(g, 'a', {type:'nexusEmperorRevive', event:offer.event, elite:0});
// A held cancellation card can keep native Tleilaxu income pending; finish
// that original response before checking the credit, without replaying the
// already-completed Nexus return.
if (result.response) {
assert.equal(result.response.kind, 'revivalIncome');
assert.equal(player(result, 'p').spice, otherSpice);
result = advanceSemutaNexusTrade(result, state => quiet(state));
}
assert.deepEqual(emperorNexusPools(player(result, 'a')), {...before, reserves:before.reserves + 3, tanks:before.tanks - 3});
assert.equal(forceRevivalRemaining(result, player(result, 'a')), quota);
assert.equal(freeRevivalRemaining(result, player(result, 'a')), free);
assert.equal(result.nexusCards!.cards!.hands.a, null);
assert.equal(result.nexusCards!.cards!.discard.filter(card => card === 'emperor').length, 1);
assert.equal(result.nexusEmperorSecretHistory?.[0]?.stage, 'returned');
assert.equal(result.pendingRevival, null);
assert.equal(result.pendingTreacheryDiscard ?? null, null);
assert.equal(result.semutaPreview, true);
assert.equal(result.players.flatMap(p => p.hand).some(card => card.id === SEMUTA_DRUG_ID), semutaHeld);
if (tleilaxu) assert.equal(player(result, 'p').spice, otherSpice + 1);
assert.deepEqual(semutaNexusTradeInventory(result), f.cards);
assert.throws(() => applyAction(result, 'a', {type:'nexusEmperorRevive', event:offer.event, elite:0}));
const continued = advanceSemutaNexusTrade(result, state => state.phase === 5 && quiet(state));
assert.equal(continued.nexusEmperorSecretHistory?.[0]?.stage, 'returned');
assert.deepEqual(semutaNexusTradeInventory(continued), f.cards);
});