98 lines
5.4 KiB
TypeScript
98 lines
5.4 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 { createBattleTerritoryExplosionFixture } from './fixture-battle-territory-explosion';
|
|
|
|
const act = (game: Game, id: string, action: Action) => applyAction(game, id, action);
|
|
const seat = (game: Game, id: string) => {
|
|
const player = game.players.find(row => row.id === id); assert.ok(player); return player;
|
|
};
|
|
const announce = (game: Game) => game.log.filter(entry => entry.text.includes('announced a Battle Plan without a leader or Cheap Hero'));
|
|
const leaderless: Action = {type:'battlePlan', dial:0, support:0, leader:null, weapon:null, defense:null};
|
|
|
|
/** Public three-seat Basic source plays both entries and the first battle, then
|
|
* chooses the real second contest in Imperial Basin. Only the Emperor's five
|
|
* leader-use locations are staged as previously used in another territory;
|
|
* the actual second battle, dealt hands and all plans stay native. */
|
|
function fixture() {
|
|
const source = createBattleTerritoryExplosionFixture({advanced:false});
|
|
const game = structuredClone(source.game);
|
|
assert.equal(game.status, 'playing'); assert.equal(game.advanced, false);
|
|
assert.equal(game.phase, 6); assert.ok(game.battle && !game.battle.revealed);
|
|
assert.deepEqual([game.battle.attacker, game.battle.defender].sort(), ['emperor', 'guild']);
|
|
assert.equal(seat(game, 'emperor').hand.some(card => card.kind === 'hero'), false);
|
|
for (const leader of seat(game, 'emperor').leaders) leader.usedAt = 'carthag';
|
|
return {game, source};
|
|
}
|
|
|
|
void test('Basic actual battle announces the newly sealed leaderless plan without exposing its dial or hand', () => {
|
|
const f = fixture(), initial = f.game, beforeLogs = announce(initial).length;
|
|
let game = act(initial, 'emperor', leaderless);
|
|
assert.equal(game.battle?.revealed, false);
|
|
assert.deepEqual(game.battle?.basicLeaderlessAnnouncements, ['emperor']);
|
|
assert.equal(announce(game).length, beforeLogs + 1);
|
|
for (const viewer of ['guild', 'atreides']) {
|
|
const battle = viewGame(game, viewer).battle!;
|
|
assert.deepEqual(battle.leaderless, ['emperor']);
|
|
assert.deepEqual(battle.plans, {}); assert.deepEqual(battle.cards, []);
|
|
}
|
|
assert.equal(viewGame(game, 'emperor').battle!.plans.emperor.dial, 0);
|
|
const saved = JSON.stringify(game); game = JSON.parse(saved);
|
|
assert.deepEqual(viewGame(game, 'guild').battle!.leaderless, ['emperor']);
|
|
assert.deepEqual(viewGame(game, 'atreides').battle!.plans, {});
|
|
assert.equal(JSON.stringify(game), saved);
|
|
assert.throws(() => act(game, 'emperor', leaderless));
|
|
assert.equal(JSON.stringify(game), saved);
|
|
game = act(game, 'guild', {type:'battlePlan', dial:0, support:0,
|
|
leader:f.source.guildLeader, weapon:null, defense:null});
|
|
assert.equal(game.battle?.revealed, true);
|
|
assert.equal(announce(game).length, beforeLogs + 1);
|
|
assert.deepEqual(viewGame(game, 'atreides').battle!.leaderless, ['emperor']);
|
|
assert.equal(viewGame(game, 'atreides').battle!.plans.emperor.dial, 0);
|
|
});
|
|
|
|
void test('a held Cheap Hero prevents a Basic leaderless plan and its legal use does not announce one', () => {
|
|
const {game} = fixture();
|
|
const heroIndex = game.deck.findIndex(card => card.kind === 'hero'); assert.ok(heroIndex >= 0);
|
|
const [hero] = game.deck.splice(heroIndex, 1); seat(game, 'emperor').hand.push(hero);
|
|
const before = structuredClone(game), logs = announce(game).length;
|
|
assert.throws(() => act(game, 'emperor', leaderless));
|
|
assert.deepEqual(game, before);
|
|
const planned = act(game, 'emperor', {...leaderless, leader:hero.id});
|
|
assert.equal(announce(planned).length, logs);
|
|
assert.deepEqual(viewGame(planned, 'guild').battle!.leaderless, []);
|
|
assert.deepEqual(viewGame(planned, 'guild').battle!.plans, {});
|
|
});
|
|
|
|
void test('an already-sealed Basic legacy plan receives no synthetic announcement', () => {
|
|
const {game} = fixture();
|
|
// Explicit legacy supplement: install the plan format an older save could
|
|
// contain, without the new announcement receipt or a historical log entry.
|
|
game.battle!.plans.emperor = {dial:0, support:0, leader:null, weapon:null, defense:null};
|
|
const saved = JSON.stringify(game);
|
|
assert.equal(game.battle!.basicLeaderlessAnnouncements, undefined);
|
|
assert.deepEqual(viewGame(game, 'guild').battle!.leaderless, []);
|
|
assert.deepEqual(viewGame(game, 'atreides').battle!.plans, {});
|
|
assert.equal(JSON.stringify(game), saved);
|
|
const restored: Game = JSON.parse(saved);
|
|
assert.deepEqual(viewGame(restored, 'guild').battle!.leaderless, []);
|
|
assert.equal(announce(restored).length, announce(game).length);
|
|
});
|
|
|
|
void test('all four minimal bot profiles seal a legal Basic leaderless plan and announce it', () => {
|
|
const {game} = fixture();
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(game, 'emperor');
|
|
view.players.find(player => player.id === 'emperor')!.bot = difficulty;
|
|
const actions = botActions(view).filter(action => action.type === 'battlePlan');
|
|
assert.ok(actions.length > 0, `${difficulty} must offer a legal plan`);
|
|
const next = act(structuredClone(game), 'emperor', actions[0]);
|
|
assert.equal(next.battle!.plans.emperor.leader, null);
|
|
assert.equal(next.battle!.plans.emperor.weapon, null);
|
|
assert.equal(next.battle!.plans.emperor.defense, null);
|
|
assert.deepEqual(viewGame(next, 'guild').battle!.leaderless, ['emperor']);
|
|
}
|
|
});
|