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 { windowCardOpportunity } from '../game/window-card-play'; import { createTleilaxuSpecialKaramaFixture, actTleilaxuSpecialKarama as act, tleilaxuSpecialKaramaPlayer as player, tleilaxuSpecialKaramaInventory as inventory, } from './fixture-tleilaxu-special-karama'; function fixture() { const f = createTleilaxuSpecialKaramaFixture(); const game = structuredClone(f.game), asker = f.observer; const hand = player(game, asker).hand; // Stage one original Truthtrance by exchanging physical cards with an // existing card zone. Neither Karama nor Ghola is displaced or duplicated. let truth = hand.find(card => card.effect === 'truthtrance'); if (!truth) { const zones = [game.deck, game.discard, player(game, f.target).hand]; const zone = zones.find(cards => cards.some(card => card.effect === 'truthtrance')); assert.ok(zone); const index = zone.findIndex(card => card.effect === 'truthtrance'); assert.ok(hand.length > 0); [hand[0], zone[index]] = [zone[index], hand[0]]; truth = hand[0]; } assert.deepEqual(inventory(game), f.inventory); const source = JSON.stringify(['karamaTleilaxuSpecial', game.turn, f.owner]); assert.equal(windowCardOpportunity(game, 'karamaTleilaxuSpecial')?.source, source); return { ...f, game, asker, truth: truth.id, windowSource: source }; } type Fixture = ReturnType; function question(f: Fixture, target = f.owner, source = f.windowSource) { return { kind: 'turnActions', target, turn: f.game.turn, mixed: { kind: 'action', action: { kind: 'windowCardPlay', effect: 'karamaTleilaxuSpecial', source, } } }; } function declare(f: Fixture) { let game = act(f.game, f.asker, { type: 'card', card: f.truth }); for (let count = 0; game.truthtrance?.stage === 'priority' && count < game.players.length; count++) { const actor = game.players.find(seat => !game.truthtrance!.passed.includes(seat.id)); assert.ok(actor); game = act(game, actor.id, { type: 'truthPass' }); } assert.equal(game.truthtrance?.stage, 'ask'); return game; } function ask(f: Fixture) { return act(declare(f), 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('Tleilaxu special Karama Yes spends the physical card and prevents a real revival', () => { const f = fixture(), asked = ask(f); assert.ok(viewGame(asked, f.owner).truthTurnActionAnswers?.includes('yes')); assert.equal(viewGame(asked, f.target).truthTurnActionAnswers, null); let game = act(asked, f.owner, { type: 'truthAnswer', answer: 'yes' }); const guide = viewGame(game, f.owner).turnActionGuide; assert.ok(guide); assert.equal(guide.action.type, 'card'); assert.equal(guide.action.mode, 'special'); assert.equal(guide.action.card, f.karama); for (const difficulty of DIFFICULTIES) { const bot = viewGame(game, f.owner); bot.players.find(seat => seat.id === f.owner)!.bot = difficulty; assert.deepEqual(botActions(bot)[0], guide.action); } game = act(game, f.owner, f.special); assert.equal(player(game, f.owner).specialKaramaUsed, true); assert.equal(player(game, f.owner).hand.some(card => card.id === f.karama), false); assert.equal(game.discard.filter(card => card.id === f.karama).length, 1); assert.deepEqual(game.revivalPrevention, { player: f.target, turn: game.turn }); const promise = game.turnActionPromises!.find(row => row.player === f.owner); assert.equal(promise?.windowCards?.karamaTleilaxuSpecial?.played, true); assert.equal(promise?.fulfilled, true); reject(game, f.target, f.ordinaryRevival); reject(game, f.target, f.gholaAction); assert.equal(viewGame(game, f.asker).players.find(seat => seat.id === f.owner)!.hand, undefined); assert.deepEqual(viewGame(game, f.asker).turnActionPromises, []); assert.deepEqual(inventory(game), f.inventory); }); void test('Tleilaxu special Karama No prevents its use and preserves the card', () => { const f = fixture(), asked = ask(f); assert.ok(viewGame(asked, f.owner).truthTurnActionAnswers?.includes('no')); const game = act(asked, f.owner, { type: 'truthAnswer', answer: 'no' }); reject(game, f.owner, f.special); reject(game, f.owner, { ...f.special, target: f.owner }); assert.ok(player(game, f.owner).hand.some(card => card.id === f.karama)); assert.notEqual(player(game, f.owner).specialKaramaUsed, true); assert.deepEqual(inventory(game), f.inventory); }); void test('Tleilaxu may fulfill Yes by preventing its own revival', () => { const f = fixture(); let game = act(ask(f), f.owner, { type: 'truthAnswer', answer: 'yes' }); game = act(game, f.owner, { ...f.special, target: f.owner }); assert.deepEqual(game.revivalPrevention, { player: f.owner, turn: game.turn }); assert.equal(game.turnActionPromises!.find(row => row.player === f.owner)?.fulfilled, true); assert.equal(game.discard.filter(card => card.id === f.karama).length, 1); assert.deepEqual(inventory(game), f.inventory); }); void test('Tleilaxu special source rejects another respondent and forged turns or owners', () => { const f = fixture(), game = declare(f); reject(game, f.asker, { type: 'truthAsk', question: question(f, f.target) }); reject(game, f.asker, { type: 'truthAsk', question: question(f, f.owner, JSON.stringify(['karamaTleilaxuSpecial', game.turn + 1, f.owner])) }); reject(game, f.asker, { type: 'truthAsk', question: question(f, f.owner, JSON.stringify(['karamaTleilaxuSpecial', game.turn, f.target])) }); });