Repair deterministic Occupy test transitions and card staging

This commit is contained in:
Quintine committed 2026-10-09 02:53:09 +11:00
1 parent 38b1ba3419
commit f2fd274502
5 files changed
+61 -18

No files matched your search

+3 -1
View File
@@ -5,6 +5,7 @@ import { botActions } from '../game/bots';
import { treacheryDeck } from '../game/cards';
import { ecazTreacheryCards } from '../game/ecaz-cards';
import { splitLocation } from '../game/board';
import { pinDeterministicRandom } from './deterministic-random';
import {
createEcazOccupySetup, ecazOccupyTerritoryForces, type EcazOccupyFixtureOptions,
} from './fixture-ecaz-occupy';
@@ -121,7 +122,8 @@ for (const lead of ['ecaz', 'ally'] as const) for (const slot of ['weapon', 'def
for (const lead of ['ecaz', 'ally'] as const) {
for (const outcome of ['loss', 'soleTraitor', 'opposingTraitor', 'mutualTraitors', 'explosion'] as const) {
void test(`Harass ${lead} lead, actual ${outcome}: returns precede losses except an opposing successful traitor`, () => {
void test(`Harass ${lead} lead, actual ${outcome}: returns precede losses except an opposing successful traitor`, (t) => {
pinDeterministicRandom(t, 62);
const result = ecazOccupyCardsCase({ lead, outcome, slot: 'defense' });
const { fixture, actor, game } = result;
const canceledReturn = outcome === 'opposingTraitor' || outcome === 'mutualTraitors';
+17 -9
View File
@@ -4,6 +4,7 @@ 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,
@@ -107,21 +108,28 @@ void test('typed Fremen and Emperor variable armies retain native legal incremen
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', () => {
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) {
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`);
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`);
}
}
});
+4 -1
View File
@@ -157,7 +157,10 @@ export function stageEcazOccupyCard(game: Game, owner: string,
assert.ok(donor, 'A conserved original physical card must supply this rule case.');
const donorIndex = donor.hand.findIndex(predicate);
card = donor.hand[donorIndex];
const replacement = player.hand.shift() ?? game.deck.shift();
// Keep previously staged battle cards when there is still hand capacity.
// A donor's replacement comes from the original deck, just as a direct
// staging draw does; only a full hand must fund it with a held card.
const replacement = player.hand.length >= 4 ? player.hand.shift() : game.deck.shift();
assert.ok(replacement, 'A conserved physical face must preserve the donor hand.');
donor.hand[donorIndex] = replacement;
}