Files
Dune/tests/ecaz-occupy-options.test.ts

136 lines
7.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction, viewGame } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { controlsLeader } from '../game/leader-control';
import { pinDeterministicRandom } from './deterministic-random';
import {
ecazOccupyDialChoices,
ecazOccupyLeadOptions,
ecazOccupyOwnProfile,
ecazOccupyPlanControl,
ecazOccupyPolicyActions,
} from '../game/ecaz-occupy-options';
import {
cancelEcazOccupy,
chooseEcazOccupyLead,
ecazOccupyFixture,
openEcazOccupyPlans,
} from './fixture-ecaz-occupy';
void test('Ecaz can choose either real plan owner; other seats cannot choose the lead', () => {
const fixture = ecazOccupyFixture({ allyFaction: 'guild', ecazForces: 3 });
const own = viewGame(fixture.game, fixture.ecaz);
const choices = ecazOccupyLeadOptions(own);
assert.deepEqual(choices.map((choice) => choice.lead), [fixture.ecaz, fixture.ally]);
for (const choice of choices) {
assert.equal(choice.blocked, null);
const game = applyAction(fixture.game, fixture.ecaz, choice.action);
const selected = ecazOccupyOwnProfile(viewGame(game, choice.lead));
assert.equal(selected?.planOwner, choice.lead);
assert.equal(selected?.payer, choice.lead);
assert.equal(selected?.forceOwner, fixture.ally);
assert.equal(selected?.fixedEcazDial, 2);
assert.equal(ecazOccupyOwnProfile(viewGame(game, fixture.opponent)), null);
}
const outsider = viewGame(fixture.game, fixture.ally);
assert.ok(ecazOccupyLeadOptions(outsider).every((choice) => !!choice.blocked));
assert.deepEqual(ecazOccupyPolicyActions(outsider), []);
for (const choice of choices)
assert.throws(() => applyAction(fixture.game, fixture.ally, choice.action));
});
void test('free fixed Ecaz strength is mandatory and cannot borrow the other ally’s spice', () => {
const fixture = ecazOccupyFixture({ allyFaction: 'guild', ecazForces: 3 });
const game = openEcazOccupyPlans(chooseEcazOccupyLead(fixture, 'ecaz'));
// Labelled Spice Bank budget staging: no allied credit or force property changes.
game.players.find((player) => player.id === fixture.ecaz)!.spice = 0;
game.players.find((player) => player.id === fixture.ally)!.spice = 30;
const view = viewGame(game, fixture.ecaz);
const choices = ecazOccupyDialChoices(view);
assert.ok(choices.some((choice) => choice.dial === 2 && choice.support === 0));
assert.ok(choices.some((choice) => choice.dial === 2.5 && choice.support === 0));
assert.ok(choices.every((choice) => choice.support === 0 && choice.dial >= 2));
assert.equal(ecazOccupyPlanControl(view, 2, 0)?.blocked, null);
assert.ok(ecazOccupyPlanControl(view, 1.5, 0)?.blocked);
assert.ok(ecazOccupyPlanControl(view, 3, 1)?.blocked);
assert.deepEqual(ecazOccupyDialChoices(viewGame(game, fixture.ally)), []);
const me = view.players.find((player) => player.id === view.me)!;
const leader = me.leaders.find((entry) => !entry.dead && controlsLeader(me, entry) &&
(!entry.usedAt || entry.usedAt === fixture.territory))!;
assert.ok(leader);
const next = applyAction(game, fixture.ecaz, {
type: 'battlePlan', dial: 2.5, support: 0, leader: leader.id,
weapon: null, defense: null, kwisatz: false,
});
assert.equal(next.battle?.plans[fixture.ecaz].dial, 2.5);
assert.equal(next.battle?.plans[fixture.ecaz].support, 0);
assert.equal(next.players.find((player) => player.id === fixture.ecaz)?.spice, 0);
assert.equal(next.players.find((player) => player.id === fixture.ally)?.spice, 30);
});
void test('native Karama keeps either selected lead but replaces the active combined dial with their own army', () => {
for (const lead of ['ecaz', 'ally'] as const) {
const fixture = ecazOccupyFixture({ allyFaction: 'guild', ecazForces: 3 });
const game = openEcazOccupyPlans(cancelEcazOccupy(fixture, lead));
const actor = lead === 'ecaz' ? fixture.ecaz : fixture.ally;
const view = viewGame(game, actor);
const control = ecazOccupyPlanControl(view, 0, 0)!;
assert.equal(control.profile.canceled, true);
assert.equal(control.profile.planOwner, actor);
assert.equal(control.profile.forceOwner, actor);
assert.equal(control.minimum, 0);
assert.equal(control.fixedEcazDial, 0);
assert.equal(control.maximum, lead === 'ecaz' ? 3 : 4);
assert.equal(control.blocked, null);
assert.ok(ecazOccupyDialChoices(view).some((choice) => choice.dial === 0 && choice.support === 0));
assert.ok(ecazOccupyPlanControl(view, control.maximum + 0.5, 0)?.blocked);
if (lead === 'ecaz') assert.ok(ecazOccupyPlanControl(view, 4, 3)?.blocked);
}
});
void test('typed Fremen and Emperor variable armies retain native legal increments and support', () => {
const fremen = ecazOccupyFixture({ allyFaction: 'fremen', ecazForces: 3 });
const fremenGame = openEcazOccupyPlans(chooseEcazOccupyLead(fremen, 'ecaz'));
const fremenView = viewGame(fremenGame, fremen.ecaz);
const free = ecazOccupyDialChoices(fremenView);
assert.ok(free.some((choice) => choice.variableDial === 6 && choice.dial === 8));
assert.ok(free.every((choice) => choice.support === 0 && Number.isInteger(choice.variableDial)));
assert.ok(ecazOccupyPlanControl(fremenView, 2.5, 0)?.blocked);
assert.ok(ecazOccupyPlanControl(fremenView, 3, 1)?.blocked);
const emperor = ecazOccupyFixture({ allyFaction: 'emperor', ecazForces: 3 });
const emperorGame = openEcazOccupyPlans(chooseEcazOccupyLead(emperor, 'ecaz'));
const emperorView = viewGame(emperorGame, emperor.ecaz);
const paid = ecazOccupyDialChoices(emperorView);
assert.ok(paid.some((choice) => choice.variableDial === 0.5 && choice.support === 0));
assert.ok(paid.some((choice) => choice.variableDial === 6 && choice.support === 4));
assert.equal(ecazOccupyPlanControl(emperorView, 8, 4)?.blocked, null);
});
void test('all four native policies finish Occupy planning through optional powers for either lead, active or canceled', (t) => {
pinDeterministicRandom(t, 8);
for (const lead of ['ecaz', 'ally'] as const) for (const canceled of [false, true]) {
const fixture = ecazOccupyFixture({ allyFaction: 'guild', ecazForces: 3 });
const game = openEcazOccupyPlans(canceled
? cancelEcazOccupy(fixture, lead) : chooseEcazOccupyLead(fixture, lead));
const actor = lead === 'ecaz' ? fixture.ecaz : fixture.ally;
for (const policy of DIFFICULTIES) {
let state = structuredClone(game);
for (let step = 0; step < 4; step++) {
const view = viewGame(state, actor);
view.players.find((player) => player.id === actor)!.bot = policy;
const actions = botActions(view);
const action = actions[0];
assert.ok(action, `${policy}/${lead}/${canceled}: planning must retain a legal continuation`);
if (action.type === 'battlePlan')
assert.equal(ecazOccupyPlanControl(view, Number(action.dial), Number(action.support ?? 0))?.blocked, null);
state = applyAction(state, actor, action);
if (viewGame(state, actor).battle?.submitted.includes(actor)) break;
}
assert.ok(viewGame(state, actor).battle?.submitted.includes(actor),
`${policy}/${lead}/${canceled}: optional powers must finish and the selected lead must seal a plan`);
}
}
});