import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, viewGame, type Action, type Game } from '../game/engine'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { advanceNativeLeaderless, basicNativeLeaderlessFixture, nativeLeaderlessClean, nativeLeaderlessPlayer, type NativeLeaderlessFaction } from './fixture-basic-native-early-leaderless'; const act = (game: Game, player: string, action: Action) => applyAction(game, player, action); const noLeader: Action = {type:'battlePlan', leader:null, dial:0, support:0, weapon:null, defense:null}; const announcements = (game: Game, player: string) => game.log.filter(entry => entry.text === `${nativeLeaderlessPlayer(game, player).name} has no available leader or Cheap Hero for this battle.`); function atPlanning(faction: NativeLeaderlessFaction, options: {dead?: boolean; hero?: boolean; ghola?: boolean} = {}) { const fixture = basicNativeLeaderlessFixture(faction, options); const game = advanceNativeLeaderless(fixture.game, state => !!state.battle && nativeLeaderlessClean(state) && !state.battle.preparation && (!state.battle.preLeader || state.battle.preLeader.closed)); assert.ok(game.battle && !game.battle.revealed); return {...fixture, game}; } function opponentPlan(game: Game, opponent: string, territory: string): Action { const leader = nativeLeaderlessPlayer(game, opponent).leaders.find(candidate => !candidate.dead && !candidate.capturedBy && (!candidate.usedAt || candidate.usedAt === territory)); assert.ok(leader); return {type:'battlePlan', leader:leader.id, dial:0, support:0, weapon:null, defense:null}; } for (const faction of ['ixians','tleilaxu','choam','richese','ecaz','moritani'] as const) for (const first of ['native','opponent'] as const) void test( `Basic ${faction} announces no eligible leader before ${first} seals a real public battle`, () => { const {game: initial, opponent, territory} = atPlanning(faction); let game = initial; assert.deepEqual(game.battle!.plans, {}); assert.ok(game.battle!.basicEarlyLeaderless?.includes(faction)); assert.equal(announcements(game, faction).length, 1); assert.ok(viewGame(game, opponent).battle!.earlyLeaderless?.includes(faction)); const other = opponentPlan(game, opponent, territory); game = act(game, first === 'native' ? faction : opponent, first === 'native' ? noLeader : other); assert.equal(game.battle!.revealed, false); assert.equal(announcements(game, faction).length, 1); game = act(game, first === 'native' ? opponent : faction, first === 'native' ? other : noLeader); assert.equal(game.battle!.revealed, true); assert.ok(viewGame(game, opponent).battle!.leaderless?.includes(faction)); assert.equal(announcements(game, faction).length, 1); }); void test('Basic native Cheap Hero prevents early leaderless announcement and remains a legal plan', () => { const {game: initial, faction, opponent} = atPlanning('richese', {hero:true}); let game = initial; const hero = nativeLeaderlessPlayer(game, faction).hand.find(card => card.kind === 'hero'); assert.ok(hero); assert.equal(announcements(game, faction).length, 0); assert.ok(!(viewGame(game, opponent).battle!.earlyLeaderless ?? []).includes(faction)); game = act(game, faction, {...noLeader, leader:hero.id}); assert.equal(game.battle!.plans[faction].leader, hero.id); }); void test('Basic native dead-leader preparation lets Ghola revive before the sealed plans', () => { const fixture = basicNativeLeaderlessFixture('moritani', {dead:true, ghola:true}); let game = advanceNativeLeaderless(fixture.game, state => !!state.battle && nativeLeaderlessClean(state) && !state.battle.preparation && !!state.battle.preLeader && !state.battle.preLeader.closed); const faction = fixture.faction, event = game.battle!.preLeader!.event; assert.equal(announcements(game, faction).length, 0); for (const difficulty of DIFFICULTIES) { const view = viewGame(game, faction); view.players.find(player => player.id === faction)!.bot = difficulty; const action = botActions(view).find(candidate => candidate.type === 'battlePreparationReady'); assert.ok(action, difficulty); assert.doesNotThrow(() => act(game, faction, action)); } game = act(game, faction, {type:'battlePreparationReady', event}); assert.equal(announcements(game, faction).length, 1); const player = nativeLeaderlessPlayer(game, faction), leader = player.leaders.find(item => item.dead); const ghola = player.hand.find(card => card.effect === 'ghola'); assert.ok(leader && ghola); game = act(game, faction, {type:'card', card:ghola.id, leader:leader.id}); assert.equal(nativeLeaderlessPlayer(game, faction).leaders.find(item => item.id === leader.id)!.dead, false); assert.ok(!(viewGame(game, fixture.opponent).battle!.earlyLeaderless ?? []).includes(faction)); assert.equal(announcements(game, faction).length, 1); game = act(game, faction, {...noLeader, leader:leader.id}); game = act(game, fixture.opponent, opponentPlan(game, fixture.opponent, fixture.territory)); assert.equal(game.battle!.revealed, true); });