feat: offer Semuta after clean battle Distrans transfers

This commit is contained in:
Quintine committed 2026-10-12 02:08:24 +11:00
1 parent 235f763625
commit d319faa80c
8 files changed
+113 -1

No files matched your search

+13
View File
@@ -29,6 +29,19 @@ 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 / clean battle Distrans — 11 October.** The completed clean transfer
now offers only its public used Distrans during battle. The private gift and
sealed opposing plan remain committed; the recipient may itself claim with
Semuta without losing or repeating the gift. The original regression fails
before and passes after; the final cases cover both separate and claimant
recipients. Types/lint and 69 affected unit cases across seven files pass.
Twenty-four runtime scenarios cover both rule modes, recipient
roles, claim/pass and all four legal AI profiles, followed by legal planning.
Competing parents remain separate. Temporary scripts are removed; no saved
store or deployed game was targeted. Existing human panels are reused; no new
browser evidence is claimed. See
[the source/runtime boundary](SEMUTA_SOURCE_UPDATE_20260907.md).
**Semuta / native Basic Terror — 11 October.** The local `semuta` entry now
admits fresh Basic native Richese/Moritani plus classics with CHOAM/Ecaz decks
and no optional modules. Private Sabotage and Robbery overflow after clean
+25
View File
@@ -1,5 +1,30 @@
# Dune implementation status
## 11 October 2026 — Semuta after a clean battle Distrans transfer
The completed `distransDiscard` receipt already binds the donor/recipient hand
identities and original battle parent. The reaction guard now admits it during
battle without changing transfer legality or competing-parent exclusions. Only
the public used Distrans is fresh; the private gift is already held and is not
a Semuta target.
The original regression fails before and passes after. The final two cases
cover a separate recipient and the Semuta claimant as recipient, preserving one
gift, card conservation and the original sealed plan. Original transfer and
retired reaction replay reject. A recipient combatant may commit the given
weapon in its next legal plan; a claimant recipient retains both its gift and
the claimed used Distrans.
Types/lint and 69 affected unit cases across seven files pass.
Twenty-four isolated engine scenarios cover Basic/Advanced, both recipient
roles, claim/pass and all four legal AI profiles, then legal planning. Each
retains the original opposing plan and one exact transfer. Temporary scripts
are removed; no saved store or deployed game was targeted. Existing transfer,
Semuta and plan controls are reused; no new browser evidence is claimed.
Full Semuta and combinations remain open. Contracts:
[Semuta](SEMUTA_SOURCE_UPDATE_20260907.md),
[printed transfer and commitments](RICHESE_DISTRANS_ENGINE_AUDIT.md).
## 11 October 2026 — native Basic Semuta/Terror continuation
The local `semuta` initializer now additionally admits Basic native
+1 -1
View File
@@ -30,7 +30,7 @@ The distinction between an ordinary Treachery effect and a cancellable faction/a
1. **Giving the activating Distrans itself.** The give-then-discard wording does not explain self-selection. Do not put the same physical card in both the recipient's hand and the discard, discard a different card, or silently omit the discard. Until clarified, require a separate transferred card and label self-selection unsupported, not a printed prohibition.
2. **Exact scope of “during a bid.”** The face does not define whether this means one bid declaration or the complete auction of a card. It does not say the entire Bidding phase. A temporary guard over unresolved normal/Once Around/Silent lots, including unsettled payment, is an explicit conservative implementation boundary. Pre-bid declaration/selection, between-lot windows and post-purchase income are distinct contexts; do not silently present a blanket phase-3 ban as the official rule. The user has already been asked about this boundary by root; this audit adds no question or assumed answer.
3. **Active Truthtrance answer priority.** Existing question/priority resolution prevents changing the queried hand mid-question. No retrieved Distrans-specific rule overrides it. Keep this narrow existing priority boundary, rather than disabling Distrans for every unrelated decision or response.
4. **Future Semuta discard reaction.** Discarding Distrans creates a real discard event. Semuta may eventually react to that card; the transferred card was not discarded and is not a Semuta target. Do not replay the transfer when resuming a future discard reaction. The exact combined Semuta capacity/counter sequence remains in its own unfinished subsystem.
4. **Semuta discard reaction — updated 11 October 2026.** The explicit development preview reacts after a clean completed transfer, including during battle. Only the used Distrans is fresh; the privately transferred card is never a Semuta target. Claim or decline cannot repeat the transfer or change a sealed plan. The recipient may itself hold Semuta and claim the used card while keeping the earlier gift. Competing parents retain their separate boundaries; the neutral response and approved full-hand exchange are documented in [the Semuta source/runtime contract](SEMUTA_SOURCE_UPDATE_20260907.md).
A setup-only prohibition is also not printed on the face. Current supported setup normally has no reachable Distrans in a hand; if a new setup module makes that reachable, verify that boundary rather than inferring a general rule from current implementation gates.
+6
View File
@@ -14,6 +14,12 @@ gifts and competing parents retain the original direct transfer
behavior without this Semuta reaction. The table below remains the
broader producer/continuation inventory, not a claim of complete coverage.
11 October 2026 update: the same clean receipt is admitted during battle.
Only the public used Distrans is fresh; the private gift and sealed plans remain
committed. Claim or decline restores clean planning without another transfer.
The recipient may claim with its own held Semuta while keeping the earlier
gift. Competing parents retain their separate boundaries.
A clean ordinary definite Truthtrance answer now offers its consumed card even
when another question remains reserved, when the answer bound a supported
shipment or unsubmitted battle-plan promise, or when it concerns a submitted/
+19
View File
@@ -247,6 +247,25 @@ 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 — clean battle Distrans.** A completed clean private
Distrans transfer now offers its public used card during battle. Only the
battle-presence guard gained that existing receipt exception; transfer legality,
sealed plans and competing-parent exclusions are unchanged. The privately given
card is already held by the recipient and never becomes a fresh discard target.
A recipient holding Semuta may claim the used Distrans while keeping the gift.
The original permanent case fails before and passes after; the final regression
also covers the claimant being the recipient. Claim/pass preserves the single
gift, card conservation and original sealed opposing plan. Retired reaction and
original transfer replays reject; the recipient combatant may use the given
weapon in its next legal plan. Twenty-four isolated engine scenarios cover
Basic/Advanced, a separate recipient or the Semuta holder, claim/pass and all
four legal AI profiles. Each resumes legal planning with the same original
opposing plan and one exact gift. Temporary scripts are removed; no saved store
or deployed game was targeted. Existing human transfer, Semuta and planning
controls are reused; no new browser evidence is claimed here.
Types/lint and 69 affected unit cases across seven files pass.
**11 October 2026 — native Basic Terror discards.** The same local `semuta`
entry additionally admits fresh Basic native Richese/Moritani with classic
opponents, exactly CHOAM/Ecaz decks and no optional modules. Native setup remains
+1
View File
@@ -6774,6 +6774,7 @@ function semutaOfferSupported(
continuation.kind === 'residualPoisonDiscard' ||
continuation.kind === 'truthtranceDiscard' ||
continuation.kind === 'nullentropyDiscard' ||
continuation.kind === 'distransDiscard' ||
(continuation.kind === 'ordinaryCardDiscard' && continuation.effect === 'ghola') ||
!g.battle) &&
(continuation.kind !== 'choamKulonDiscard' ||
+1
View File
@@ -1175,6 +1175,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'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.',
'A clean Distrans transfer during battle also offers only the used Distrans. The private card has already reached its recipient; claim or Continue cannot repeat the gift or change a sealed plan. A recipient holding Semuta may claim the used Distrans and keep the already-transferred card.',
'The same local semuta entry also admits Basic native Richese and Moritani with classic opponents, exactly CHOAM/Ecaz decks and no optional modules. A clean shipment/movement Sabotage or Robbery overflow offers its private fresh discard without showing the face before commitment. Claim or Continue preserves the original victim/draw, removed token, shipment payment and forces; Sabotage may then offer its original optional gift. Advanced Moritani, native Ecaz, Ix/E3 mixtures and worm/Ambassador/overlap arrivals remain outside this bounded entry. The table visibly labels the development preview.',
] : []),
],
+47
View File
@@ -695,6 +695,53 @@ void test('final Ornithopter discard resolves Semuta before the original Bene Ge
assert.equal(claimed.discard.some(card => card.id === semuta.id), true);
});
for (const recipient of ['a', 'r'] as const) void test(
`Semuta claims battle Distrans to ${recipient} without repeating its private transfer or changing a sealed plan`, () => {
const { g, semuta, hajr } = fixture();
player(g, 'a').hand = [];
g.deck.push(hajr);
const distrans = take(g, 'richese-distrans');
const weapon = take(g, g.deck.find(c => c.kind === 'projectile')!.id);
player(g, 'e').hand.push(distrans, weapon);
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);
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 transfer = { type: 'card', card: distrans.id, target: recipient, give: weapon.id };
const pending = applyAction(state, 'e', transfer);
assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer');
assert.deepEqual(pending.pendingTreacheryDiscard!.batch.entries.map(entry => entry.card.id), [distrans.id]);
const event = pending.pendingTreacheryDiscard!.batch.event;
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, recipient).hand.filter(c => c.id === weapon.id).length, 1);
assert.equal(player(resumed, 'e').hand.some(c => c.id === weapon.id || c.id === distrans.id), false);
assert.deepEqual(resumed.battle, state.battle);
assert.deepEqual(inventory(resumed), original);
assert.throws(() => applyAction(resumed, 'r', { type: 'semutaCommit', event }));
assert.throws(() => applyAction(resumed, 'e', transfer));
const planned = applyAction(resumed, 'a', { type: 'battlePlan', dial: 1, support: 0,
leader: player(resumed, 'a').leaders[0].id, ...(recipient === 'a' ? { weapon: weapon.id } : {}) });
if (recipient === 'a') 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 === distrans.id), true);
assert.equal(player(claimed, 'r').hand.some(c => c.id === weapon.id), recipient === 'r');
assert.equal(claimed.discard.some(c => c.id === semuta.id), true);
});
void test('a completed private Distrans transfer offers only its public used card', () => {
const { g, semuta, hajr } = fixture();
const distrans = take(g, 'richese-distrans');