Repair deterministic Occupy test transitions and card staging
This commit is contained in:
1 parent
38b1ba3419
commit
f2fd274502
5 files changed
+61
-18
No files matched your search
+11
-7
@@ -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
|
cancellation of an already-canceled compulsory cache lot remains an explicit
|
||||||
open boundary recorded in the decision index. Two intermittently failing
|
open boundary recorded in the decision index. Two intermittently failing
|
||||||
Moritani skill-runtime tests were measured at the same 3/30 rate on both
|
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
|
revisions, so they are pre-existing nondeterminism rather than a regression.
|
||||||
same holds for `tests/ecaz-occupy-options.test.ts`, whose Easy Occupy plan
|
The Occupy policy failure (2/20 at `4ed6beb`, 3/20 here) is now explained by
|
||||||
candidate is occasionally absent (2/20 failing runs at `4ed6beb` versus 3/20
|
an obsolete immediate-plan assertion: seed 8 legally activates Ecaz special
|
||||||
now), for the paired Moritani skill-runtime tests, and for
|
Karama before sealing. All four profiles finish that captured state in two
|
||||||
`tests/ecaz-occupy-cards-runtime.test.ts` (one intermittent Harass/explosion
|
actions; the deterministic regression now checks finite legal progression.
|
||||||
case, passing 5/5 standalone). Those intermittent failures are pre-existing
|
The Harass/explosion failure is now reproduced at deterministic seed 62:
|
||||||
prototype/AI gaps and are not new regressions.
|
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
|
**Gated captures resumed under the new prototypes.** The 14 captures that
|
||||||
remained gated after the 7 October rulings were resumed in place against their
|
remained gated after the 7 October rulings were resumed in place against their
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
# Dune implementation status
|
# 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
|
## 9 October 2026 — provisional Kulon/Ornithopter coexistence
|
||||||
|
|
||||||
Active Kulon no longer blocks either mode of the Richese movement card.
|
Active Kulon no longer blocks either mode of the Richese movement card.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { botActions } from '../game/bots';
|
|||||||
import { treacheryDeck } from '../game/cards';
|
import { treacheryDeck } from '../game/cards';
|
||||||
import { ecazTreacheryCards } from '../game/ecaz-cards';
|
import { ecazTreacheryCards } from '../game/ecaz-cards';
|
||||||
import { splitLocation } from '../game/board';
|
import { splitLocation } from '../game/board';
|
||||||
|
import { pinDeterministicRandom } from './deterministic-random';
|
||||||
import {
|
import {
|
||||||
createEcazOccupySetup, ecazOccupyTerritoryForces, type EcazOccupyFixtureOptions,
|
createEcazOccupySetup, ecazOccupyTerritoryForces, type EcazOccupyFixtureOptions,
|
||||||
} from './fixture-ecaz-occupy';
|
} 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 lead of ['ecaz', 'ally'] as const) {
|
||||||
for (const outcome of ['loss', 'soleTraitor', 'opposingTraitor', 'mutualTraitors', 'explosion'] 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 result = ecazOccupyCardsCase({ lead, outcome, slot: 'defense' });
|
||||||
const { fixture, actor, game } = result;
|
const { fixture, actor, game } = result;
|
||||||
const canceledReturn = outcome === 'opposingTraitor' || outcome === 'mutualTraitors';
|
const canceledReturn = outcome === 'opposingTraitor' || outcome === 'mutualTraitors';
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { applyAction, viewGame } from '../game/engine';
|
|||||||
import { botActions } from '../game/bots';
|
import { botActions } from '../game/bots';
|
||||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||||
import { controlsLeader } from '../game/leader-control';
|
import { controlsLeader } from '../game/leader-control';
|
||||||
|
import { pinDeterministicRandom } from './deterministic-random';
|
||||||
import {
|
import {
|
||||||
ecazOccupyDialChoices,
|
ecazOccupyDialChoices,
|
||||||
ecazOccupyLeadOptions,
|
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);
|
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]) {
|
for (const lead of ['ecaz', 'ally'] as const) for (const canceled of [false, true]) {
|
||||||
const fixture = ecazOccupyFixture({ allyFaction: 'guild', ecazForces: 3 });
|
const fixture = ecazOccupyFixture({ allyFaction: 'guild', ecazForces: 3 });
|
||||||
const game = openEcazOccupyPlans(canceled
|
const game = openEcazOccupyPlans(canceled
|
||||||
? cancelEcazOccupy(fixture, lead) : chooseEcazOccupyLead(fixture, lead));
|
? cancelEcazOccupy(fixture, lead) : chooseEcazOccupyLead(fixture, lead));
|
||||||
const actor = lead === 'ecaz' ? fixture.ecaz : fixture.ally;
|
const actor = lead === 'ecaz' ? fixture.ecaz : fixture.ally;
|
||||||
for (const policy of DIFFICULTIES) {
|
for (const policy of DIFFICULTIES) {
|
||||||
const view = viewGame(game, actor);
|
let state = structuredClone(game);
|
||||||
view.players.find((player) => player.id === actor)!.bot = policy;
|
for (let step = 0; step < 4; step++) {
|
||||||
const plan = botActions(view).find((action) => action.type === 'battlePlan');
|
const view = viewGame(state, actor);
|
||||||
assert.ok(plan, `${policy}/${lead}/${canceled}: native policy must supply a sealable plan`);
|
view.players.find((player) => player.id === actor)!.bot = policy;
|
||||||
assert.equal(ecazOccupyPlanControl(view, Number(plan.dial), Number(plan.support ?? 0))?.blocked, null);
|
const actions = botActions(view);
|
||||||
const next = applyAction(game, actor, plan);
|
const action = actions[0];
|
||||||
assert.ok(viewGame(next, actor).battle?.submitted.includes(actor),
|
assert.ok(action, `${policy}/${lead}/${canceled}: planning must retain a legal continuation`);
|
||||||
`${policy}/${lead}/${canceled}: the native table must show the selected lead's sealed plan`);
|
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`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -157,7 +157,10 @@ export function stageEcazOccupyCard(game: Game, owner: string,
|
|||||||
assert.ok(donor, 'A conserved original physical card must supply this rule case.');
|
assert.ok(donor, 'A conserved original physical card must supply this rule case.');
|
||||||
const donorIndex = donor.hand.findIndex(predicate);
|
const donorIndex = donor.hand.findIndex(predicate);
|
||||||
card = donor.hand[donorIndex];
|
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.');
|
assert.ok(replacement, 'A conserved physical face must preserve the donor hand.');
|
||||||
donor.hand[donorIndex] = replacement;
|
donor.hand[donorIndex] = replacement;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user