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 { actFutureBattleLeader as act, advanceFutureBattleLeader as advance, answerFutureBattleLeader as answer, createFutureBattleLeaderFixture as fixture, declareFutureBattleLeader as declare, futureBattleLeaderClean as clean, futureBattleLeaderInventory as inventory, futureBattleLeaderRecords as records, reachFutureBattle as reachBattle, } from './fixture-truth-future-battle-leader'; function rejected(game: Game, actor: string, action: Action) { const before = structuredClone(game); assert.throws(() => act(game, actor, action)); assert.deepEqual(game, before); } for (const expected of ['yes', 'no'] as const) { void test(`Basic next battle leader ${expected} binds the first real battle and constrains its native plan`, () => { const f = fixture(); const asked = declare(f); assert.equal(asked.truthtrance?.stage, 'answer'); assert.deepEqual(asked.truthtrance?.question, f.question); let game = answer(asked, f, expected); assert.equal(game.truthtrance, null); assert.equal(game.discard.filter(card => card.id === f.truth).length, 1); assert.equal(records(game).length, 1); assert.equal(records(game)[0].status, 'waiting'); assert.equal(records(game)[0].player, f.target); assert.equal(records(game)[0].leader, f.leader); assert.equal(records(game)[0].answer, expected === 'yes'); rejected(game, f.target, { type: 'truthAnswer', answer: expected }); assert.equal(records(game).length, 1); game = reachBattle(game, f); assert.equal(records(game)[0].status, 'bound'); assert.equal(records(game)[0].battle, game.battle!.event); const yesPlan = { ...f.targetPlan, leader: f.leader } as Action; const noPlan = { ...f.targetPlan, leader: f.otherLeader } as Action; rejected(game, f.target, expected === 'yes' ? noPlan : yesPlan); game = act(game, f.target, expected === 'yes' ? yesPlan : noPlan); assert.equal(records(game)[0].status, 'bound'); game = act(game, f.asker, f.ownPlan); assert.ok(game.battle?.revealed); assert.equal(records(game)[0].status, 'settled'); assert.equal(records(game).length, 1); assert.equal(game.discard.filter(card => card.id === f.truth).length, 1); assert.deepEqual(inventory(game), f.originalCards); }); } void test('Basic next battle leader question expires without creating a battle or forcing a shipment', () => { const f = fixture(); for (const expected of ['yes', 'no'] as const) { let game = answer(declare(f), f, expected); const before = structuredClone(game.players.find(player => player.id === f.target)); game = act(game, f.target, { type: 'endMovement' }); game = advance(game, state => state.turn !== f.game.turn || state.phase > 6 || state.status === 'finished'); assert.equal(game.battle, null); assert.equal(records(game).length, 1); assert.equal(records(game)[0].status, 'expired'); const after = game.players.find(player => player.id === f.target); assert.ok(before && after); assert.equal(after.reserves, before.reserves); assert.deepEqual(after.forces, before.forces); assert.equal(game.discard.filter(card => card.id === f.truth).length, 1); assert.deepEqual(inventory(game), f.originalCards); assert.ok(clean(game)); } }); void test('future battle questions use a current turn, another seat and that faction’s native public leader', () => { const 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 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 invalid = [ { ...f.question, turn: f.game.turn + 1 }, { ...f.question, target: f.asker }, { ...f.question, leader: f.base.leaders.atreides }, { ...f.question, leader: 'unknown-leader' }, ]; for (const question of invalid) rejected(game, f.asker, { type: 'truthAsk', question } as unknown as Action); const asked = act(game, f.asker, f.ask); rejected(asked, f.asker, { type: 'truthAnswer', answer: 'yes' }); }); void test('each AI profile chooses a legal conditional answer and honors it in the real battle', () => { for (const difficulty of DIFFICULTIES) { const f = fixture(); const asked = declare(f); const respondent = asked.players.find(player => player.id === f.target)!; respondent.bot = difficulty; const reply = botActions(viewGame(asked, f.target)).find(action => action.type === 'truthAnswer'); assert.ok(reply, difficulty); let game = act(asked, f.target, reply); assert.equal(records(game)[0].status, 'waiting'); game = reachBattle(game, f); const planned = botActions(viewGame(game, f.target)).find(action => action.type === 'battlePlan'); assert.ok(planned, difficulty); game = act(game, f.target, planned); game = act(game, f.asker, f.ownPlan); assert.equal(records(game)[0].status, 'settled'); } });