feat: offer Semuta after paid battle Box searches

This commit is contained in:
Quintine committed 2026-10-12 01:44:21 +11:00
1 parent 066a097618
commit 6709522890
8 files changed
+116

No files matched your search

+7
View File
@@ -10,6 +10,13 @@ The `nullentropyDiscard` continuation records the search owner/event, activating
With one eligible target, staging and retirement occur in the original paid action without another acknowledgement. Multiple targets retain the existing owner-only paid decision. A restored Harkonnen bonus may legitimately refill the deck after the completed Box frame retires; that subsequent draw does not repeat the Box search.
11 October 2026 Semuta update: the explicit Richese preview offers the used Box
after a completed paid battle search, including automatic sole-target selection.
The recovered card, payment, shuffled pile and original battle already commit.
Claim or all-seat decline restores clean planning without another charge,
search or shuffle; a sealed opposing plan remains unchanged. Competing parents
retain their existing exclusions.
## Integrity and private information
Action, projection and automatic recovery validate the one public activating Box, exact former owner, canonical card identity, unique physical custody, top position, selected card in its owner's hand and the frozen final pile. Search identity and suspended controls are bound to the unchanged live pending obligations by a serialized parent signature. This detects changed saved records; it is not cryptographic authentication against wholesale rewriting of every receipt and signature.
+12
View File
@@ -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 / paid battle Box — 11 October.** A completed paid Nullentropy Box
search now offers only the used Box during battle, preserving the payment,
private acquired card, shuffled pile and opposing sealed plan. The regression
fails before and passes after; types/lint and 72 affected unit cases across seven
files pass. Twenty-four isolated runtime scenarios cover
Basic/Advanced, sole/multiple-target selection, claim/pass and all four legal AI
profiles; each uses the recovered weapon in a legal plan with one two-spice
payment. Competing parents remain separate. Temporary scripts are removed; no
saved store or deployed game was targeted. Existing human controls are reused;
no new browser evidence is claimed. See
[the source/runtime boundary](SEMUTA_SOURCE_UPDATE_20260907.md).
**Semuta / ordinary battle Ghola — 11 October.** A completed ordinary Ghola
now offers its consumed card during battle. Revival, the opposing sealed plan
and existing competing-parent boundaries are unchanged. The regression fails
+23
View File
@@ -1,5 +1,28 @@
# Dune implementation status
## 11 October 2026 — Semuta after a paid battle Box search
The completed `nullentropyDiscard` receipt already binds the acquired card,
ordered shuffled pile and original battle. The Semuta battle-presence guard now
admits that receipt without changing search/payment rules or existing
competing-parent exclusions. Only the used Box is fresh; claiming it does not
claim the searched card or replay the transaction.
The permanent regression fails before and passes after, covering claim/pass,
exact two-spice payment, one recovered weapon, unchanged opposing sealed plan,
discard order and card conservation. Retired reaction and paid-search replay
reject. Twenty-four isolated runtime scenarios cover both rule modes,
automatic sole-target or explicit multiple-target selection, claim/pass and
all four legal AI profiles. Each uses the recovered weapon in the next legal
plan, preserving the original opposing plan and exactly one payment.
Types/lint and 72 affected unit cases across seven files pass.
Temporary scripts are removed; no saved store or deployed game was targeted.
Existing human Box, Semuta and planning controls are reused; this is engine
runtime proof, not new browser/deployed acceptance. Full Semuta remains open.
Contracts: [Semuta](SEMUTA_SOURCE_UPDATE_20260907.md),
[paid search continuation](BOX_DISCARD_CONTINUATIONS.md).
## 11 October 2026 — Semuta after ordinary battle Ghola revival
The completed ordinary-card receipt already binds Ghola revival and the original
+5
View File
@@ -39,6 +39,11 @@ original battle, including a sealed opposing plan. Claim or decline restores
planning without reviving again or changing that plan. This does not admit
competing parents or market-specific Ghola interruptions.
Completed paid Box disposal may now offer Semuta during battle as well. Only the
used Box is fresh; payment, private acquisition, shuffled pile and original
battle remain committed. Claim or decline restores clean planning without
repeating the search, shuffle or payment. Competing parents remain separate.
Clean Once Around Sapho stages `saphoAuctionDiscard` after the open
lot's finite bidder reorder, hand-slot release and public card disposal,
but before settlement. The saved receipt binds the lot, prior funding,
+18
View File
@@ -245,6 +245,24 @@ separate [reaction privacy audit](SEMUTA_PRIVACY_REVIEW.md) explains why
auto-closing only when nobody secretly holds the card would leak
possession. The main game and public expansion gates remain closed.
**11 October 2026 — paid battle Box.** A completed paid Nullentropy Box
search now offers the used Box during battle. Only the battle-presence guard
gained that receipt exception; payment, private acquisition, shuffled pile and
original battle already commit and remain unchanged. Claim or all-seat decline
restores clean planning without another search, shuffle or payment. Competing
parents retain their existing exclusions.
The permanent regression fails before and passes after, covering claim/pass,
exact two-spice payment, one recovered weapon, unchanged opposing sealed plan,
discard order, card conservation and retired-reaction/search replay rejection.
Twenty-four isolated engine scenarios cover Basic/Advanced, automatic sole-target
or explicit multiple-target selection, claim/pass and all four legal AI profiles.
Each commits the recovered weapon in the next legal plan while preserving the
original opposing plan and exactly one payment. Temporary scripts are removed;
no saved store or deployed game was targeted. Existing human search, Semuta and
plan panels are reused; no new browser evidence is claimed at this checkpoint.
Types/lint and 72 affected unit cases across seven files pass.
**11 October 2026 — ordinary battle Ghola.** The consumed Ghola now receives
the same Semuta offer during battle after its revival commits. Only the
battle-presence guard gained the completed ordinary Ghola exception; revival,
+1
View File
@@ -6765,6 +6765,7 @@ function semutaOfferSupported(
(continuation.kind === 'saphoAggressorDiscard' ||
continuation.kind === 'residualPoisonDiscard' ||
continuation.kind === 'truthtranceDiscard' ||
continuation.kind === 'nullentropyDiscard' ||
(continuation.kind === 'ordinaryCardDiscard' && continuation.effect === 'ghola') ||
!g.battle) &&
(continuation.kind !== 'choamKulonDiscard' ||
+1
View File
@@ -1174,6 +1174,7 @@ export const RULE_TOPICS: RuleTopic[] = [
...(definition.card.effect === 'semutaDrug' ? [
'Mandatory and winner battle discards also pause before native Advanced Harkonnen capture. Claim or Continue completes the original casualties and card cleanup before the capture offer; it does not sample a captive early or repeat capture. Other pending rewards remain outside this bounded path.',
'An ordinary Ghola played during battle also offers its consumed card after revival. Claim or Continue preserves the revived leader or forces and the opposing sealed plan, then normal planning resumes. It cannot revive again by resuming the receipt.',
'A completed paid Nullentropy Box search during battle offers only the used Box. Claim or Continue preserves the two-spice payment, private recovered card, already-shuffled pile and sealed opposing plan. The recovered card may be used in normal legal planning; the search is not repeated.',
] : []),
],
...(definition.card.effect === 'semutaDrug'
+49
View File
@@ -488,6 +488,55 @@ void test('a completed paid Box discard can be claimed before resumption without
assert.deepEqual(normalizeAutomaticGame(reload(claimed)), reload(claimed));
});
void test('Semuta claims a battle Box after its paid search without changing the recovered weapon or sealed plan', () => {
const { g, semuta, hajr } = fixture();
player(g, 'a').hand = [];
g.deck.push(hajr);
const box = take(g, 'richese-nullentropy-box');
const weapon = take(g, g.deck.find(c => c.kind === 'projectile')!.id);
player(g, 'a').hand.push(box);
g.discard.push(weapon, g.deck.shift()!);
Object.assign(g, { phase: 6, active: 'a', order: ['a', 'e', 'r'],
response: null, decision: null, phaseOpening: null, movementRemaining: [] });
for (const id of ['a', 'e']) {
player(g, id).forces = { 'arrakeen:10': 4 };
player(g, id).reserves = 16;
}
const original = inventory(g);
const spice = player(g, 'a').spice;
let state = applyAction(g, 'a', { type: 'chooseBattle', territory: 'arrakeen', target: 'e' });
while (state.battle?.preparation)
state = applyAction(state, state.battle.preparation.owner, { type: 'declineBattlePower' });
for (const id of ['a', 'e'])
state = applyAction(state, id, { type: 'battlePreparationReady', event: state.battle!.preLeader!.event });
state = applyAction(state, 'e', { type: 'battlePlan', dial: 0, support: 0,
leader: player(state, 'e').leaders[0].id });
const paid = applyAction(state, 'a', { type: 'card', card: box.id });
const search = paid.pendingNullentropy!.event;
const pending = applyAction(paid, 'a', { type: 'decision', event: search, card: weapon.id });
assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer');
const event = pending.pendingTreacheryDiscard!.batch.event;
const shuffled = pending.discard.filter(c => c.id !== box.id).map(c => c.id);
let declined = reload(pending);
for (const id of ['a', 'e', 'r']) declined = applyAction(declined, id, { type: 'semutaPass', event });
const claimed = applyAction(reload(pending), 'r', { type: 'semutaCommit', event });
for (const resumed of [declined, claimed]) {
assert.equal(player(resumed, 'a').spice, spice - 2);
assert.equal(player(resumed, 'a').hand.filter(c => c.id === weapon.id).length, 1);
assert.deepEqual(resumed.battle, state.battle);
assert.deepEqual(resumed.discard.filter(c => c.id !== box.id && c.id !== semuta.id).map(c => c.id), shuffled);
assert.deepEqual(inventory(resumed), original);
assert.throws(() => applyAction(resumed, 'r', { type: 'semutaCommit', event }));
assert.throws(() => applyAction(resumed, 'a', { type: 'decision', event: search, card: weapon.id }));
const planned = applyAction(resumed, 'a', { type: 'battlePlan', dial: 1, support: 0,
leader: player(resumed, 'a').leaders[0].id, weapon: weapon.id });
assert.equal(planned.battle!.plans.a.weapon, weapon.id);
assert.deepEqual(planned.battle!.plans.e, state.battle!.plans.e);
}
assert.equal(player(claimed, 'r').hand.some(c => c.id === box.id), true);
assert.equal(claimed.discard.some(c => c.id === semuta.id), true);
});
void test('a retired Ornithopter can be claimed after one flight group without replaying movement', () => {
const { g, semuta, hajr } = fixture();
player(g, 'a').hand = [];