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

+11 -7
View File
@@ -215,13 +215,17 @@ reveals before that companion moves, in both modes. A repeated-Karama
cancellation of an already-canceled compulsory cache lot remains an explicit
open boundary recorded in the decision index. Two intermittently failing
Moritani skill-runtime tests were measured at the same 3/30 rate on both
revisions, so they are pre-existing nondeterminism rather than a regression; the
same holds for `tests/ecaz-occupy-options.test.ts`, whose Easy Occupy plan
candidate is occasionally absent (2/20 failing runs at `4ed6beb` versus 3/20
now), for the paired Moritani skill-runtime tests, and for
`tests/ecaz-occupy-cards-runtime.test.ts` (one intermittent Harass/explosion
case, passing 5/5 standalone). Those intermittent failures are pre-existing
prototype/AI gaps and are not new regressions.
revisions, so they are pre-existing nondeterminism rather than a regression.
The Occupy policy failure (2/20 at `4ed6beb`, 3/20 here) is now explained by
an obsolete immediate-plan assertion: seed 8 legally activates Ecaz special
Karama before sealing. All four profiles finish that captured state in two
actions; the deterministic regression now checks finite legal progression.
The Harass/explosion failure is now reproduced at deterministic seed 62:
the fixture's donor exchange removed its already-selected Harass card.
Staging now uses a conserved deck replacement while hand capacity remains.
The repaired physical scenario returns two forces before the explosion,
discards Harass once and reaches Collection. Neither finding changes rules
or bot behavior; the earlier attribution to prototype/AI gaps was incorrect.
**Gated captures resumed under the new prototypes.** The 14 captures that
remained gated after the 7 October rulings were resumed in place against their
+26
View File
@@ -1,5 +1,31 @@
# Dune implementation status
## 9 October 2026 — two Occupy failures isolated to test contracts and staging
Deterministic seed 8 reproduces the recorded missing-immediate-plan failure.
The bot actually offers a legal Ecaz special Karama action; Easy, Medium,
Hard and Brutal each then seal the selected lead's plan on their next action.
The actual captured engine smoke consumes Karama once and submits the plan.
No bot or rule behavior changed.
The regression now pins entropy and follows actual first-policy actions to
a sealed plan for both leads and active/canceled Occupy instead of requiring
the first action list to contain a plan. That case passes.
Deterministic seed 62 also reproduces the Harass ally/explosion failure:
when Lasgun must come from another hand, fixture staging gives that donor
the actor's sole held Harass, then tries to seal the now-unavailable defense.
The engine correctly rejects it. The staging helper now funds a donor
replacement from the original deck when the recipient has room, retaining
its earlier selected physical cards. Full hands keep the existing exchange.
The real repaired scenario returns two forces before the explosion,
discards Harass once and reaches Collection. Its deterministic regression
passes. Neither repair changes production engine or bot behavior.
Frozen verification: `check:quick` passes and all 270 cases in the nine
affected Occupy, Stronghold and native-tech files pass. No full offline
suite, HTTP campaign, database audit or deployment was run.
## 9 October 2026 — provisional Kulon/Ornithopter coexistence
Active Kulon no longer blocks either mode of the Richese movement card.
+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;
}