Files
Dune/tests/ecaz-homeworld-lead.test.ts

114 lines
7.1 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { newPlayer, viewGame, type Game } from '../game/engine';
import { createHomeworldCustody, quoteHomeworldCustody, HomeworldCustodyError } from '../game/homeworld-custody';
import { homeworldContext } from '../game/homeworld-game';
import { quoteEcazBattleLead } from '../game/ecaz-homeworld-lead';
import { ecazOccupyLeadOptions, ecazOccupyPolicyActions } from '../game/ecaz-occupy-options';
import { EcazOccupyLeadChoice } from '../components/ecaz-occupy';
import { ecazOccupyFixture } from './fixture-ecaz-occupy';
/** Controlled population and custody contexts isolate the printed chooser;
* they are not earned battle or setup chronologies. */
function populationContext(population = 7, advanced = false): Pick<Game, 'advanced' | 'players' | 'homeworlds'> {
const players = [newPlayer('ec', 'Ecaz', 'ecaz'), newPlayer('enemy', 'Guild', 'guild'),
newPlayer('ally', 'Atreides', 'atreides')];
players[0].reserves = population;
players[0].tanks = 20 - population;
players[0].ally = 'ally'; players[2].ally = 'ec';
return { advanced, players, homeworlds: { custody: createHomeworldCustody(homeworldContext({ advanced, players })) } };
}
void test('Low Ecaz gives its opponent the choice in Basic and Advanced; seven native counters restore Ecaz choice', () => {
for (const advanced of [false, true]) for (const population of [0, 6, 7, 14, 20]) {
const context = populationContext(population, advanced), before = structuredClone(context);
assert.deepEqual(quoteEcazBattleLead(context, 'ec', 'enemy'), population < 7
? { player: 'enemy', lowEcaz: true } : { player: 'ec' });
assert.deepEqual(context, before);
}
});
void test('without actual Homeworld custody the ordinary Ecaz chooser remains unchanged', () => {
const context = populationContext(0);
assert.deepEqual(quoteEcazBattleLead({ ...context, homeworlds: undefined }, 'ec', 'enemy'), { player: 'ec' });
assert.deepEqual(quoteEcazBattleLead({ ...context, homeworlds: { custody: null } }, 'ec', 'enemy'), { player: 'ec' });
});
void test('foreign visitors do not raise native Low Ecaz population', () => {
const context = populationContext(6);
const visit = quoteHomeworldCustody(homeworldContext(context), context.homeworlds!.custody!, [
{ homeworld: 'homeworld:guild', player: 'enemy', withdraw: { normal: 8, elite: 0 }, deposit: { normal: 0, elite: 0 } },
{ homeworld: 'homeworld:ecaz', player: 'enemy', withdraw: { normal: 0, elite: 0 }, deposit: { normal: 8, elite: 0 } },
]);
for (const next of visit.players) context.players.find(player => player.id === next.id)!.reserves = next.reserves;
context.homeworlds!.custody = visit.state;
const before = structuredClone(context);
assert.deepEqual(quoteEcazBattleLead(context, 'ec', 'enemy'), { player: 'enemy', lowEcaz: true });
assert.deepEqual(context, before);
});
void test('current native departures and returns determine fresh quotes without changing an earlier captured result', () => {
const context = populationContext(8);
const original = quoteEcazBattleLead(context, 'ec', 'enemy');
const away = quoteHomeworldCustody(homeworldContext(context), context.homeworlds!.custody!, [
{ homeworld: 'homeworld:ecaz', player: 'ec', withdraw: { normal: 2, elite: 0 }, deposit: { normal: 0, elite: 0 } },
{ homeworld: 'homeworld:guild', player: 'ec', withdraw: { normal: 0, elite: 0 }, deposit: { normal: 2, elite: 0 } },
]);
for (const next of away.players) context.players.find(player => player.id === next.id)!.reserves = next.reserves;
context.homeworlds!.custody = away.state;
const low = quoteEcazBattleLead(context, 'ec', 'enemy');
assert.deepEqual(low, { player: 'enemy', lowEcaz: true });
assert.deepEqual(original, { player: 'ec' });
const back = quoteHomeworldCustody(homeworldContext(context), context.homeworlds!.custody!, [
{ homeworld: 'homeworld:guild', player: 'ec', withdraw: { normal: 2, elite: 0 }, deposit: { normal: 0, elite: 0 } },
{ homeworld: 'homeworld:ecaz', player: 'ec', withdraw: { normal: 0, elite: 0 }, deposit: { normal: 2, elite: 0 } },
]);
for (const next of back.players) context.players.find(player => player.id === next.id)!.reserves = next.reserves;
context.homeworlds!.custody = back.state;
assert.deepEqual(quoteEcazBattleLead(context, 'ec', 'enemy'), { player: 'ec' });
assert.deepEqual(low, { player: 'enemy', lowEcaz: true });
});
void test('the chooser quote requires distinct seated Ecaz and opponent roles', () => {
const context = populationContext(6), before = structuredClone(context);
for (const [ecaz, opponent] of [['missing', 'enemy'], ['enemy', 'ec'], ['ec', 'missing'], ['ec', 'ec'], ['', 'enemy']])
assert.throws(() => quoteEcazBattleLead(context, ecaz, opponent), HomeworldCustodyError);
assert.deepEqual(context, before);
});
void test('controlled captured Low chooser projection gives both original lead choices only to the recorded opponent', () => {
const fixture = ecazOccupyFixture({ allyFaction: 'guild', ecazForces: 3 });
// Only the public recorded chooser is controlled here. The native engine
// integration tests own earning Low population and opening this decision.
const own = viewGame(fixture.game, fixture.opponent);
assert.equal(own.decision?.kind, 'ecazBattleLead');
if (own.decision?.kind !== 'ecazBattleLead') throw Error('Expected original lead choice');
own.decision = { ...own.decision, player: fixture.opponent, lowEcaz: true };
const before = structuredClone(own), choices = ecazOccupyLeadOptions(own);
assert.deepEqual(choices.map(choice => choice.lead), [fixture.ecaz, fixture.ally]);
assert.ok(choices.every(choice => choice.blocked === null));
assert.deepEqual(ecazOccupyPolicyActions(own), choices.map(choice => choice.action));
const chooser = own.players.find(player => player.id === fixture.opponent)!.name;
const other = viewGame(fixture.game, fixture.ecaz);
other.decision = structuredClone(own.decision);
assert.ok(ecazOccupyLeadOptions(other).every(choice => choice.blocked === `Only ${chooser} chooses the lead.`));
assert.deepEqual(ecazOccupyPolicyActions(other), []);
const html = renderToStaticMarkup(createElement(EcazOccupyLeadChoice, { game: own, busy: false, act() {} }));
assert.ok(html.includes(chooser));
assert.match(html, /chooses who forms the plan/);
assert.match(html, /was at Low population when this battle opened/);
assert.deepEqual(own, before);
});
void test('an original lead decision without the Low flag keeps its recorded chooser and ordinary explanation', () => {
const fixture = ecazOccupyFixture({ allyFaction: 'guild', ecazForces: 3 });
const own = viewGame(fixture.game, fixture.ecaz), before = structuredClone(own);
assert.ok(ecazOccupyLeadOptions(own).every(choice => choice.blocked === null));
const html = renderToStaticMarkup(createElement(EcazOccupyLeadChoice, { game: own, busy: false, act() {} }));
assert.match(html, /chooses who forms the plan/);
assert.doesNotMatch(html, /Low population/);
assert.deepEqual(own, before);
});