import assert from 'node:assert/strict'; import test from 'node:test'; import { viewGame, type Action, type Game } from '../game/engine'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { actTruthLeaderRevival as act, answerTruthLeaderRevival as answer, createTruthLeaderRevivalFixture as fixture, declareTruthLeaderRevival as declare, settleTruthLeaderRevival as settle, truthLeaderRevivalInventory as inventory, truthLeaderRevivalPlayer as player, } from './fixture-truth-leader-revival'; function rejected(game: Game, actor: string, action: Action) { const before = structuredClone(game); assert.throws(() => act(game, actor, action)); assert.deepEqual(game, before); } function askGhola(f: ReturnType) { const declared = declare(f); const game = act(declared.game, f.asker, { type: 'truthAsk', question: { kind: 'gholaLeader', target: f.target, turn: declared.game.turn, leader: f.leader, } }); return game; } for (const advanced of [false, true]) { void test(`${advanced ? 'Advanced' : 'Basic'} named Ghola Yes is fulfilled by an actual Ghola leader return`, () => { const f = fixture({ advanced, ghola: true }); assert.ok(f.ghola); const asked = askGhola(f); assert.ok(viewGame(asked, f.target).truthGholaLeaderAnswers?.includes('yes')); let game = answer(asked, f.target, 'yes'); const promise = game.gholaLeaderPromises!.find(row => row.player === f.target)!; assert.equal(promise.leader, f.leader); assert.ok(!promise.fulfilled); // Spending the only dealt Ghola on a force return cannot satisfy the named leader. rejected(game, f.target, { type: 'card', card: f.ghola, amount: 1, elite: 0 }); game = settle(act(game, f.target, { type: 'card', card: f.ghola, leader: f.leader })); assert.equal(player(game, f.target).leaders.find(leader => leader.id === f.leader)!.dead, false); assert.equal(player(game, f.target).leaderRevived, false); assert.equal(game.gholaLeaderPromises!.find(row => row.event === promise.event)!.fulfilled, true); assert.equal(game.discard.filter(card => card.id === f.ghola).length, 1); assert.deepEqual(inventory(game), f.originalCards); }); } void test('named Ghola No rejects the matching leader return but allows its independent force return', () => { const f = fixture({ ghola: true }); assert.ok(f.ghola); const asked = askGhola(f); assert.ok(viewGame(asked, f.target).truthGholaLeaderAnswers?.includes('no')); let game = answer(asked, f.target, 'no'); const promise = game.gholaLeaderPromises!.find(row => row.player === f.target)!; rejected(game, f.target, { type: 'card', card: f.ghola, leader: f.leader }); game = settle(act(game, f.target, { type: 'card', card: f.ghola, amount: 1, elite: 0 })); assert.equal(player(game, f.target).leaders.find(leader => leader.id === f.leader)!.dead, true); assert.ok(!game.gholaLeaderPromises!.find(row => row.event === promise.event)!.fulfilled); assert.equal(game.discard.filter(card => card.id === f.ghola).length, 1); assert.deepEqual(inventory(game), f.originalCards); }); void test('two Truthtrances cannot bind contradictory answers about the same Ghola leader', () => { const f = fixture({ ghola: true }); let game = declare(f, f.game, { copies: 2 }).game; const question = { kind: 'gholaLeader', target: f.target, turn: game.turn, leader: f.leader }; game = act(game, f.asker, { type: 'truthAsk', question }); game = answer(game, f.target, 'yes'); game = act(game, f.asker, { type: 'truthAsk', question }); assert.ok(viewGame(game, f.target).truthGholaLeaderAnswers?.includes('yes')); assert.ok(!viewGame(game, f.target).truthGholaLeaderAnswers?.includes('no')); rejected(game, f.target, { type: 'truthAnswer', answer: 'no' }); game = answer(game, f.target, 'yes'); assert.equal(game.gholaLeaderPromises!.filter(row => row.player === f.target).length, 2); }); void test('prior ordinary-revival Yes prevents a conflicting Ghola leader Yes', () => { const f = fixture({ ghola: true }); let game = declare(f, f.game, { copies: 2 }).game; game = act(game, f.asker, { type: 'truthAsk', question: { kind: 'leaderRevival', target: f.target, turn: game.turn, leader: f.leader, } }); game = answer(game, f.target, 'yes'); game = act(game, f.asker, { type: 'truthAsk', question: { kind: 'gholaLeader', target: f.target, turn: game.turn, leader: f.leader, } }); assert.ok(!viewGame(game, f.target).truthGholaLeaderAnswers?.includes('yes')); assert.ok(viewGame(game, f.target).truthGholaLeaderAnswers?.includes('no')); rejected(game, f.target, { type: 'truthAnswer', answer: 'yes' }); game = answer(game, f.target, 'no'); assert.ok(game.leaderRevivalPromises!.some(row => row.player === f.target && row.answer)); assert.ok(game.gholaLeaderPromises!.some(row => row.player === f.target && !row.answer)); }); void test('all bot profiles choose the native named Ghola guide', () => { const f = fixture({ ghola: true }); const game = answer(askGhola(f), f.target, 'yes'); const view = viewGame(game, f.target); const guide = view.gholaLeaderPromiseGuide; assert.ok(guide); assert.equal(guide.action.type, 'card'); assert.equal(guide.action.leader, f.leader); for (const difficulty of DIFFICULTIES) { const botView = viewGame(game, f.target); botView.players.find(seat => seat.id === f.target)!.bot = difficulty; assert.deepEqual(botActions(botView)[0], guide.action); } const after = settle(act(game, f.target, guide.action)); assert.ok(after.gholaLeaderPromises!.find(row => row.player === f.target)?.fulfilled); }); for (const advanced of [false, true]) { void test(`${advanced ? 'Advanced' : 'Basic'} ordinary revival cannot consume the leader promised for Ghola`, () => { const f = fixture({ advanced, ghola: true }); const game = answer(askGhola(f), f.target, 'yes'); assert.ok(game.gholaLeaderPromises!.some(row => row.player === f.target && row.answer && !row.fulfilled)); rejected(game, f.target, f.revivalAction); assert.ok(player(game, f.target).leaders.find(leader => leader.id === f.leader)!.dead); }); }