80 lines
5.1 KiB
TypeScript
80 lines
5.1 KiB
TypeScript
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);
|
|
});
|