diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 20c473e6..a72522a1 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -29,6 +29,13 @@ 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 / battle-plan promise — 11 October.** An unsubmitted battle-plan +Truthtrance answer now offers the consumed card too; the battle promise and its +legality enforcement are unchanged, and submitted/revealed plans stay automatic. +Types/lint pass and 126 affected cases pass after adding the regression and a +corrected readiness step in its fixture. No saved store or deployed game was +targeted. See [the source/runtime boundary](SEMUTA_SOURCE_UPDATE_20260907.md). + **Semuta / Truthtrance shipment promise — 11 October.** A classic payer's ordinary Truthtrance shipment answer in the Richese Semuta profile now offers the consumed card; the binding promise and its enforcement are unchanged. The structured diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index ac93f720..6d7ce4f1 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,26 @@ # Dune implementation status +## 11 October 2026 — Semuta with a bound battle-plan promise + +The Truthtrance receipt already carried a battle promise and the integrity +validator already enforced it. The Semuta admission excluded every promise kind +other than none, and a separate guard refused any reaction while a battle existed. +Admitting `kind === 'battle'` and exempting `truthtranceDiscard` from the +battle-presence guard lets an unsubmitted battle-plan answer offer its consumed +card without touching the promise or its enforcement. Submitted and revealed plans +bind no new promise and remain automatic. + +The regression fails before (no offer) and passes after, covering claim and +decline, the surviving promise, an illegal-dial rejection, the legal dial, public +history and inventory conservation. Types/lint pass and the 126-case affected +union passes. The fixture needed both combatants marked ready in the shared +pre-leader phase before a plan can be committed; that is existing battle +preparation, not a change to it. + +This is local changed-path proof, not complete Semuta or deployed acceptance. +Contracts: [source boundary](SEMUTA_SOURCE_UPDATE_20260907.md), +[discard continuations](SEMUTA_DISCARD_CONTINUATIONS.md). + ## 11 October 2026 — Semuta with a bound Truthtrance shipment promise The typed `truthtranceDiscard` receipt already carried a bound promise and the diff --git a/docs/SEMUTA_DISCARD_CONTINUATIONS.md b/docs/SEMUTA_DISCARD_CONTINUATIONS.md index 49875ec5..6a6f09a5 100644 --- a/docs/SEMUTA_DISCARD_CONTINUATIONS.md +++ b/docs/SEMUTA_DISCARD_CONTINUATIONS.md @@ -16,10 +16,11 @@ broader producer/continuation inventory, not a claim of complete coverage. A clean definite Truthtrance answer now offers its consumed card even when another question remains reserved, and also when the answer bound a shipment promise in -the supported Richese Semuta profile. Claim or all-seat decline leaves public -history, the consumed card's custody and any bound promise settled, then restores -the same remaining queue at `ask` once. Shrine-source, battle-plan and unsupported -native No-Field shipment answers retain their automatic continuation. +the supported Richese Semuta profile or an unsubmitted battle-plan promise. Claim +or all-seat decline leaves public history, the consumed card's custody and any +bound promise settled, then restores the same remaining queue at `ask` once. +Shrine-source, submitted/revealed battle-plan and unsupported native No-Field +shipment answers retain their automatic continuation. The same preview also pauses a final definite Truthtrance after its public answer, history and physical card disposal are committed. An diff --git a/docs/SEMUTA_SOURCE_UPDATE_20260907.md b/docs/SEMUTA_SOURCE_UPDATE_20260907.md index b1b70595..9c1e052f 100644 --- a/docs/SEMUTA_SOURCE_UPDATE_20260907.md +++ b/docs/SEMUTA_SOURCE_UPDATE_20260907.md @@ -234,7 +234,7 @@ policy**, not a newly discovered publisher timing rule. Full-hand Semuta uses the explicit user-approved atomic exchange; its completed hand count is unchanged. Pending gifts, competing responses, completed Ornithopter arrivals with Ecaz Ambassador or Moritani Terror, -battle-plan Truthtrance answers, pending battle rewards and other +pending battle rewards and other typed producers retain their automatic continuation; Semuta cannot claim those events yet. The completed Box's selected @@ -245,6 +245,17 @@ 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 — battle-plan promise answer.** An unsubmitted battle-plan +Truthtrance answer now offers the consumed card too. Only the promise-kind +admission and the battle-presence exemption changed; the battle promise and its +legality enforcement are untouched, and a submitted or revealed plan still binds +no new promise and stays automatic. Claim or all-seat decline leaves the plan +binding enforceable. + +The permanent regression fails before and passes after; the affected union passes +126 cases and typecheck/lint pass. This is local changed-path evidence, not +complete Semuta or deployed acceptance. + **11 October 2026 — shipment-promise answer.** A classic payer's ordinary Truthtrance shipment answer in the Richese Semuta profile now offers the consumed card. The binding promise, public history and enforcement are unchanged; claim or diff --git a/game/engine.ts b/game/engine.ts index f3d228c9..9d18e763 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -6678,7 +6678,9 @@ function semutaOfferSupported( !g.pendingArrivalOverlap))) && !(continuation.kind === 'truthtranceDiscard' && continuation.consumed.source === undefined && - (continuation.promise === null || continuation.promise.kind === 'shipment'))) return false; + (continuation.promise === null || + continuation.promise.kind === 'shipment' || + continuation.promise.kind === 'battle'))) return false; if (continuation.kind === 'battleResolved' && !g.lastBattleContext?.mandatoryDiscard) return false; if ((continuation.kind === 'battleCleanup' || @@ -6761,7 +6763,8 @@ function semutaOfferSupported( (continuation.kind === 'saphoAuctionDiscard' || continuation.kind === 'ixAllyCard' || !g.richeseAuction) && (continuation.kind === 'saphoAggressorDiscard' || - continuation.kind === 'residualPoisonDiscard' || !g.battle) && + continuation.kind === 'residualPoisonDiscard' || + continuation.kind === 'truthtranceDiscard' || !g.battle) && (continuation.kind !== 'choamKulonDiscard' || (!g.ornithopter && !g.pendingChoamMove && !g.summonedWorm && g.wormRides.length === 0)) && diff --git a/game/reference.ts b/game/reference.ts index 8284fd5a..1dace050 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -1169,7 +1169,7 @@ export const RULE_TOPICS: RuleTopic[] = [ : definition.card.effect === 'portableSnooper' ? 'Choose Portable Snooper as your ordinary poison defense, or use the late-defense panel after reveal before your own traitor decision. It uses ordinary winner retention and cannot stop Poison Tooth.' : definition.card.effect === 'semutaDrug' - ? 'In the explicit Richese development preview, every seat sees a neutral response to supported clean discards. A CHOAM & Richese game may include the Ix deck: Thumper pauses before worm/spice draw; Amal pauses after halving spice before opening resumes. Other public sources include ordinary cards, paid Box, Ornithopter, Distrans, final Truthtrance including a queued ordinary answer or a supported shipment promise, four clean Sapho scopes, pre-leader Residual Poison, printed Karama canceling clean CHOAM Charity, Inflation or Bene Gesserit Charity, a clean printed Karama normal-auction purchase or winning-bid payment before lot settlement, a completed CHOAM card sale after its bank income, clean Kulon Worthless disposal before its movement bonus, proactive La La La disposal before blocking free revival, proactive Baliset before restricting movement, proactive ordinary Trip to Gamont before returning a force, accepted Jubba Cloak before storm protection, and three battle batches. A private normal-auction or Black Market Ixian ally replacement also pauses before drawing, without showing its face until a holder commits; original payment and seller income settle once. Cache purchases cannot use native Ixian allied replacement. Ordinary definite Truthtrance answers pause even when another question stays reserved or the answer bound a supported shipment promise; battle-plan answers do not. Under the user-selected exchange ruling, Commit may replace Semuta even at your faction hand limit: acquisition and Semuta disposal settle atomically, without increasing your completed hand. Reserved incoming cards still need room. For multiple eligible cards, inspect and select exactly one after commitment. Saved effects resume once. Other private discards, Karama uses, reactive Worthless uses except printed Jubba, elite or concealed Trip returns, inspected/partly ready battles, nested responses, pending rewards and public starts remain unfinished.' + ? 'In the explicit Richese development preview, every seat sees a neutral response to supported clean discards. A CHOAM & Richese game may include the Ix deck: Thumper pauses before worm/spice draw; Amal pauses after halving spice before opening resumes. Other public sources include ordinary cards, paid Box, Ornithopter, Distrans, final Truthtrance including a queued ordinary answer, a supported shipment promise or an unsubmitted battle-plan promise, four clean Sapho scopes, pre-leader Residual Poison, printed Karama canceling clean CHOAM Charity, Inflation or Bene Gesserit Charity, a clean printed Karama normal-auction purchase or winning-bid payment before lot settlement, a completed CHOAM card sale after its bank income, clean Kulon Worthless disposal before its movement bonus, proactive La La La disposal before blocking free revival, proactive Baliset before restricting movement, proactive ordinary Trip to Gamont before returning a force, accepted Jubba Cloak before storm protection, and three battle batches. A private normal-auction or Black Market Ixian ally replacement also pauses before drawing, without showing its face until a holder commits; original payment and seller income settle once. Cache purchases cannot use native Ixian allied replacement. Ordinary definite Truthtrance answers pause even when another question stays reserved or the answer bound a supported shipment or unsubmitted battle-plan promise. Under the user-selected exchange ruling, Commit may replace Semuta even at your faction hand limit: acquisition and Semuta disposal settle atomically, without increasing your completed hand. Reserved incoming cards still need room. For multiple eligible cards, inspect and select exactly one after commitment. Saved effects resume once. Other private discards, Karama uses, reactive Worthless uses except printed Jubba, elite or concealed Trip returns, inspected/partly ready battles, nested responses, pending rewards and public starts remain unfinished.' : 'Reference component only: this Richese card’s game actions are not enabled.', ], ...(definition.card.effect === 'semutaDrug' @@ -1179,7 +1179,7 @@ export const RULE_TOPICS: RuleTopic[] = [ area: 'Implementation' as const, status: 'Partial' as const, detail: - 'The Richese preview interrupts clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, four Sapho scopes, pre-leader Residual Poison, printed Karama canceling clean CHOAM Charity, Inflation or Bene Gesserit Charity or settling a normal auction purchase or winning bid, a completed CHOAM sale, clean Kulon and proactive La La La/Baliset/ordinary Trip to Gamont Worthless use, accepted printed Jubba before storm protection, Ix-deck Thumper/Amal and three battle discard stages. A private normal-auction or Black Market Ixian ally replacement pauses before its draw with a neutral, face-hidden offer; original payment and seller income settle once. Cache purchases retain their native allied-replacement prohibition. Ordinary definite Truthtrance answers pause whether or not another question remains reserved, and also when the answer bound a supported shipment promise; battle-plan answers stay automatic. CHOAM sale income is committed before its neutral offer and the same market resumes once; Kulon adds one movement range, La La La blocks one target’s free revival, Baliset restricts the chosen player’s movement, ordinary Trip returns one selected force, and Jubba protects one CHOAM territory only after their offers. Karama’s physical cost is discarded before its neutral offer; the original response is canceled or the reserved lot settles once. Residual Poison commits one random leader death; Sapho commits its finite priority. Thumper waits before worm/spice draw; Amal restores an already-halved opening. Owner-only candidates and one physical Semuta transfer retain privacy. The user-approved full-hand exchange preserves final hand count and incoming reservations. Other Karama uses, other reactive Worthless uses, private discards, nested responses and pending rewards remain unfinished.', + 'The Richese preview interrupts clean ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, four Sapho scopes, pre-leader Residual Poison, printed Karama canceling clean CHOAM Charity, Inflation or Bene Gesserit Charity or settling a normal auction purchase or winning bid, a completed CHOAM sale, clean Kulon and proactive La La La/Baliset/ordinary Trip to Gamont Worthless use, accepted printed Jubba before storm protection, Ix-deck Thumper/Amal and three battle discard stages. A private normal-auction or Black Market Ixian ally replacement pauses before its draw with a neutral, face-hidden offer; original payment and seller income settle once. Cache purchases retain their native allied-replacement prohibition. Ordinary definite Truthtrance answers pause whether or not another question remains reserved, and also when the answer bound a supported shipment or unsubmitted battle-plan promise. CHOAM sale income is committed before its neutral offer and the same market resumes once; Kulon adds one movement range, La La La blocks one target’s free revival, Baliset restricts the chosen player’s movement, ordinary Trip returns one selected force, and Jubba protects one CHOAM territory only after their offers. Karama’s physical cost is discarded before its neutral offer; the original response is canceled or the reserved lot settles once. Residual Poison commits one random leader death; Sapho commits its finite priority. Thumper waits before worm/spice draw; Amal restores an already-halved opening. Owner-only candidates and one physical Semuta transfer retain privacy. The user-approved full-hand exchange preserves final hand count and incoming reservations. Other Karama uses, other reactive Worthless uses, private discards, nested responses and pending rewards remain unfinished.', }, { area: 'Player controls' as const, @@ -1197,7 +1197,7 @@ export const RULE_TOPICS: RuleTopic[] = [ area: 'Documentation' as const, status: 'Partial' as const, detail: - 'The guide distinguishes clean public ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance (including a queued ordinary answer or a supported shipment promise), four Sapho scopes, pre-leader Residual Poison, printed Karama cancellation of CHOAM Charity, Inflation or Bene Gesserit Charity or clean normal-auction purchase or winning-bid payment, completed CHOAM card sales, clean Kulon/proactive La La La/Baliset/ordinary Trip to Gamont Worthless and accepted printed Jubba Cloak, Ix-deck Thumper/Amal and three battle batches from a private normal-auction or Black Market Ixian ally replacement, the cache replacement prohibition, privacy-neutral acknowledgements, the user-approved atomic full-hand exchange and remaining discard sources.', + 'The guide distinguishes clean public ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance (including a queued ordinary answer, a supported shipment promise or an unsubmitted battle-plan promise), four Sapho scopes, pre-leader Residual Poison, printed Karama cancellation of CHOAM Charity, Inflation or Bene Gesserit Charity or clean normal-auction purchase or winning-bid payment, completed CHOAM card sales, clean Kulon/proactive La La La/Baliset/ordinary Trip to Gamont Worthless and accepted printed Jubba Cloak, Ix-deck Thumper/Amal and three battle batches from a private normal-auction or Black Market Ixian ally replacement, the cache replacement prohibition, privacy-neutral acknowledgements, the user-approved atomic full-hand exchange and remaining discard sources.', }, { area: 'Verification' as const, diff --git a/tests/semuta-engine.test.ts b/tests/semuta-engine.test.ts index 7e932172..c46353b1 100644 --- a/tests/semuta-engine.test.ts +++ b/tests/semuta-engine.test.ts @@ -784,6 +784,58 @@ for (const answer of ['yes', 'no'] as const) void test( assert.equal(claimed.discard.some(card => card.id === semuta.id), true); }); +void test('Semuta preserves an unsubmitted battle-plan promise through claim and decline', () => { + const { g, semuta, hajr } = fixture(); + player(g, 'a').hand = []; + g.deck.push(hajr); + const truth = take(g, 'truthtrance'); + player(g, 'a').hand.push(truth); + Object.assign(g, { phase: 6, active: 'a', order: ['a', 'e', 'r'], + decision: null, response: 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' }); + while (state.battle?.preLeader && !state.battle.preLeader.closed) + for (const id of ['a', 'e']) + if (!state.battle!.preLeader!.ready.includes(id)) + state = applyAction(state, id, { type: 'battlePreparationReady', event: state.battle!.preLeader!.event }); + state = applyAction(state, 'a', { type: 'card', card: truth.id }); + while (state.truthtrance?.stage === 'priority') { + const actor = state.players.find(p => !state.truthtrance!.passed.includes(p.id))!; + state = applyAction(state, actor.id, { type: 'truthPass' }); + } + state = applyAction(state, 'a', { type: 'truthAsk', + question: { kind: 'battlePlan', target: 'e', territory: 'arrakeen', + claim: { kind: 'dial', compare: 'gte', value: 2 } } }); + const pending = applyAction(state, 'e', { type: 'truthAnswer', answer: 'yes' }); + assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer'); + assert.equal(pending.battle?.truthPromises?.length, 1); + 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 }); + const plan = (dial: number) => ({ type: 'battlePlan' as const, dial, support: 0, + leader: player(pending, 'e').leaders[0].id }); + for (const resumed of [declined, claimed]) { + assert.equal(resumed.pendingTreacheryDiscard, null); + assert.deepEqual(resumed.battle, pending.battle); + assert.deepEqual(resumed.truthHistory, pending.truthHistory); + assert.throws(() => applyAction(resumed, 'e', plan(0))); + const planned = applyAction(resumed, 'e', plan(2)); + assert.equal(planned.battle!.plans.e.dial, 2); + assert.equal(planned.battle!.truthPromises!.length, 1); + assert.equal(planned.battle!.truthPromises![0].released ?? false, false); + assert.deepEqual(inventory(planned), original); + } + assert.equal(player(claimed, 'r').hand.some(card => card.id === truth.id), true); + assert.equal(claimed.discard.some(card => card.id === semuta.id), true); +}); + void test('a completed Sapho first-movement reorder offers its public card before changing the active turn', () => { const { g, semuta, hajr } = fixture(); player(g, 'a').hand = [];