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
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user