Integrate authorized Advanced Ecaz Occupy combined battles

This commit is contained in:
Quintine committed 2026-10-02 13:06:00 +10:00
1 parent bf4122029e
commit e112df858a
29 files changed
+1897 -123

No files matched your search

+127
View File
@@ -0,0 +1,127 @@
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 {
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 can seal a legal total plan for either lead, active or canceled', () => {
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) {
const view = viewGame(game, actor);
view.players.find((player) => player.id === actor)!.bot = policy;
const plan = botActions(view).find((action) => action.type === 'battlePlan');
assert.ok(plan, `${policy}/${lead}/${canceled}: native policy must supply a sealable plan`);
assert.equal(ecazOccupyPlanControl(view, Number(plan.dial), Number(plan.support ?? 0))?.blocked, null);
const next = applyAction(game, actor, plan);
assert.ok(viewGame(next, actor).battle?.submitted.includes(actor),
`${policy}/${lead}/${canceled}: the native table must show the selected lead's sealed plan`);
}
}
});