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 { createAtreidesFullPlanWindowFixture, actAtreidesFullPlanWindow as act, atreidesFullPlanWindowPlayer as player, atreidesFullPlanWindowInventory as inventory, finishAtreidesFullPlanWindow as finish } from './fixture-atreides-full-plan-window'; /** The underlying battle and Karama are earned by native actions. Stage one * original Truthtrance into Guild's hand by exchanging it with an undealt card; * this does not claim the question card was earned through play. */ function fixture() { const f = createAtreidesFullPlanWindowFixture(), game = structuredClone(f.game); const asker = player(game, f.guild); const index = game.deck.findIndex(card => card.effect === 'truthtrance'); assert.ok(index >= 0); const original = asker.hand.findIndex(card => card.effect !== 'truthtrance'); assert.ok(original >= 0); [game.deck[index], asker.hand[original]] = [asker.hand[original], game.deck[index]]; const truth = asker.hand[original].id; assert.deepEqual(inventory(game), f.originalInventory); const window = windowCardOpportunity(game, 'karamaAtreidesSpecial'); assert.ok(window); return { ...f, game, asker: asker.id, truth, source: window.source }; } function reject(game: Game, actor: string, action: Action) { const before = structuredClone(game); assert.throws(() => act(game, actor, action)); assert.deepEqual(game, before); } function declare(f: ReturnType, source = f.game) { let game = act(source, f.asker, { type: 'card', card: f.truth }); for (let i = 0; game.truthtrance?.stage === 'priority' && i < game.players.length; i++) { const actor = game.players.find(p => !game.truthtrance!.passed.includes(p.id)); assert.ok(actor); game = act(game, actor.id, { type: 'truthPass' }); } assert.equal(game.truthtrance?.stage, 'ask'); return game; } function question(f: ReturnType, target = f.owner) { return { kind: 'turnActions', target, turn: f.game.turn, mixed: { kind: 'action', action: { kind: 'windowCardPlay', effect: 'karamaAtreidesSpecial', source: f.source, } } }; } function ask(f: ReturnType, source = f.game) { return act(declare(f, source), f.asker, { type: 'truthAsk', question: question(f) }); } void test('Advanced Atreides special Yes uses the original physical card and target-first plan', () => { const f = fixture(), asked = ask(f); assert.ok(viewGame(asked, f.owner).truthTurnActionAnswers?.includes('yes')); assert.equal(viewGame(asked, f.opponent).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(p => p.id === f.owner)!.bot = difficulty; assert.deepEqual(botActions(bot)[0], guide.action); } game = act(game, f.owner, f.inspect); assert.equal(player(game, f.owner).specialKaramaUsed, true); assert.equal(game.turnActionPromises!.find(p => p.player === f.owner)!.fulfilled, true); assert.equal(game.turnActionPromises!.find(p => p.player === f.owner)!.windowCards!.karamaAtreidesSpecial!.played, true); assert.equal(game.discard.filter(card => card.id === f.karama).length, 1); assert.equal(viewGame(game, f.asker).battle?.fullPlanInsight, null); reject(game, f.owner, f.ownPlan); game = act(game, f.opponent, f.opponentPlan); assert.ok(viewGame(game, f.owner).battle?.fullPlanInsight); assert.equal(viewGame(game, f.asker).battle?.fullPlanInsight, null); game = finish(game, f); assert.deepEqual(inventory(game), f.originalInventory); }); void test('Advanced Atreides special No blocks that role but preserves the held 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.inspect); 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.originalInventory); }); void test('a sealed target stays private and does not alter the availability of the same commitment', () => { const f = fixture(); const sealed = act(f.game, f.opponent, f.opponentPlan); assert.equal(viewGame(sealed, f.asker).battle?.plans[f.opponent], undefined); const asked = ask(f, sealed); assert.ok(viewGame(asked, f.owner).truthTurnActionAnswers?.includes('yes')); let game = act(asked, f.owner, { type: 'truthAnswer', answer: 'yes' }); const before = structuredClone(game.battle!.plans); game = act(game, f.owner, f.inspect); assert.deepEqual(game.battle!.plans, before); assert.ok(viewGame(game, f.owner).battle?.fullPlanInsight); assert.equal(viewGame(game, f.asker).battle?.fullPlanInsight, null); assert.deepEqual(inventory(game), f.originalInventory); }); void test('Atreides source rejects a different respondent or changed battle event', () => { const f = fixture(), game = declare(f); reject(game, f.asker, { type: 'truthAsk', question: question(f, f.opponent) }); const forged = question(f); forged.mixed.action.source = JSON.stringify(['karamaAtreidesSpecial', game.turn, f.owner, 'other-battle', f.territory]); reject(game, f.asker, { type: 'truthAsk', question: forged }); });