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); });