Files
Dune/docs/SEMUTA_DISCARD_CONTINUATIONS.md
T

45 KiB
Raw Blame History

Semuta discard producers and continuation inventory

Read-only engine audit, 2026-09-07. Read the complete docs/SEMUTA_DRUG_ENGINE_AUDIT.md, then inspected every current discard(...) call, direct Treachery-discard mutation and draw(g) caller in game/engine.ts, plus the Truthtrance discard callback and pure Box/Distrans results. Names below are integration anchors; approximate current lines are navigation aids only. This report adopts no additional source ruling and changes no project file.

Update: the exchange producers, three battle producers, Sabotage, Robbery overflow and completed Nullentropy Box searches, Ornithopter escrow retirement, definite Truthtrance answers and the five ordinary completed-card effects below now have production automatic continuation frames; see exchange checkpoint, battle checkpoint, forced Terror checkpoint, Box checkpoint, Ornithopter checkpoint, Truthtrance checkpoint and ordinary-card checkpoint. The remaining table is an implementation inventory, not a claim that Semuta is active.

30 September 2026 checkpoint: the explicit Semuta development preview now stages a distransDiscard receipt only after a clean completed private transfer. Its fresh batch contains the public played Distrans, not the private card already transferred to its recipient. Neutral all-seat acknowledgement precedes closure; claim or decline retires the saved receipt without redoing the hand transfer. Pending responses, 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.

The same preview also pauses a final definite Truthtrance after its public answer, history and physical card disposal are committed. Only an ordinary held Truthtrance with no following queued question, new promise or competing parent qualifies. A claim may move that consumed card, never re-answer the question or rewrite its history; other Truthtrance continuations remain automatic.

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, active bidder and event; claim or decline restores bidding once. Clean movement-order Sapho stages saphoMovementDiscard after its finite first/last queue and public card disposal are committed. The reaction retires before movementTurn opens the new actor's turn; the queue, prior active seat and protected-last receipt bind its suffix. Clean between-battles Sapho stages saphoBattleOrderDiscard after its finite chooser priority and public card disposal commit, before any next battle is selected. The receipt binds the last battle, unselected board, chooser uses, hands and card identity; claim or decline resumes the same active chooser without repeating the reorder. Clean pre-plan aggressor Sapho stages saphoAggressorDiscard after the aggressor event, ordinary tie priority and card disposal commit. The receipt binds the same unsealed battle, readiness, player hands and reserves. Claim or decline restores shared pre-leader preparation without rerunning the aggressor change.

Clean Residual Poison stages residualPoisonDiscard only after its single random leader death and public used-card disposal, before reconciling battle inspections and preparation readiness. The receipt binds the victim, leader custody and unsealed battle. The neutral offer is limited to a pre-leader board with no inspection or ready combatant; other accepted plays retain automatic continuation.

Printed Karama canceling clean CHOAM Charity, Inflation or Bene Gesserit Charity stages karamaCharityDiscard, karamaInflationDiscard or karamaBgCharityDiscard after the public card cost but before accepted cancellation. The first two retain their existing saved receipt signatures. Each binds the sole suspended response, spent card and hand/spice custody; claim or decline restores that response and finishes cancellation once. Other cancellations and shipping retain their automatic paths.

Clean printed Karama normal-auction purchase or winning-bid payment stages karamaPurchaseDiscard or karamaPaymentDiscard after the public cost and before the reserved lot settles. The receipt binds the original lot, round, cache, order, deck, hands and bid winner; a neutral claim or decline completes the same free settlement once. Richese lots, Worthless conversion and nested responses retain their previous automatic paths.

A clean private normal-auction Ixian ally replacement now stages the existing ixAllyCard continuation as a neutral Semuta offer before draw(g). The original buyer owns the discarded card. Its face stays unknown to rivals and uncommitted holders; a sole committed claim moves that exact card before one saved replacement draw and ordinary sale continuation. Richese cache and Black Market sales retain the automatic path until their distinct custody and offer windows are integrated.

A clean completed CHOAM market sale stages choamSaleDiscard only after the accepted public card disposal and its quoted two- or three-spice income. The saved receipt binds the sold card, duplicate witness when present, earned balance and still-open market; claim or decline restores the same owner's next market decision without paying again. Canceled/invalid sales have no batch; competing market parents retain their automatic continuation.

Clean Kulon stages choamKulonDiscard after its accepted public Worthless card cost but before adding one turn-scoped movement range. The receipt binds the active CHOAM player, prior bonus, card and unspent movement. A neutral claim or decline restores the same one-range increment without replaying the response. Reactive, Nexus and overlapping movement parents retain their automatic path.

Clean proactive La La La stages choamLaLaLaDiscard after its accepted public Worthless card cost but before blocking a selected target's free force revival. The receipt binds the target, old Revival rules and physical card. A neutral claim or decline applies the one saved block; paid revival remains legal. Reactive requests, Nexus and other Worthless effects retain their automatic paths.

Clean proactive Baliset stages choamBalisetDiscard after its accepted public Worthless card cost but before restricting the chosen player from movement into CHOAM's occupied territory. The receipt binds the target, territory, CHOAM's current forces, existing restrictions and physical card. A neutral claim or decline installs the one saved restriction; shipment remains possible. Declared moves, Nexus conversion and overlapping arrival parents retain their automatic paths.

Clean proactive ordinary Trip to Gamont stages choamGamontDiscard after the accepted public card cost but before returning one selected opponent force to reserves. The receipt binds the selected location, target force/reserve custody and CHOAM hand; claim or decline completes the one return without replaying the response. Elite, concealed No-Field, closing Mentat decision, Nexus and competing parents retain their automatic paths.

Completed Ornithopter movement now offers the same retired public card before its already committed arrival suffix only in a roster without Bene Gesserit intrusion, Ecaz Ambassador or Moritani Terror response sources. The final forces and move counter are held in the existing bound flight frame; claim or decline retires that frame and logs the arrival once. Mixed entry reactions remain unsupported for Semuta.

Clean battleResolved now pauses the first public mandatory batch after combat result and actual card custody are committed but before casualty choices or winner follow-up. A mutual-Traitor outcome may place two cards from different former owners in that one batch; claimants can choose only a card from another player. Later mandatory winning-card cleanup uses a separate winnerMandatoryDiscard batch, and an optional winner selection uses a third battleCleanup batch. Every supported stage has a neutral all-seat offer; committed holders select one fresh card, leave the others in the pile and resume exactly once. Pending Auditor, capture, tech, Face Dance or CHOAM rewards remain automatic without Semuta.

The explicit Richese preview also accepts a fresh physical Ix deck. Clean Thumper disposal stages thumperDiscard before blowSpice injects a worm or draws from either spice pile. The saved frame binds the undrawn decks, previous land/discard, forces and alliance state; Semuta claim or all-seat decline retires it before one ordinary Spice Blow suffix. A CHOAM-only preview has no Thumper identity.

The same Ix deck supplies Amal. In a clean phase opening, its public disposal precedes halving every personal spice amount and clearing prior opening passes. amalDiscard binds original balances, the halved result and suspended opening; a neutral offer retires before restoring that opening. Bidding pool draws, phase income and other opening effects therefore still occur only after everyone finishes the opening. Nested openings remain unintegrated.

Main findings

A post-applyAction pile diff cannot implement immediate Semuta. Ix Ambassador and Ixian ally replacement discard then draw directly. Ordinary Karama cancellation/purchase can reach replacement, Harkonnen bonus or a new auction pool indirectly. A stopped frame must resume at a named stage, never rerun its original public action.

There are three direct real discard-pile producers outside discard: finishNullentropy installs the pure result pile containing the newly used Box; finishOrnithopter pushes its owned escrow card; the Distrans branch pushes only the used Distrans. draw drains/shuffles the pile but creates no new discard. Setup resets and trial-state discards are not live reaction events.

For each batch retain actual former owner per entry, exact physical ID, a face snapshot as evidence (not extra custody), and whether the face is already public. The initiator may differ from the discarder. Private batch faces cannot appear in public decision, response, logs or event payloads before an entitled committed inspection.

Producer table

“Public” means the current engine actually announces/reveals the used face to the table. Private does not prevent a previously entitled individual from remembering it; it means the new feature must not broadcast that face. Continuation names and parameter sets below are proposed internal discriminants, not existing exports.

Producer / anchor Logical batch, former owner and face Work at boundary; typed continuation needed
discard(g,p,id) (~1806) Primitive for a caller-defined batch, never automatically a whole HTTP action. Owner p.id; face policy supplied by caller. Removes one exact held card after Box/gift guards. A batch primitive should validate all requested members before removal. Return/store a committed batch identity; do not throw midway and pretend the whole caller finished.
finishNullentropy (~1241–1305) One new Box, public; owner pending.player. The old shuffled cards are not new discards, and the recovered card is a transfer. Search payment already happened in beginNullentropy; selection validation and the final shuffle already happen once here. Commit recovered hand + shuffled remainder + used Box, clear paid search, then pause before resuming the original context. afterBox {player, boxEvent, resume:{response,decision,pendingKarama,phaseOpening}}; never repeat payment, shuffle or selected transfer. Snapshot/verify the final pile, not the original discardSignature after it changes.
spendKarama (~3455), printed card One activating Karama, public; card player's owner, not beneficiary. Discard precedes completeKarama. afterKaramaCost {owner,card,form:'printed',use:KaramaUse} must continue the accepted use without rechecking that the spent card remains in hand. use already discriminates cancellation, shipment, immediate purchase and auction payment.
spendKarama, BG Worthless One used Worthless, public because log names its conversion; owner BG. afterKaramaCost {owner,card,form:'worthless',use} opens the conversion response once. Preserve the original canceled response inside use, and any surrounding Box/gift/conversion frame. Do not overwrite pendingKarama with Semuta state.
finishOrnithopter (~3896) One public Ornithopter, owner flight.player, from g.ornithopter.card escrow rather than hand. Clear escrow and record completion once. Caller determines suffix: afterMovementCard {player,origin,to,sector,amount,elite,noField,entryKind:'movement'} for completeMove, or afterEndMovementCard {player} for endMovement. The former has already placed/removed forces and incremented moves before the discard; still needs rate clear, movement log, intrusion and territory entry. The latter still needs rate clear, allied separation, removing the actor, Sapho-last clear and movementTurn.
decideAmbassador, CHOAM cards (~4502) All selected IDs from the beneficiary form one simultaneous batch; private (current log announces count, not identities). Empty selection creates no discard event. Benefit is 3 * selectedCount, not a recomputation from the hand after Semuta. Prefer commit this earned spice with the discard, then afterAmbassadorDiscard {entryEvent,beneficiary,effect:'choam',selectedCount} → finishAmbassador. The finalizer can RNG-replenish ambassadors and resume a worm ride, so it must run once after the claim.
decideAmbassador, Ix cards (~4502–4522) Exactly one private card from beneficiary. Hard stop before draw(g). afterAmbassadorDiscard {entryEvent,beneficiary,effect:'ixians',replacementPending:true} → draw at most once, append private replacement, log, finish. Revalidate that the pending entry still belongs to the frame, not that its discarded card is still held.
decideTerror, Sabotage (~4795) One randomly selected private victim card, owner entrant, not Moritani actor. No card/no batch if victim hand empty. Victim selection RNG is before the discard: save that identity once. Terror token reveal/removal already happened. afterSabotageDiscard {entryEvent,entrant,moritani} → open optional gift if Moritani has cards, otherwise finishTerrorEntry; do not reroll or re-remove token. The later gift is not a discard.
decideTerror, Robbery overflow (~4878) One private selected card per explicit overflow decision; owner current beneficiary p. Separate decisions are separate batches. Robbery draw already happened. afterRobberyDiscard {entryEvent,player} → reopen discard if still over limit, else finish entry. Never repeat the Robbery draw or paid territory entry.
playSapho, Once Around (~5181) One public Sapho from actor. Queue reordering and any just-restored bidder eligibility happen before discard. afterSapho {scope:'onceAround',event,player,mode} → log once and settleRicheseLot. No new reorder, bidder grant or reset of acted; settling can eventually start a later pool.
playSapho, movement (~5204) One public Sapho from actor. Queue/protected-last update already happened. afterSapho {scope:'movement',event,player,mode,previousActive} → log, invoke movementTurn only if current actor changed. Do not reopen an already settled Guild timing decision or lose the last override.
playResidualPoison (~5794–5851) One public Residual Poison from attacker using it. Victim RNG, death, captured/Duke cleanup already happened. afterResidualPoison {battleEvent,owner,target,victimId} → existing promise reconciliation, invalid Prescience-value re-answer and pre-leader readiness update. Do not reroll, kill again or award nonexistent bounty.
resolveBattle, mandatory card cleanup (~7398) Collect all actually mandatory-discarded cards across both combatants as one mixed-owner batch. Faces public through revealed plans/late defense. Exclude retentionReservesCard; exclude retained winner cards. Activated Tooth/Artillery/hero/late defense obey their existing physical used/retention eligibility. Battle result, most deaths/force losses/bounty and pending income/Auditor/FaceDance facts precede this loop. Finish non-drawing bookkeeping and set lastBattle, clear battle, snapshot winner retained-card IDs and remaining casualty choices before exposing a pause. afterMandatoryBattleDiscards {battleEvent,territory,winner,winnerCards,lossChoices} → settleWinnerCasualties / battleLosses decision / finishWinner / finishBattle. Never rerun resolveBattle; do not call used() against hands after a Semuta transfer.
Winner decision.kind:'battleCards' fallback (~11468) Selected subset of already played public winner cards, one batch. Empty subset means no discard event. Decision has been taken; save normalized selected IDs/count. afterWinnerCardDiscards {winner,territory} → log and finishBattle. This is a later batch than mandatory loser cleanup.
finishMoritaniRetention (~7697) All pending.played except keep form one later public batch; former owner pending.player, not the Moritani ally granting retention. Preserve chosen kept physical card and accepted retention result. Clear retention record at commitment so resumption does not require discarded cards in hand. afterMoritaniRetention {player,territory,keep} → one retention log then finishBattle.
finishResponse('choamWorthless') (~8094) One public named Worthless on successful effect only; CHOAM owner. Canceled/stale effect leaves card held and produces no batch. Local pending was already removed from g.pendingChoamWorthless; it must be retained in a typed frame. afterChoamWorthlessDiscard {pending:PendingChoamWorthless} applies its accepted effect once, then exact storm/revival/movement/Mentat suffix. Alternatively commit effect first and store only the suffix; do not rerun effect validation against post-discard custody. See branch inventory below.
finishResponse('ixAllyCard') (~8233) One purchased card discarded by buyer. Private for normal/Black Market purchases unless separately publicly revealed; public for the offered Richese cache face. Do not assume every auction face was public: ordinary projection is Atreides-only. Hard stop before draw(g). afterIxAllyDiscard {player,card,free,saleBinding} → one private draw, clear pendingIxAlly, continueAuctionSale(free). Original payment and acquired card already committed; seller/Emperor income and Harkonnen bonus may still follow.
finishResponse('choamSale') (~8401) One card per successful sale; public because declaration logs revealed name/duplicate witness. Owner market owner. Witness duplicate remains held and is not in batch. Price was quoted before the response; record earned sale.price once with discard. afterChoamSale {owner,saleIdOrBinding,price} → clear market.sale, resume market. Do not recompute duplicate eligibility after claimed card changes hands or repeat payout.
Distrans branch in applyActionInner (~10286) Only result.discarded (public Distrans), owner giver. The secretly given card is a transfer. Both hand transfers already assigned from the pure result. afterDistrans {owner,recipient} needs at most log and generic action finalization. Never rerun transfer or expose action.give through public batch metadata.
Phase-opening Amal (~10351) One public Amal from actor. Discard precedes halving every personal spice and resetting opening passes. afterAmal {owner,openingBinding} applies the effect once, preserving phaseOpening.initialize; or commit halving first and resume only its opening suffix. Never call beginPhase here merely because a Semuta claim completed.
Thumper branch (~13037) One public Thumper from actor. afterThumper {owner,turn,phase} → log and blowSpice(g,true) once. Spice-card draw/worm sequence is a different deck, but still must not run twice or overtake the discard interruption.
Generic Hajr/Ghola/Harvester/Weather/Atomics (~13103) One public played card from actor, one batch each. Effects have already happened before the shared discard. afterOrdinaryCard {owner,cardId,cardName,effect} → log and normal finalization; do not reapply movement allowance, Ghola revival/tech/income, spice multiplier, weather setting or Wall casualties. A Ghola-created revival-income response must survive the pause.
resolveTruthAction callback in game/truthtrance.ts (~460) One public Truthtrance belonging to queued asker, not the answering actor. Multiple queued questions are successive events, not one batch. Unknown/save does not discard. Definite answer, promise binding, public history and answer log already committed. afterTruthtranceDiscard {queueHeadPlayer,queueHeadCard,questionBinding,remainingWindow} activates the next queued question exactly once; do not answer/bind/history again. Detach the consumed head into this frame before exposing the pause: the saved-shipment-question validator requires a live queued Truthtrance still be held. Preserve the remaining queue privately in the continuation until Semuta ends. A generic top-level truthtrance early return must not prevent this nested claim.

Special Karama stage inventory

executeSpecialKaramaIntent (~9579) already has a typed, validated declaration for each power. It is useful input to an after-activation executor, but cannot simply be called again after discarding: preparation assumes the activating physical card is still held. Every activating Karama face is public and belongs to its player. These are separate from any later discard caused by the power.

Intent Accepted continuation after activating discard
richese {owner,acquire,turn,phase,resume}: pay 3, move exact private cache card, set once-use, then open stored Emperor income continuation. Selected acquisition is private; no extra discard for removing it from cache. Bind reservation/price before pause so resumption does not become a different purchase.
choam {owner,selectedIds,selectedCount}: commit one second simultaneous private batch for the selected cards, credit 3 each and set once-use exactly once. Activator and selected cash-in are distinct stages; no array-diff grouping across both. A second frame must own the post-cash-in suffix.
ixians {owner,route,collect}: set once-use then relocate/collect once. Preserve movement route and all territory-entry follow-up.
tleilaxu {owner,target}: set prevention and once-use, clear the bound revival/request/decision once. The interrupted revival opportunity must be explicitly bound, not whatever decision happens to be current after Semuta.
fremen {owner,territory,resume}: set once-use, install summoned-worm resume frame, clear the owned old spice/response/decision fields, then beginWorm once. afterWorm restores that frame via Object.assign; it must not restore an already completed Semuta opportunity.
atreides {owner,target,battleBinding}: set once-use/full-plan entitlement and clear the bound offer once. The battle/plan remains private.
guild {owner,shipper,shipmentBinding,rateRefundBinding}: set once-use and shipper opportunity spent; apply the existing provisional reserved-rate return and clear pending shipment/rate once. The already discarded shipping-rate Karama is not a fresh discard here. See reservation warning below.
emperorLeader / emperorForces Revival, logs and tech income currently occur before activating discard. Save {owner,intentKind} only for setting once-use/finalization; never repeat those already applied mutations.
harkonnen {owner,target,count,resume}: set once-use, perform random take exactly once, then create the equal-return decision. Current random selection is after discard, unlike Sabotage's already chosen victim. If Semuta truly pauses immediately here, sample the resulting live hand only after that pause. Do not prematurely sample and then pretend it was after Semuta, and never reroll on reconnect. The taken/returned cards are transfers, not discards.

CHOAM Worthless suffix detail

The accepted pending record must retain owner,card,effect,target,location,elite,noFieldEvent and its existing storm/revival/movement/mentat continuation flags. Kulon changes movement bonus; Jubba changes storm protection; Baliset may clear the pending move; Gamont may reveal a No-Field and return one typed force; La La La sets free-revival prevention and may clear the revival. After that, the current suffix calls offerChoamStorm, offerRevivalStop, resumeChoamMovement, or opens choamMentat. Those are not interchangeable with a generic nextPhase.

Draw/refill consumers to guard

All actual Treachery draws funnel through draw(g) (~1795). It currently blocks only a paid Box; an unresolved Semuta batch needs an equivalent guard against refill/drain. Direct consumers are dealStartingTreachery; setAuction's pool loop; Ix Ambassador replacement; Robbery; finishResponse('ixAuction')'s multi-card pool loop; finishResponse('ixAllyCard'); finishResponse('harkonnenBonus'); and the Ix setup decision's Harkonnen starting bonus. Setup normally cannot have a fresh Semuta discard, but the guard should remain universal.

Cancellation/purchase chain: completeKarama → finishResponse or settleAuction → continueAuctionSale → auctionBonus/nextAuction → Richese next-lot or phase preparation. Preserve currentAuctionSale, original payment, lot/card index, free, and pending Ix replacement across that chain. A future multi-draw continuation must store already drawn cards plus remaining count if it can itself suspend; restarting a draw loop from zero duplicates cards. No current raw draw(g) performs a discard itself, so well-placed discard pauses can usually leave these loops indivisible.

finishAmbassador may replenish five tokens using RNG; finishBattle can open retention, CHOAM income, tech, capture, Auditor, FaceDance or the next phase; endMovement may start a new phase; finishChoamMarket advances storm/phase; Fremen worm restoration resumes older phase state. None may run ahead merely because Semuta is represented by a field that the current automatic-finalization loop does not know.

Exclusions, reservations and privacy hazards

  • Not discards: Ix Technology substitutes hand and auction-pool cards; Ix auction selection returns a pool card to deck; unused auction cards return to deck; CHOAM allied swaps; Richese gifts; Harkonnen equal exchange; Sabotage gift; cache purchase; cache remove-from-game; normal purchase; Box's recovered target and shuffled old pile; Spice/Traitor/FaceDance/Nexus/leader/token movement. Do not emit events from generic hand disappearance or pile replacement.
  • Reserved rate card: karamaShipping.card points to a physically discarded card which Guild special currently returns provisionally. BG conversion can carry this binding earlier inside pendingKarama.use; if pausing immediately after the cost, the saved KaramaUse already owns that future reservation before karamaShipping exists. An exclusion/escrow rule must inspect the suspended intent, not only the live top-level rate field. Do not silently settle the underlying refund ambiguity by giving the same card to two owners.
  • Other reserved cards: Truthtrance queue heads, paid Box, committed gifts, Black Market offerings, Moritani retained cleanup and Ornithopter escrow each have existing ownership rules. Semuta can target only a completed discard, not a still-held reserved card. Preserve reserved incoming hand slots from auctions and mandatory exchanges when checking Semuta capacity; printed full-hand sequencing remains unresolved in the source audit.
  • Trial engines: findReachableBattlePlan discards Ghola on trial; special-intent and shipment-completion searches call effect executors on clones. These must not produce real Semuta prompts, consume RNG just to enumerate options, or treat a newly paused effect as already earned income. Give search execution an explicit internal simulation path that retains physical accounting without public interactive side effects.
  • Finalizer fences: add pending-discard/claim checks before finishActionContinuations, both promise reconciliations and settleAutomaticContinuations; also room recovery/bot scheduling and top-level Truthtrance/Box handling. Otherwise temporary suspension can release a feasible promise, answer the next queued question, settle a payout or draw before Semuta. Once a batch closes, resume its typed suffix then ordinary finalization exactly once.
  • Integrity before visibility: do not publish a battle halfway through mandatory cleanup: existing Auditor/Stone/late-defense invariants require coherent completed contexts. Similarly, do not leave a consumed Truthtrance at a live validated queue head or a discarded Ix purchase inside a validator requiring it still held. Move ownership to a stage-specific continuation before opening the neutral reaction.

Verification targets

Prioritize empty-deck Ix replacement (fresh target stays in pile until claim/pass); Sabotage (victim RNG once and private); mandatory mixed-owner battle batch plus separate winner/Moritani batches; Box (paid shuffle once); movement-card escrow (no second move/end-turn); nested Karama purchase/cancel and shipping refund binding; CHOAM activation versus cash-in batches; and Truthtrance history/binding once. At every stage test JSON/module reload and concurrent identical claim CAS, original resource benefit once, exact inventory, owner-only unknown faces after commitment, and byte-equivalent rival projections. Known source limits remain full-hand Semuta sequencing and the existing provisional shipping-rate refund. No new timing ruling is asserted by this inventory.