import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, viewGame, type Action, type Game } from '../game/engine'; import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions'; import { advanceTruthTurnPower, createTruthTurnPowerFixture, truthTurnPowerClean } from './fixture-truth-turn-power'; const act = (game: Game, player: string, action: Action) => applyAction(game, player, action); const seat = (game: Game, id: string) => { const player = game.players.find(row => row.id === id); assert.ok(player); return player; }; const notices = (game: Game) => game.log.filter(entry => entry.text === 'Atreides has no available leader or Cheap Hero for this battle.'); const nullPlan: Action = {type:'battlePlan', leader:null, dial:0, support:0, weapon:null, defense:null}; let fixture: ReturnType | undefined; const source = () => fixture ??= createTruthTurnPowerFixture({power:'voice', remote:false}); /** A real public Basic game: BG has shipped into Atreides' Arrakeen. Stage * only Atreides' alive leader-use history and custody of one physical Cheap * Hero before making the real battle choice and native Voice decision. */ function beforeVoice() { const f = source(); let game = advanceTruthTurnPower(structuredClone(f.shipment.after), state => state.phase === 6 && !!state.active && !state.battle && truthTurnPowerClean(state), [], true); const target = seat(game, 'atreides'); assert.ok(target.leaders.every(leader => !leader.dead && !leader.capturedBy)); for (const leader of target.leaders) leader.usedAt = 'carthag'; const heroIndex = game.deck.findIndex(card => card.kind === 'hero'); assert.ok(heroIndex >= 0); const hero = game.deck.splice(heroIndex, 1)[0]; target.hand.push(hero); const chooser = game.active!; const choice = viewGame(game, chooser).battleChoices.find(row => row.territory === 'arrakeen' && [row.attacker,row.defender].includes('atreides') && [row.attacker,row.defender].includes('beneGesserit')); assert.ok(choice); game = act(game, chooser, {type:'chooseBattle', territory:choice.territory, target:choice.attacker === chooser ? choice.defender : choice.attacker}); assert.equal(game.battle?.basicEarlyLeaderlessVersion, 1); assert.equal(game.battle?.preparation?.kind, 'voice'); assert.equal(game.battle.preparation.owner, 'beneGesserit'); assert.equal(viewGame(game, 'emperor').battle!.earlyLeaderless, undefined); assert.equal(notices(game).length, 0); return {game, hero:hero.id, karama:f.karama}; } function finishPreparation(game: Game) { for (let count = 0; count < 20 && (game.response || game.battle?.preparation); count++) { const step = nextStrongholdFactionsNativeStep(game); assert.ok(step); game = act(game, step.actor, step.action); } assert.equal(game.response, null); assert.equal(game.battle?.preparation ?? null, null); return game; } void test('Basic Voice forbidding a held Cheap Hero announces leaderlessness only after preparation', () => { let {game} = beforeVoice(); game = act(game, 'beneGesserit', {type:'voice', kind:'hero', must:false}); if (game.response || game.battle?.preparation) { assert.equal(viewGame(game, 'emperor').battle!.earlyLeaderless, undefined); assert.equal(notices(game).length, 0); } game = finishPreparation(game); assert.equal(game.battle!.voice?.target, 'atreides'); assert.deepEqual(game.battle!.plans, {}); for (const viewer of ['atreides','beneGesserit','emperor']) assert.deepEqual(viewGame(game, viewer).battle!.earlyLeaderless, ['atreides']); assert.equal(notices(game).length, 1); game = act(game, 'atreides', nullPlan); assert.equal(game.battle!.plans.atreides.leader, null); assert.equal(notices(game).length, 1); }); void test('Basic Karama cancellation of Voice leaves the Cheap Hero legal and no early notice', () => { const {hero, karama, game: initial} = beforeVoice(); let game = initial; assert.ok(seat(game, 'emperor').hand.some(card => card.id === karama)); game = act(game, 'beneGesserit', {type:'voice', kind:'hero', must:false}); assert.equal(game.response?.kind, 'voice'); game = act(game, 'emperor', {type:'card', card:karama, mode:'cancel'}); game = finishPreparation(game); assert.equal(game.battle!.voice, undefined); assert.equal(viewGame(game, 'emperor').battle!.earlyLeaderless, undefined); assert.equal(notices(game).length, 0); game = act(game, 'atreides', {...nullPlan, leader:hero}); assert.equal(game.battle!.plans.atreides.leader, hero); assert.equal(notices(game).length, 0); });