146 lines
7.8 KiB
TypeScript
146 lines
7.8 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 { ecazOccupyLeadOptions } from '../game/ecaz-occupy-options';
|
|
import { homeworldGameIntegrity, homeworldTable } from '../game/homeworld-game';
|
|
import {
|
|
createLowEcazBattleLeadFixture as fixture,
|
|
actLowEcazBattleLead as act,
|
|
chooseLowEcazBattleLead as choose,
|
|
openLowEcazBattleLeadPlans as openPlans,
|
|
lowEcazBattleLeadInventory as inventory,
|
|
lowEcazBattleLeadPlayer as player,
|
|
} from './fixture-low-ecaz-battle-lead';
|
|
|
|
function reject(game: Game, actor: string, action: Action) {
|
|
const before = structuredClone(game);
|
|
assert.throws(() => applyAction(game, actor, action));
|
|
assert.deepEqual(game, before);
|
|
}
|
|
function physical(game: Game) {
|
|
return game.players.map(seat => ({
|
|
id: seat.id, reserves: seat.reserves, tanks: seat.tanks, forces: structuredClone(seat.forces),
|
|
elites: structuredClone(seat.elites), spice: seat.spice, ally: seat.ally,
|
|
leaders: structuredClone(seat.leaders),
|
|
}));
|
|
}
|
|
|
|
for (const advanced of [false, true]) {
|
|
const mode = advanced ? 'Advanced' : 'Basic';
|
|
|
|
void test(`${mode} native Low Ecaz lets its actual opponent select either coalition lead`, () => {
|
|
const f = fixture({ advanced });
|
|
assert.equal(f.game.homeworlds !== null, true); assert.deepEqual(f.game.expansions, ['ecaz']);
|
|
assert.ok(!f.game.nexusCards); assert.ok(!f.game.leaderSkills);
|
|
assert.equal(player(f.afterSetup, f.ecaz).reserves, 14);
|
|
assert.equal(player(f.afterSetup, f.ecaz).spice, 12);
|
|
assert.equal(player(f.game, f.ecaz).reserves, 6);
|
|
assert.equal(player(f.game, f.ecaz).spice, 4);
|
|
assert.equal(player(f.game, f.ecaz).forces[f.location], 8);
|
|
assert.deepEqual(f.allianceSteps, [
|
|
{ actor: f.ecaz, action: { type: 'alliance', target: f.ally } },
|
|
{ actor: f.ally, action: { type: 'alliance', target: f.ecaz } },
|
|
]);
|
|
const shipment = f.shipments.find(step => step.actor === f.ecaz); assert.ok(shipment);
|
|
assert.equal(shipment.action.amount, 8); assert.ok(shipment.action.homeworldSources);
|
|
assert.equal(f.setupSteps.some(step => step.actor === f.ecaz && step.action.type === 'ecazSetup'), true);
|
|
assert.equal(f.sourceActionCount, f.actions.length);
|
|
const home = homeworldTable(f.game)!.find(home => home.native === f.ecaz)!;
|
|
assert.equal(home.population, 6); assert.equal(home.side, 'low');
|
|
assert.ok(f.game.decision?.kind === 'ecazBattleLead');
|
|
assert.equal(f.game.decision.player, f.opponent); assert.equal(f.game.decision.lowEcaz, true);
|
|
assert.equal(f.game.decision.event, f.game.battle!.event);
|
|
const options = ecazOccupyLeadOptions(viewGame(f.game, f.opponent));
|
|
assert.deepEqual(options.map(option => option.lead), [f.ecaz, f.ally]);
|
|
assert.ok(options.every(option => option.blocked === null));
|
|
assert.ok(ecazOccupyLeadOptions(viewGame(f.game, f.ecaz)).every(option => option.blocked !== null));
|
|
for (const lead of [f.ecaz, f.ally]) {
|
|
const selected = choose(f.game, lead);
|
|
assert.equal(selected.battle!.ecazOccupy!.lead, lead);
|
|
assert.equal(selected.response?.kind, 'ecazOccupy'); assert.equal(selected.response?.owner, f.ecaz);
|
|
assert.deepEqual(physical(selected), physical(f.game));
|
|
const game = openPlans(selected), profile = viewGame(game, lead).battle!.ecazOccupy!.profile!;
|
|
assert.equal(profile.planOwner, lead); assert.equal(profile.payer, lead);
|
|
assert.equal(profile.forceOwner, f.ally); assert.equal(profile.fixedEcazDial, 4);
|
|
assert.equal(profile.ecazForces.normal, 8); assert.equal(profile.canceled, false);
|
|
assert.ok([game.battle!.attacker, game.battle!.defender].includes(lead));
|
|
assert.ok([game.battle!.attacker, game.battle!.defender].includes(f.opponent));
|
|
assert.deepEqual(inventory(game), f.originalInventory); homeworldGameIntegrity(game);
|
|
}
|
|
});
|
|
|
|
void test(`${mode} native Low Ecaz rejects Ecaz self-selection and a stale opponent event`, () => {
|
|
const f = fixture({ advanced }), action = f.leadActions[0].action;
|
|
reject(f.game, f.ecaz, action); reject(f.game, f.ally, action);
|
|
reject(f.game, f.opponent, { ...action, event: `${f.event}:stale` });
|
|
reject(f.game, f.opponent, { ...action, lead: f.opponent });
|
|
const selected = choose(f.game, f.ally);
|
|
assert.equal(selected.battle!.ecazOccupy!.lead, f.ally);
|
|
reject(selected, f.opponent, { ...action, lead: f.ecaz });
|
|
assert.equal(selected.battle!.ecazOccupy!.lead, f.ally);
|
|
});
|
|
|
|
void test(`${mode} native seven-reserve Ecaz retains its ordinary battle-lead choice`, () => {
|
|
const f = fixture({ advanced, reserves: 7 });
|
|
assert.equal(player(f.game, f.ecaz).reserves, 7);
|
|
assert.equal(player(f.game, f.ecaz).forces[f.location], 7);
|
|
assert.equal(player(f.game, f.ecaz).spice, 5);
|
|
const home = homeworldTable(f.game)!.find(home => home.native === f.ecaz)!;
|
|
assert.equal(home.population, 7); assert.equal(home.side, 'high');
|
|
assert.ok(f.game.decision?.kind === 'ecazBattleLead');
|
|
assert.equal(f.game.decision.player, f.ecaz); assert.equal(f.game.decision.lowEcaz, undefined);
|
|
reject(f.game, f.opponent, f.leadActions[0].action);
|
|
const selected = choose(f.game, f.ally);
|
|
assert.equal(selected.battle!.ecazOccupy!.lead, f.ally);
|
|
assert.equal(selected.response?.kind, 'ecazOccupy');
|
|
assert.deepEqual(inventory(selected), f.originalInventory);
|
|
});
|
|
|
|
void test(`${mode} native Guild Karama cancels Occupy while preserving either opponent-selected lead`, () => {
|
|
const f = fixture({ advanced });
|
|
assert.deepEqual(player(f.afterSetup, f.holder).hand.map(card => card.id), [f.karama]);
|
|
for (const lead of [f.ecaz, f.ally]) {
|
|
let game = choose(f.game, lead);
|
|
game = act(game, f.holder, f.cancelAction);
|
|
game = openPlans(game);
|
|
assert.equal(game.battle!.ecazOccupy!.lead, lead);
|
|
assert.equal(game.battle!.ecazOccupy!.canceled, true);
|
|
const profile = viewGame(game, lead).battle!.ecazOccupy!.profile!;
|
|
assert.equal(profile.planOwner, lead); assert.equal(profile.payer, lead);
|
|
assert.equal(profile.forceOwner, lead); assert.equal(profile.fixedEcazDial, 0);
|
|
assert.equal(profile.forces.normal, lead === f.ecaz ? 8 : 2);
|
|
assert.equal(profile.forces.elite, lead === f.ecaz ? 0 : 2);
|
|
assert.equal(game.discard.filter(card => card.id === f.karama).length, 1);
|
|
assert.equal(player(game, f.holder).hand.some(card => card.id === f.karama), false);
|
|
assert.deepEqual(physical(game), physical(f.game));
|
|
assert.equal(game.decision?.kind, undefined);
|
|
assert.deepEqual(inventory(game), f.originalInventory); homeworldGameIntegrity(game);
|
|
}
|
|
});
|
|
|
|
void test(`${mode} all four native opponent policies make a legal Low Ecaz lead selection`, () => {
|
|
const f = fixture({ advanced });
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(f.game, f.opponent);
|
|
view.players.find(seat => seat.id === f.opponent)!.bot = difficulty;
|
|
const before = structuredClone(view), actions = botActions(view);
|
|
assert.ok(actions.length > 0, difficulty);
|
|
assert.deepEqual(view, before);
|
|
for (const action of actions) {
|
|
assert.equal(action.type, 'decision', difficulty);
|
|
assert.equal(action.event, f.event, difficulty);
|
|
assert.ok(action.lead === f.ecaz || action.lead === f.ally, difficulty);
|
|
const selected = act(f.game, f.opponent, action);
|
|
assert.equal(selected.battle!.ecazOccupy!.lead, action.lead, difficulty);
|
|
assert.equal(selected.response?.kind, 'ecazOccupy', difficulty);
|
|
assert.deepEqual(inventory(selected), f.originalInventory);
|
|
}
|
|
const ecazView = viewGame(f.game, f.ecaz);
|
|
ecazView.players.find(seat => seat.id === f.ecaz)!.bot = difficulty;
|
|
assert.deepEqual(botActions(ecazView), []);
|
|
}
|
|
});
|
|
}
|