Files
Dune/tests/caladan-face-dance-engine.test.ts

160 lines
9.1 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import {
actCaladanFaceDance, caladanFaceDanceInventory, caladanFaceDancePhysical,
caladanFaceDancePlayer as player, caladanFaceDancePopulation, createCaladanFaceDanceFixture,
faceDanceCaladanFaceDance, finishCaladanFaceDance, reinforceCaladanFaceDance,
type CaladanFaceDanceFixture,
} from './fixture-caladan-face-dance';
function rejected(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => actCaladanFaceDance(game, actor, action));
assert.deepEqual(game, before);
}
function source(f: CaladanFaceDanceFixture) {
assert.equal(f.game.status, 'playing');
assert.equal(f.game.turn, f.beforeBattle.turn);
assert.equal(f.game.phase, 6);
assert.deepEqual(f.game.expansions, ['ix']);
assert.ok(f.game.homeworlds?.custody);
assert.ok(!f.game.nexusCards && !f.game.leaderSkills && !f.game.techTokens &&
!f.game.strongholdCards && !f.game.discoveryEnabled && !f.game.ecazTreachery);
assert.ok(f.game.decision?.kind === 'caladanReinforcement');
assert.equal(f.game.decision.player, f.owner);
assert.equal(f.game.decision.event, f.event);
assert.equal(f.game.response ?? null, null);
assert.equal(f.game.phaseOpening ?? null, null);
assert.equal(f.game.pendingTreacheryDiscard ?? null, null);
assert.equal(f.game.pendingFaceDance!.player, f.dancer);
assert.equal(f.game.pendingFaceDance!.winner, f.owner);
assert.equal(f.game.pendingFaceDance!.identity, f.leader);
assert.equal(f.game.homeworldVictoryReinforcement!.stage, 'choice');
assert.equal(f.game.homeworldVictoryReinforcement!.offer!.blocked, null);
assert.equal(f.game.homeworldVictoryReinforcement!.offer!.population, 6);
assert.equal(f.game.homeworldVictoryReinforcement!.offer!.survivors, 10);
assert.equal(caladanFaceDancePopulation(f.afterSetup), 10);
assert.equal(caladanFaceDancePopulation(f.game), 6);
assert.equal(player(f.game, f.owner).forces[f.location], 10);
assert.equal(player(f.game, f.owner).forces['polar_sink:0'], 4);
assert.equal(player(f.game, f.dancer).reserves, 20);
assert.equal(f.shipments.length, 2);
assert.equal(f.shipments[0].step.actor, f.owner);
assert.equal(f.shipments[0].step.action.amount, 4);
assert.equal(f.shipments[0].step.action.territory, 'polar_sink');
assert.ok(f.shipments[0].step.action.homeworldSources);
assert.equal(f.shipments[1].step.actor, f.opponent);
assert.equal(f.shipments[1].step.action.amount, 1);
assert.equal(f.shipments[1].step.action.territory, f.territory);
assert.ok(player(f.afterSetup, f.dancer).faceDancers!.some(card => card.leader === f.leader && !card.revealed));
assert.ok(!player(f.game, f.owner).leaders.find(leader => leader.id === f.leader)!.dead);
assert.equal(f.game.lastBattleContext!.winner, f.owner);
assert.equal(f.game.lastBattleContext!.event, f.event);
assert.equal(f.sourceActionCount, f.actions.length);
assert.deepEqual(caladanFaceDanceInventory(f.game), f.originalInventory);
}
function completedGrant(f: CaladanFaceDanceFixture, game: Game, give: boolean) {
assert.equal(game.homeworldVictoryReinforcement!.event, f.event);
assert.equal(game.homeworldVictoryReinforcement!.stage, 'complete');
assert.equal(game.homeworldVictoryReinforcement!.destination, give ? f.destination : 'decline');
assert.equal(game.lastBattleContext!.caladanReinforcement!.event, f.event);
assert.equal(game.lastBattleContext!.caladanReinforcement!.completed, true);
}
for (const advanced of [false, true]) {
for (const give of [true, false]) for (const reveal of [true, false]) {
void test(`${advanced ? 'Advanced' : 'Basic'} native Caladan ${give ? 'reinforces before' : 'declines before'} Face Dance ${reveal ? 'reveal' : 'decline'}`, () => {
const f = createCaladanFaceDanceFixture({ advanced });
source(f);
let game = reinforceCaladanFaceDance(f, structuredClone(f.game), give);
const amount = give ? 11 : 10;
assert.equal(game.decision?.kind, 'faceDance');
assert.deepEqual(game.pendingFaceDance, f.originalFaceDance);
assert.equal(player(game, f.owner).forces[f.location], amount);
assert.equal(caladanFaceDancePopulation(game), give ? 5 : 6);
assert.equal(player(game, f.owner).spice, player(f.game, f.owner).spice);
assert.equal(player(game, f.owner).shipped, player(f.game, f.owner).shipped);
assert.equal(player(game, f.owner).moved, player(f.game, f.owner).moved);
completedGrant(f, game, give);
rejected(game, f.owner, f.action);
rejected(game, f.dancer, { type: 'decision', reveal: true, sources: { reserves: amount + 1 }, sector: 10 });
const beforeDance = structuredClone(game);
game = faceDanceCaladanFaceDance(f, game, reveal);
assert.equal(player(game, f.owner).spice, player(beforeDance, f.owner).spice);
assert.equal(player(game, f.dancer).spice, player(beforeDance, f.dancer).spice);
// Later collection may pay the original or replacement stronghold owner.
game = finishCaladanFaceDance(f, game);
completedGrant(f, game, give);
assert.equal(game.pendingFaceDance ?? null, null);
assert.notEqual(game.decision?.kind, 'caladanReinforcement');
assert.notEqual(game.decision?.kind, 'faceDance');
assert.equal(player(game, f.owner).forces['polar_sink:0'], 4);
if (reveal) {
assert.equal(player(game, f.owner).forces[f.location] ?? 0, 0);
assert.equal(player(game, f.dancer).forces[f.location], amount);
assert.equal(player(game, f.dancer).reserves, player(beforeDance, f.dancer).reserves - amount);
assert.equal(caladanFaceDancePopulation(game), 16,
'All live winning counters, including the granted one, return to the same native Caladan pool');
assert.equal(player(game, f.owner).leaders.find(leader => leader.id === f.leader)!.dead, true);
assert.equal(player(game, f.owner).leaders.find(leader => leader.id === f.leader)!.deaths,
player(f.game, f.owner).leaders.find(leader => leader.id === f.leader)!.deaths + 1);
assert.equal(player(game, f.dancer).faceDancers!.filter(card => card.leader === f.leader && card.revealed).length, 1);
} else {
assert.equal(player(game, f.owner).forces[f.location], amount);
assert.equal(player(game, f.dancer).forces[f.location] ?? 0, 0);
assert.equal(player(game, f.dancer).reserves, 20);
assert.equal(caladanFaceDancePopulation(game), give ? 5 : 6);
assert.equal(player(game, f.owner).leaders.find(leader => leader.id === f.leader)!.dead, false);
assert.ok(player(game, f.dancer).faceDancers!.some(card => card.leader === f.leader && !card.revealed));
}
rejected(game, f.owner, f.action);
assert.deepEqual(caladanFaceDanceInventory(game), f.originalInventory);
});
}
void test(`${advanced ? 'Advanced' : 'Basic'} native Caladan pending Face Dance rejects stale, foreign and early reveal choices`, () => {
const f = createCaladanFaceDanceFixture({ advanced });
source(f);
rejected(f.game, f.dancer, f.action);
rejected(f.game, f.opponent, f.action);
rejected(f.game, f.owner, { ...f.action, event: 'stale' });
rejected(f.game, f.owner, { ...f.action, amount: 2 });
rejected(f.game, f.dancer, { type: 'decision', reveal: true, sources: { reserves: 11 }, sector: 10 });
assert.deepEqual(caladanFaceDancePhysical(f.game), f.originalPhysical);
assert.deepEqual(f.game.pendingFaceDance, f.originalFaceDance);
assert.equal(f.game.homeworldVictoryReinforcement!.stage, 'choice');
assert.deepEqual(caladanFaceDanceInventory(f.game), f.originalInventory);
});
}
void test('four minimal profiles choose the real Caladan reinforcement and native Face Dance without a second grant', () => {
for (const advanced of [false, true]) {
const f = createCaladanFaceDanceFixture({ advanced });
for (const difficulty of DIFFICULTIES) {
const ownerView = viewGame(f.game, f.owner);
ownerView.players.find(seat => seat.id === f.owner)!.bot = difficulty;
const reinforcement = botActions(ownerView)[0];
assert.deepEqual(reinforcement, f.action, `${advanced}/${difficulty} reinforces the actual surviving army`);
let game = actCaladanFaceDance(structuredClone(f.game), f.owner, reinforcement);
assert.equal(player(game, f.owner).forces[f.location], 11);
assert.equal(game.decision?.kind, 'faceDance');
const dancerView = viewGame(game, f.dancer);
dancerView.players.find(seat => seat.id === f.dancer)!.bot = difficulty;
const dance = botActions(dancerView)[0];
assert.ok(dance);
assert.equal(dance.type, 'decision');
assert.equal(dance.reveal, true);
assert.deepEqual(dance.sources, { reserves: 11 });
game = actCaladanFaceDance(game, f.dancer, dance);
game = finishCaladanFaceDance(f, game);
assert.equal(player(game, f.dancer).forces[f.location], 11);
assert.equal(caladanFaceDancePopulation(game), 16);
completedGrant(f, game, true);
assert.deepEqual(caladanFaceDanceInventory(game), f.originalInventory);
}
}
});