import assert from 'node:assert/strict'; import test from 'node:test'; import { viewGame, type Action, type Game } from '../game/engine'; import { windowCardOpportunity } from '../game/window-card-play'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { actTruthRevival as act, createTruthRevivalFixture, declareTruthRevival, settleTruthRevival as settle, truthRevivalInventory as inventory, truthRevivalPlayer as player } from './fixture-truth-revival'; /** Native Advanced Revival after Emperor shipped and lost three forces in the * original Storm; physical Truthtrance and Karama come from the real deal. */ function fixture() { const f = createTruthRevivalFixture({ advanced: true, targetKarama: true }); assert.ok(f.auctionKarama); const source = JSON.stringify(['karamaEmperorSpecial', f.game.turn, f.target]); assert.equal(windowCardOpportunity(f.game, 'karamaEmperorSpecial')?.source, source); return { ...f, source, karamaCard: f.auctionKarama }; } function question(f: ReturnType, target = f.target) { return { kind: 'turnActions', target, turn: f.game.turn, mixed: { kind: 'action', action: { kind: 'windowCardPlay', effect: 'karamaEmperorSpecial', source: f.source, } } }; } function ask(f: ReturnType, state = f.game) { const declared = declareTruthRevival(f, state); return act(declared.game, f.asker, { type: 'truthAsk', question: question(f) }); } function reject(game: Game, actor: string, action: Action) { const before = structuredClone(game); assert.throws(() => act(game, actor, action)); assert.deepEqual(game, before); } void test('Advanced Emperor special Karama Yes uses a physical card for a real free force return', () => { const f = fixture(), asked = ask(f); assert.ok(viewGame(asked, f.target).truthTurnActionAnswers?.includes('yes')); assert.equal(viewGame(asked, f.opponent).truthTurnActionAnswers, null); let game = act(asked, f.target, { type: 'truthAnswer', answer: 'yes' }); const guide = viewGame(game, f.target).turnActionGuide; assert.ok(guide); assert.equal(guide.action.type, 'card'); assert.equal(guide.action.mode, 'special'); assert.equal(guide.action.card, f.karamaCard); for (const difficulty of DIFFICULTIES) { const bot = viewGame(game, f.target); bot.players.find(seat => seat.id === f.target)!.bot = difficulty; assert.deepEqual(botActions(bot)[0], guide.action); } const before = structuredClone(game), priorTanks = player(game, f.target).tanks; const priorReserves = player(game, f.target).reserves, priorSpice = player(game, f.target).spice; // A voluntary ordinary return using every tank counter cannot eliminate the // only native special-return witness for the accepted Yes. reject(game, f.target, { type: 'revive', amount: 3, elite: 1 }); game = act(game, f.target, guide.action); assert.deepEqual(before.turnActionPromises?.[0].windowCards?.karamaEmperorSpecial?.played, false); assert.equal(player(game, f.target).specialKaramaUsed, true); assert.equal(game.turnActionPromises!.find(row => row.player === f.target)!.windowCards!.karamaEmperorSpecial!.played, true); assert.equal(game.turnActionPromises!.find(row => row.player === f.target)!.fulfilled, true); const returned = priorTanks - player(game, f.target).tanks; assert.ok(returned >= 1 && returned <= 3); assert.equal(player(game, f.target).reserves, priorReserves + returned); assert.equal(player(game, f.target).spice, priorSpice); assert.equal(game.discard.filter(card => card.id === f.karamaCard).length, 1); assert.equal(viewGame(game, f.asker).players.find(seat => seat.id === f.target)!.hand, undefined); assert.deepEqual(viewGame(game, f.asker).turnActionPromises, []); game = settle(game); assert.deepEqual(inventory(game), f.originalCards); }); void test('Advanced Emperor special Karama No blocks special use without spending the card', () => { const f = fixture(), asked = ask(f); assert.ok(viewGame(asked, f.target).truthTurnActionAnswers?.includes('no')); const game = act(asked, f.target, { type: 'truthAnswer', answer: 'no' }); reject(game, f.target, { type: 'card', card: f.karamaCard, mode: 'special', amount: 1, elite: 0 }); assert.ok(player(game, f.target).hand.some(card => card.id === f.karamaCard)); assert.notEqual(player(game, f.target).specialKaramaUsed, true); assert.deepEqual(inventory(game), f.originalCards); }); void test('Advanced Emperor can use special Karama to revive a dead leader after Yes', () => { const f = fixture(), staged = structuredClone(f.game), emperor = player(staged, f.target); // This separate branch stages one existing Emperor leader's death, without // creating a leader or changing any card or force custody. const leader = emperor.leaders[0]; assert.ok(leader && !leader.dead); leader.dead = true; leader.deaths = 1; delete leader.usedAt; const asked = ask(f, staged); assert.ok(viewGame(asked, f.target).truthTurnActionAnswers?.includes('yes')); let game = act(asked, f.target, { type: 'truthAnswer', answer: 'yes' }); game = act(game, f.target, { type: 'card', card: f.karamaCard, mode: 'special', leader: leader.id }); assert.equal(player(game, f.target).leaders.find(row => row.id === leader.id)!.dead, false); assert.equal(game.turnActionPromises!.find(row => row.player === f.target)!.windowCards!.karamaEmperorSpecial!.played, true); assert.equal(game.turnActionPromises!.find(row => row.player === f.target)!.fulfilled, true); game = settle(game); assert.deepEqual(inventory(game), f.originalCards); }); void test('the Emperor special source rejects other respondents and forged turns', () => { const f = fixture(), game = declareTruthRevival(f).game; reject(game, f.asker, { type: 'truthAsk', question: question(f, f.opponent) }); const forged = question(f); forged.mixed.action.source = JSON.stringify(['karamaEmperorSpecial', game.turn + 1, f.target]); reject(game, f.asker, { type: 'truthAsk', question: forged }); }); void test('Emperor special guide uses its own promise event after an earlier unrelated whole-turn answer', () => { const f = fixture(); // Both original Truthtrances were dealt to the asker by the native fixture. let game = declareTruthRevival(f, f.game, { copies: 2 }).game; game = act(game, f.asker, { type: 'truthAsk', question: { kind: 'turnActions', target: f.target, turn: game.turn, mixed: { kind: 'action', action: { kind: 'cardPlay', effect: 'hajr' } }, } }); assert.ok(viewGame(game, f.target).truthTurnActionAnswers?.includes('no')); game = act(game, f.target, { type: 'truthAnswer', answer: 'no' }); assert.equal(game.truthtrance?.stage, 'ask'); game = act(game, f.asker, { type: 'truthAsk', question: question(f) }); assert.ok(viewGame(game, f.target).truthTurnActionAnswers?.includes('yes')); game = act(game, f.target, { type: 'truthAnswer', answer: 'yes' }); const rows = game.turnActionPromises!.filter(row => row.player === f.target && !row.fulfilled && !row.released); assert.equal(rows.length, 2); const special = rows.find(row => row.windowCards?.karamaEmperorSpecial); assert.ok(special); const guide = viewGame(game, f.target).turnActionGuide; assert.ok(guide); assert.equal(guide.event, special.event); assert.equal(guide.action.turnActionPromiseEvent, special.event); assert.equal(guide.action.mode, 'special'); game = act(game, f.target, guide.action); assert.ok(game.turnActionPromises!.find(row => row.event === special.event)?.fulfilled); assert.deepEqual(inventory(settle(game)), f.originalCards); });