import assert from 'node:assert/strict'; import test from 'node:test'; import { newPlayer, viewGame, type Action, type Game } from '../game/engine'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { createTruthMovementChoamFixture } from './fixture-truth-movement-choam'; import { actFutureBattleLeader as act, advanceFutureBattleLeader as advance, futureBattleLeaderClean as clean, futureBattleLeaderRecords as records, } from './fixture-truth-future-battle-leader'; import { createTruthtranceRichesePromisesFixture, passTruthRicheseFreshDiscard, finishTruthtranceRichesePromises, truthtranceRichesePromisesInventory, } from './fixture-truthtrance-richese-promises'; function ask(game: Game, asker: string, card: string, target: string, leader: string): Game { assert.ok(viewGame(game, asker).truthFutureBattleLeaderTargets.includes(target)); game = act(game, asker, { type: 'card', card }); for (let count = 0; game.truthtrance?.stage === 'priority' && count < game.players.length; count++) { const player = game.players.find(seat => !game.truthtrance!.passed.includes(seat.id)); assert.ok(player); game = act(game, player.id, { type: 'truthPass' }); } assert.equal(game.truthtrance?.stage, 'ask'); return act(game, asker, { type: 'truthAsk', question: { kind: 'futureBattleLeader', target, turn: game.turn, leader, } } as unknown as Action); } for (const answer of ['yes', 'no'] as const) { void test(`Basic CHOAM ${answer} binds its first real battle and constrains its native leader`, () => { const f = createTruthMovementChoamFixture(); const leader = f.beforeShipment.players.find(player => player.id === f.target)!.leaders[0].id; const other = f.beforeShipment.players.find(player => player.id === f.target)!.leaders[1].id; let game = ask(f.beforeShipment, f.asker, f.truth, f.target, leader); assert.ok(viewGame(game, f.target).truthFutureBattleLeaderAnswers?.includes(answer)); game = act(game, f.target, { type: 'truthAnswer', answer }); assert.equal(records(game)[0].status, 'waiting'); game = act(game, f.target, { type: 'ship', amount: 1, territory: 'arrakeen', sector: 10 }); game = advance(game, clean); game = act(game, f.target, { type: 'endMovement' }); game = advance(game, state => clean(state) && state.phase === 5 && state.active === f.asker); game = act(game, f.asker, { type: 'ship', amount: 1, territory: 'arrakeen', sector: 10 }); game = advance(game, clean); game = act(game, f.asker, { type: 'endMovement' }); game = advance(game, state => clean(state) && state.phase === 6 && !!state.active && !state.battle); const choice = viewGame(game, game.active!).battleChoices.find(row => row.territory === 'arrakeen'); assert.ok(choice); game = act(game, choice.chooser, { type: 'chooseBattle', territory: choice.territory, target: choice.attacker === choice.chooser ? choice.defender : choice.attacker }); game = advance(game, state => !!state.battle && clean(state) && !state.battle.preparation && state.battle.preLeader?.closed !== false); assert.equal(records(game)[0].status, 'bound'); assert.equal(records(game)[0].battle, game.battle!.event); const plan = (id: string): Action => ({ type: 'battlePlan', leader: id, dial: 0, support: 0, weapon: null, defense: null }); const invalid = plan(answer === 'yes' ? other : leader); const before = structuredClone(game); assert.throws(() => act(game, f.target, invalid)); assert.deepEqual(game, before); game = act(game, f.target, plan(answer === 'yes' ? leader : other)); const emperorLeader = game.players.find(player => player.id === f.asker)!.leaders[0].id; game = act(game, f.asker, plan(emperorLeader)); assert.equal(records(game)[0].status, 'settled'); }); } void test('Basic Richese answer survives physical Truthtrance discard and Semuta continuation, then expires without a battle', () => { const f = createTruthtranceRichesePromisesFixture(); const leader = f.game.players.find(player => player.id === f.target)!.leaders[0].id; let game = ask(f.game, f.asker, f.truth, f.target, leader); assert.ok(viewGame(game, f.target).truthFutureBattleLeaderAnswers?.includes('yes')); for (const difficulty of DIFFICULTIES) { const copy = structuredClone(game); copy.players.find(player => player.id === f.target)!.bot = difficulty; const reply = botActions(viewGame(copy, f.target)).find(action => action.type === 'truthAnswer'); assert.ok(reply, difficulty); const answered = act(copy, f.target, reply); assert.equal(records(answered)[0].status, 'waiting'); } game = act(game, f.target, { type: 'truthAnswer', answer: 'yes' }); assert.equal(records(game)[0].status, 'waiting'); assert.equal(game.pendingTreacheryDiscard?.reaction?.stage, 'offer'); game = passTruthRicheseFreshDiscard(game); assert.equal(records(game)[0].status, 'waiting'); assert.equal(game.discard.filter(card => card.id === f.truth).length, 1); game = finishTruthtranceRichesePromises(game); assert.equal(game.battle, null); assert.equal(records(game)[0].status, 'expired'); assert.deepEqual(truthtranceRichesePromisesInventory(game), f.originalCards); }); void test('Advanced Ecaz participation guards the future-battle question in the view and parser', () => { const f = createTruthMovementChoamFixture(); // Controlled guard probe: this extra seat is staged on a genuine quiet // CHOAM source; it does not claim to be an Ecaz setup or played journey. let game = structuredClone(f.beforeShipment); game.advanced = true; game.expansions.push('ecaz'); game.players.push(newPlayer('ecaz-guard', 'Ecaz', 'ecaz')); assert.deepEqual(viewGame(game, f.asker).truthFutureBattleLeaderTargets, []); const leader = game.players.find(player => player.id === f.target)!.leaders[0].id; game = act(game, f.asker, { type: 'card', card: f.truth }); for (let count = 0; game.truthtrance?.stage === 'priority' && count < game.players.length; count++) { const player = game.players.find(seat => !game.truthtrance!.passed.includes(seat.id)); assert.ok(player); game = act(game, player.id, { type: 'truthPass' }); } assert.equal(game.truthtrance?.stage, 'ask'); const before = structuredClone(game); assert.throws(() => act(game, f.asker, { type: 'truthAsk', question: { kind: 'futureBattleLeader', target: f.target, turn: game.turn, leader, } } as unknown as Action)); assert.deepEqual(game, before); });