Retain trained Ixian response inside paused Semuta cost
This commit is contained in:
1 parent
90c6f0020c
commit
5b3729e2db
7 files changed
+92
-8
No files matched your search
@@ -29,6 +29,18 @@ optional skill bands, combinations and public expansion starts remain open.
|
||||
Rules implementation continues; comprehensive assurance and administration
|
||||
remain deferred until all rules are implemented.
|
||||
|
||||
**Semuta / trained Ixian response ownership — 11 October.** Newly admitted
|
||||
native Ixian skills exposed a real projection failure: during a printed
|
||||
substitution-cancellation cost, the validator ignored the response saved in
|
||||
`karamaBattleDiscard` and reported a lost original choice. It now recognizes
|
||||
that exact saved Ixian response while preserving quote/signature/force checks.
|
||||
Both claim/pass regressions fail before and pass after. Types/lint and70
|
||||
affected unit cases across9 files pass; six runtime cases cover claim/pass/four
|
||||
legal profiles and all three views. Styled390px Cancel→Commit→later Tooth
|
||||
acknowledgements→original Shield choice reaches phase7; actual counters remain
|
||||
board2Suboids/Tanks1Cyborg/reserves17, cost held by Richese, with no overflow.
|
||||
No skill/native effect, module gate, existing game/store or deployment changed.
|
||||
|
||||
**Semuta / explicit original Leader Skills — 11 October.** The initializer's
|
||||
optional second argument and `--profile semuta --leader-skills` now delegate
|
||||
original all14 native Basic Richese/CHOAM or Advanced E1/E2 training, retaining
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
# Dune implementation status
|
||||
|
||||
## 11 October 2026 — trained Ixian response retained across Semuta cost
|
||||
|
||||
The explicit skill entry makes the formerly unadmitted receipt path reachable.
|
||||
Actual Advanced native all14 setup, original posture/preparation, first loser
|
||||
acknowledgement and declared1/1 exchange precede printed rival Karama. Cost
|
||||
staging succeeded but every player projection failed: the skill-trained
|
||||
substitution validator recognized only live response/Worthless conversion,
|
||||
not the response held by `karamaBattleDiscard`.
|
||||
|
||||
The validator now recognizes that exact saved Ixian cancellation response.
|
||||
No quote/signature/counter check is weakened, and native cancellation still
|
||||
finishes once before the held used Tooth and optional Shield cleanup.
|
||||
Existing fixture entrypoints accept an optional skills argument with default
|
||||
unchanged; their original preparation consumes actual posture/Mentat controls.
|
||||
|
||||
Both consumer-visible claim/pass regressions fail before and pass after.
|
||||
Types/lint and70 unit cases across9 files pass. Six actual runtime cases
|
||||
cover claim/pass/four legal profiles, all three pause views, retired/repeated
|
||||
actions, physical inventory and one native canceled exchange. Styled real
|
||||
GameTable at390px completes Cancel with Karama→Commit→Tooth acknowledgements
|
||||
→original Shield choice. Force inspector shows board2Suboids, Tanks1Cyborg,
|
||||
reserves17; Richese retains acquired Karama at phase7 opening, no overflow.
|
||||
|
||||
This was a previously static warning made reproducible by the new skill
|
||||
composition, not a Basic/Advanced force-rule difference. Existing native
|
||||
handlers, gates and unrelated saves remain unchanged. Temporary probes,
|
||||
service and tab removed; existing games/storage/user PDF/deployment untouched.
|
||||
|
||||
## 11 October 2026 — explicit Semuta/original Leader Skills composition
|
||||
|
||||
`initializeSemutaGameForAudit(state,skills=false)` and the new scoped
|
||||
|
||||
@@ -770,3 +770,15 @@ acknowledgements→Shield cleanup→Collection. Native CHOAM's five eligible nor
|
||||
discs exclude Auditor; human training in all three seats reaches real Traitors.
|
||||
Observed stale skill guidance was corrected and its actual surface reinspected.
|
||||
This is bounded local evidence, not full skill/mode or deployed certification.
|
||||
|
||||
**11 October 2026 — trained Ixian saved response.** Actual skill-trained
|
||||
substitution cost views previously threw a lost-choice error because
|
||||
`ixSubstitutionIntegrity` ignored the detached response in `karamaBattleDiscard`.
|
||||
Recognizing that exact saved Ixian response retains its declared sources and
|
||||
recovery while Semuta owns controls. Original quote/signature/physical checks
|
||||
and one cancellation stay authoritative. Claim/pass do not resurrect the
|
||||
exchange; used Tooth and optional Shield cleanup still follow.
|
||||
Both regressions fail before and pass after; six runtime cases cover all
|
||||
views, claim/pass/four legal profiles and native remainder. Styled390px
|
||||
controls finish with board2Suboids/Tanks1Cyborg/reserves17 and Richese's acquired
|
||||
Karama, with no overflow. No additional skill/module or mode gate opens.
|
||||
+4
-2
@@ -22216,8 +22216,10 @@ function ixSubstitutionIntegrity(g: Game) {
|
||||
'The saved Ixian substitution changed its original battle losses, cards or physical counters.');
|
||||
const decisions = homeworldSavedDecisions(g);
|
||||
const selecting = decisions.filter(d => d.kind === 'ixSubstitution');
|
||||
const exchangeResponse = g.response?.kind === 'ixSubstitution' || g.pendingKarama?.use.kind === 'cancel' &&
|
||||
g.pendingKarama.use.response.kind === 'ixSubstitution';
|
||||
const continuation = g.pendingTreacheryDiscard?.continuation;
|
||||
const exchangeResponse = g.response?.kind === 'ixSubstitution' ||
|
||||
(g.pendingKarama?.use.kind === 'cancel' && g.pendingKarama.use.response.kind === 'ixSubstitution') ||
|
||||
(continuation?.kind === 'karamaBattleDiscard' && continuation.use.response.kind === 'ixSubstitution');
|
||||
requireRule(selecting.every(d => d.player === pending.player && d.territory === pending.territory &&
|
||||
JSON.stringify(d.losses) === JSON.stringify(pending.losses)) &&
|
||||
(exchangeResponse ? !!pending.sources && !!pending.recover : !pending.sources && !pending.recover) &&
|
||||
|
||||
@@ -26,12 +26,12 @@ export function semutaIxPhysical(game: Game) {
|
||||
}
|
||||
|
||||
/** Original native lobby, private Ix card choice, Traitors and actual first Storm/HMS placement. */
|
||||
export function initializeSemutaIxGame(advanced = false): Game {
|
||||
export function initializeSemutaIxGame(advanced = false, skills = false): Game {
|
||||
let game = createGame('SEMUTAIX', newPlayer('r', 'Richese', 'richese'), advanced, ['choam', 'ix']);
|
||||
joinGame(game, newPlayer('i', 'Ixians', 'ixians'));
|
||||
joinGame(game, newPlayer('e', 'Emperor', 'emperor'));
|
||||
for (const p of game.players) { p.bot = 'Medium'; p.ready = true; }
|
||||
game = initializeSemutaGameForAudit(game);
|
||||
game = initializeSemutaGameForAudit(game, skills);
|
||||
for (let step = 0; (game.status === 'setup' || game.phase === 1 || !game.mobileStronghold?.location) && step < 160; step++) {
|
||||
let next: Game | undefined;
|
||||
for (const p of game.players) {
|
||||
@@ -49,8 +49,8 @@ export function initializeSemutaIxGame(advanced = false): Game {
|
||||
}
|
||||
|
||||
/** Later rule position uses conserved native pieces, not a synthetic Game or replacement players. */
|
||||
export function semutaIxBattleFixture(advanced = false): SemutaIxFixture {
|
||||
const game = initializeSemutaIxGame(advanced);
|
||||
export function semutaIxBattleFixture(advanced = false, skills = false): SemutaIxFixture {
|
||||
const game = initializeSemutaIxGame(advanced, skills);
|
||||
const cards = semutaIxInventory(game);
|
||||
assert.equal(new Set(cards).size, cards.length);
|
||||
for (const p of game.players) {
|
||||
|
||||
@@ -24,8 +24,8 @@ export function semutaNativeKaramaAct(game: Game, step: SemutaNativeKaramaStep,
|
||||
}
|
||||
|
||||
/** Original lobby and conserved later battle; only the existing printed Karama changes hands. */
|
||||
export function semutaNativeIxKaramaFixture(advanced = false): SemutaNativeKaramaFixture {
|
||||
const ix = semutaIxBattleFixture(advanced);
|
||||
export function semutaNativeIxKaramaFixture(advanced = false, skills = false): SemutaNativeKaramaFixture {
|
||||
const ix = semutaIxBattleFixture(advanced, skills);
|
||||
const richese = semutaIxPlayer(ix.game, ix.holder);
|
||||
const index = richese.hand.findIndex(card => card.effect === 'karama');
|
||||
assert.ok(index >= 0);
|
||||
@@ -85,6 +85,10 @@ export function semutaNativeKaramaPreCost(f: SemutaNativeKaramaFixture, actions?
|
||||
step = { actor: game.battle.preparation.owner, action: { type: 'declineBattlePower' } };
|
||||
else if (game.decision?.kind === 'fullPlanOffer')
|
||||
step = { actor: game.decision.player, action: { type: 'decision', decline: true } };
|
||||
else if (game.decision?.kind === 'leaderSkillVisibility')
|
||||
step = { actor: game.decision.player, action: { type: 'leaderSkillVisibility', event: game.decision.event, hide: true } };
|
||||
else if (game.decision?.kind === 'mentatQuestion')
|
||||
step = { actor: game.decision.player, action: { type: 'decision', event: game.decision.event, decline: true } };
|
||||
else break;
|
||||
game = semutaNativeKaramaAct(game, step, actions);
|
||||
}
|
||||
|
||||
@@ -203,3 +203,29 @@ void test('All four profiles legally retire native printed costs and their origi
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const claim of [false, true]) void test(`Skill-trained Ix cancellation cost ${claim ? 'claim' : 'pass'} preserves the declared response while Semuta owns controls`, () => {
|
||||
const f = semutaNativeIxKaramaFixture(true, true); assert.ok(f.ix);
|
||||
const before = semutaNativeKaramaPreCost(f);
|
||||
assert.ok(before.pendingIxSubstitution?.receipt);
|
||||
const pending = semutaNativeKaramaCost(before, f);
|
||||
const event = pending.pendingTreacheryDiscard!.batch.event;
|
||||
assert.deepEqual(pending.pendingIxSubstitution!.sources, { [f.ix.location]: 1 });
|
||||
assert.deepEqual(pending.pendingIxSubstitution!.recover, { [f.ix.location]: 1 });
|
||||
for (const player of pending.players)
|
||||
assert.equal(viewGame(reload(pending), player.id).semutaReaction!.event, event);
|
||||
let game = semutaNativeKaramaReact(reload(pending), f, claim);
|
||||
assert.equal(game.pendingIxSubstitution, null);
|
||||
assert.equal(game.lastBattleContext!.ixSubstitution!.completed, true);
|
||||
assert.equal(game.pendingTreacheryDiscard!.continuation.kind, 'winnerMandatoryDiscard');
|
||||
game = semutaNativeKaramaRemainder(game, f);
|
||||
assert.equal(game.phase, 7);
|
||||
assert.equal(semutaIxPlayer(game, f.owner).forces[f.ix.location], 2);
|
||||
assert.equal(semutaIxPlayer(game, f.owner).elites!.forces[f.ix.location] ?? 0, 0);
|
||||
assert.equal(semutaIxPlayer(game, f.owner).elites!.tanks, 1);
|
||||
assert.equal(semutaIxPlayer(game, f.holder).hand.some(card => card.id === f.karama), claim);
|
||||
assert.deepEqual(semutaIxInventory(game), f.cards);
|
||||
for (const player of game.players) viewGame(reload(game), player.id);
|
||||
const cost = semutaNativeKaramaCostStep(f); rejected(game, cost.actor, cost.action);
|
||||
rejected(game, f.holder, { type: 'semutaCommit', event });
|
||||
});
|
||||
Reference in new issue
Block a user