Allow user-approved atomic full-hand Semuta exchange

This commit is contained in:
Quintine committed 2026-09-30 17:39:23 +10:00
1 parent 98d556a0e0
commit 3285172eec
16 files changed
+168 -69

No files matched your search

+1 -1
View File
@@ -152,4 +152,4 @@ Preparation includes public-dispatcher timing gates, with CHOAM's existing phase
Ten regression cases in `tests/karama-intents.test.ts` cover all variants, serialization, preparation purity/no random draw, stale timing/custody/resources, copied CHOAM selection, exact declaration binding, deferred Harkonnen randomness, injected execution failure isolation, dispatcher parity and preserved CHOAM exceptions. The full 728-case unit suite and 33 persisted/database cases pass.
This prerequisite does not add a pending Kull frame, a phase restriction, or any counter-priority interpretation. The user has been asked whether to use a documented provisional policy (reserve the attempted card, allow a different Karama to counter Kull before its ban, intercept BG before conversion/discard) while CHOAM stays gated. No answer has been received at this checkpoint, so dependent timing implementation remains pending.
This prerequisite does not add a pending Kull frame or phase restriction. On 30 September the user approved the distinct-card counter and pre-conversion BG interpretation, while deferring unpayable winning overbids; see the [selected preview policy](CHOAM_KULL_SOURCE_UPDATE.md#user-selected-preview-timing--30-september-2026). Dependent Kull timing implementation remains unfinished.
+18
View File
@@ -2,6 +2,24 @@
Checked 2026-09-07 for classic GF9 Dune and CHOAM & Richese. This supplements [CHOAM_KULL_DESIGN.md](CHOAM_KULL_DESIGN.md) and the Kull section of [CHOAM_REMAINING_RULES.md](CHOAM_REMAINING_RULES.md). No runtime behavior changes in this audit.
## User-selected preview timing — 30 September 2026
The user selected **Different Karama**: reserve the interrupted
physical card; a distinct eligible Karama may counter Kull before
the phase restriction takes effect. The interrupted card cannot
serve both the original attempt and its counter. They selected
**Before conversion** for Bene Gesserit's Worthless-as-Karama:
intercept while the card is still held; a successful Kull retains
the unplayed card. They selected **Defer interaction** for a winning
overbid made unpayable by loss of its only Karama payment. That
composition must remain guarded, not restart bidding, cap hidden
reaction-dependent bids, grant a free card or fabricate funding.
These are explicit provisional product interpretations, not a
publisher clarification. They supersede the older unanswered
counter/BG proposals below; full CHOAM starts remain gated.
## Result and provenance
No additional verified publisher or designer ruling was located that settles Kull counter priority, Bene Gesserit conversion timing, interrupted-card custody, or an unfunded winning overbid. This is a bounded retrieval result, not proof that no clarification exists.
+21 -6
View File
@@ -46,9 +46,23 @@ committed owner-only selection; mandatory and optional winner cleanup
remain separate. Minimal legal AI, JSON/SQLite continuation and
competing-room CAS are connected. Mixed arrivals, queued or
promise-bound Truthtrance, other Karama uses, pending rewards,
full-hand ordering, normal Richese starts and deployed acceptance
remain unfinished. Neutral passes and free-slot guard are product
choices.
normal Richese starts and deployed acceptance remain unfinished.
Neutral passes are application policy. The user approved Semuta's
atomic exchange at the faction hand limit; completed hand counts
and reserved incoming capacity remain protected.
Full-hand Semuta exchange passes normal/faction-limit and
incoming-reservation regressions, authenticated SQLite and
independent review. `npm run check` passed types, lint and
5,854 offline tests; build passed. Direct smoke and an actual
phone-width full-hand QA seat acquired Hajr, spent Semuta,
kept four cards and preserved that result across refresh.
Isolated Wrangler-dev HTTP passed 49/55 with five proxy 503s
and one timeout; this is not a green local HTTP suite.
Proxy-free CI for the exact checkpoint remains pending.
Kull's approved distinct counter/BG-before-conversion timing
is recorded but runtime remains unfinished; unpayable
winning-overbid recovery is deliberately not chosen.
Accepted printed Jubba Cloak passes focused engine and
authenticated SQLite, a direct one-storm claim smoke and
@@ -56,9 +70,10 @@ independent review without findings. `npm run check` passed
types, lint and 5,853 offline tests; the production build
and phone-width in-app guide passed. Isolated built-worker
HTTP passed 50/55 with four Wrangler-dev proxy 503s and
one timeout. The local suite is not green; proxy-free CI
for this exact checkpoint is pending. No NAS deployment
or complete faction claim follows.
one timeout. The local suite is not green. Proxy-free workflow
`36676105785` passed the full isolated HTTP suite for `98d556a`
and published its image. No NAS deployment or complete faction
claim follows.
Clean ordinary Trip to Gamont passes focused engine and
authenticated SQLite checks, a direct physical claim/force
+32 -2
View File
@@ -1,5 +1,34 @@
# Dune implementation status
## 30 September 2026 — user-approved full-hand Semuta exchange
The user selected atomic acquisition/disposal at the faction
hand limit. Engine commitment and private controls now allow
a full hand; the claimed card replaces Semuta without increasing
the completed hand. Already overfull hands still reject, and
the pure exchange quote preserves incoming reservations.
The obsolete caller-selectable free-slot policy was removed.
Normal and Harkonnen capacity boundaries, physical conservation,
private projection, JSON continuation and authenticated full-hand
Trip recovery have focused regressions.
The same conversation approved Kull's distinct-card counter and
pre-conversion BG interception, while deferring an unpayable
winning-overbid remedy. These rulings are recorded in the
decision index and source update; Kull runtime is not yet
implemented by this capacity checkpoint.
`npm run check` passed types, lint and 5,854 offline tests;
the production build passed. Independent review found no
scoped defect. Direct engine smoke kept four cards before
and after claiming Hajr. In an isolated authenticated
phone-width QA room, the real full-hand seat showed enabled
Commit, exchanged Semuta for Hajr and retained 4/4 after
refresh. The throwaway staging script was removed.
Isolated Wrangler-dev HTTP passed 49/55: five proxy 503s
and one timeout. This local HTTP run is not green;
proxy-free CI and NAS deployment are separate pending gates.
## 30 September 2026 — Semuta after accepted CHOAM Jubba Cloak cost
In the explicit Richese preview, CHOAM's printed moving-storm
@@ -28,8 +57,9 @@ found no scoped defect. The built phone-width guide visibly
described Jubba's before-protection offer. Isolated
Wrangler-dev HTTP passed 50/55: four requests returned its
proxy-layer 503 and one discussion request timed out. This
is not a green local HTTP suite. Proxy-free CI and live NAS
deployment remain pending for this checkpoint.
is not a green local HTTP suite. Proxy-free workflow `36676105785`
passed the full isolated HTTP suite for `98d556a` and published its
image. No live NAS deployment is claimed.
## 30 September 2026 — Semuta after proactive ordinary Trip cost
+3 -1
View File
@@ -47,6 +47,8 @@ Updated 14 September 2026. This is the authoritative navigation index for existi
| Homeworld effects | Keep original payment/revival/placement identities and qualification observations; observations alone do not choose an occupied-benefit owner. | [Payments](HOMEWORLD_PAYMENT_INCOME_RULES.md), [card economy](HOMEWORLD_CARD_ECONOMY_RULES.md), [Collection](HOMEWORLD_COLLECTION_RULES.md), [revival deployment](HOMEWORLD_REVIVAL_DEPLOYMENT_RULES.md), [victory reinforcement](HOMEWORLD_VICTORY_REINFORCEMENT_RULES.md), [replacement](HOMEWORLD_REPLACEMENT_RULES.md), [Tupile](HOMEWORLD_TUPILE_INTELLIGENCE_RULES.md) |
| Duke Vidal revival custody | The user selected set-aside custody after Ecaz revives the shared disc: revival clears the Tanks but does not itself grant battle control. Ecaz still needs a separate Ambassador or Nexus acquisition; only Ecaz may revive Duke, including by Ghola. This resolves one post-revival product interpretation, **not** the separate six-disc repeated-cycle, exceptional capture or battle-used Ghola questions. No publisher clarification assigning a post-revival controller was located. | [Publisher Ecaz/Moritani pp. 7–9](https://gamers-hq.de/media/pdf/0f/7a/86/Dune_EcazMoritani_Rulebook_EN.pdf#page=9), [revival readiness](ECAZ_DUKE_REVIVAL_READINESS.md) |
| Mirror Weapon copied-card cleanup | The user selected ordinary winner retention for the physical Mirror even if its effective copy activates Tooth or Artillery. The original Tooth/Artillery cards retain their named mandatory-discard exceptions. This is a product interpretation, **not** a located publisher ruling. Independent copy-first Tooth/Stone choices and physical cleanup now have a bounded CHOAM/Richese-deck battle runtime; combined modules and full Richese games remain gated. | [Printed Mirror face and source audit](MIRROR_WEAPON_ENGINE_AUDIT.md#user-selected-cleanup-interpretation-28-september-2026) |
| Kull Wahad preview timing | User selected a distinct eligible Karama counter while the interrupted card stays reserved, and interception of BG Worthless-as-Karama before conversion/discard. A prevented unplayable winning overbid remains guarded; no remedy was approved. Full CHOAM starts remain gated. | [Selected preview policy](CHOAM_KULL_SOURCE_UPDATE.md#user-selected-preview-timing--30-september-2026) |
| Semuta full-hand exchange | User selected **Allow exchange**: acquisition followed by Semuta disposal is an atomic net-zero hand exchange at the faction limit. Existing incoming reservations still consume capacity; this does not permit an already overfull hand or settle Box/Richese purchase timing. | [Selected capacity policy](SEMUTA_SOURCE_UPDATE_20260907.md#user-selected-full-hand-exchange--30-september-2026) |
Shipment-only AND/OR now follows the FAQ's whole-question truth: Yes to AND
requires all conditions, Yes to OR requires one, and No negates the whole
@@ -78,7 +80,7 @@ These are unresolved in the cited records. Do not infer an answer from elapsed t
| Recruits late accounting and rates | A user question has been sent for two free plus one paid Atreides return before Recruits: keep the disputed case gated, or one/two further free returns without refund. No answer is assumed. Late-changing free rates, pending repricing and same-turn recovered-card replay also stay gated. The connected clean-case preview retains the independent seven-force cap after faction-advantage cancellation as an explicit inference, not a direct combined FAQ. | [Source audit](RECRUITS_RULES.md), [runtime scope](RECRUITS_RUNTIME.md) |
| Sandmaster / native HMS relocation | No official source located establishes that passengers remaining inside the HMS enter outside territories for this skill. Extra collection eligibility is pending a user ruling; native sector collection and ordinary force entry/exit remain available. | [Source comparison and question](SANDMASTER_MOVEMENT.md#21-september-native-hms-relocation-eligibility) |
| Sapho remaining timing | First versus unspent Advanced Guild priority, partly completed combined turns, cyclic auction duration and later battle timing stay guarded. The existing pre-plan opportunity supports [early aggressor](SAPHO_AGGRESSOR.md). Prior questions remain unanswered. | [Source audit](JUICE_OF_SAPHO_SOURCE_UPDATE.md), [battle review](JUICE_OF_SAPHO_BATTLE_REVIEW.md) |
| Semuta Drug | Full-hand take-before-discard and the privacy-neutral immediate reaction protocol remain unresolved. Typed discard continuations already exist; they are not activation. | [Source update](SEMUTA_SOURCE_UPDATE_20260907.md), [privacy review](SEMUTA_PRIVACY_REVIEW.md) |
| Semuta Drug | Full-hand exchange is user-approved; neutral all-seat acknowledgements remain the explicit application policy. Other discard producers, nested continuation coverage and full-game acceptance remain unfinished. | [Source update and selected exchange](SEMUTA_SOURCE_UPDATE_20260907.md), [privacy review](SEMUTA_PRIVACY_REVIEW.md) |
| Leader Skills edge cases | Capture/replacement eligibility, Banker escrow custody, Smuggler modifier/allocation boundaries, Mentat empty-hand fallback, modified Diplomat defenses and simultaneous optional timing remain source gaps. After an online publisher/designer/tournament check found no explicit priority ruling, the bounded standalone Diplomat retreat prototype explicitly chooses printed/copied disc strength and a surviving-loser retreat before ordinary casualties; other combinations remain guarded. This is an implementation interpretation, not an official clarification. | [Material boundaries and selected retreat interpretation](LEADER_SKILLS_RULES.md#bounded-diplomat-retreat-interpretation), [Diplomat defense boundary](DIPLOMAT_DEFENSE.md#evidence-and-remaining-scope) |
| Smuggler collection strength | A fresh primary-source check found no modifier ruling. The user was asked whether to keep affected combinations gated, use printed/copied disc value, or include leader bonuses with Stunner zero. No answer is assumed. The reveal-bound deferred receipt is supported as an explicit timing inference; unmodified lower collection is now [prototyped](SMUGGLER_BATTLE.md); modified strength, multiple positive piles and combined modules remain gated. | [Smuggler follow-up](LEADER_SKILLS_RULES.md#20-september-smuggler-follow-up) |
| Suk Graduate / Atreides | Whether rescued dialed casualties count toward the seven-loss Kwisatz Haderach threshold remains unresolved. The user question has been sent; Advanced Atreides rescue stays guarded. | [Source and runtime boundary](SUK_GRADUATE_RULES.md) |
+2 -2
View File
@@ -225,11 +225,11 @@ Cancellation/purchase chain: `completeKarama` → `finishResponse` or `settleAuc
- **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.
- **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's user-approved atomic full-hand exchange; the final hand must still fit those obligations.
- **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.
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. Full-hand Semuta exchange is user-approved; the provisional shipping-rate refund remains separate. No new publisher timing ruling is asserted by this inventory.
+1 -1
View File
@@ -1,6 +1,6 @@
# Semuta Drug: discard interception audit
Audited 6 September 2026. This document originally proposed an implementation contract. The 7 September [discard continuation checkpoint](TREACHERY_DISCARD_CONTINUATIONS.md) now implements isolated claim rules and three saved exchange stages. Semuta activation remains unavailable and expansion starts remain gated.
Audited 6 September 2026. This document originally proposed an implementation contract. The 7 September [discard continuation checkpoint](TREACHERY_DISCARD_CONTINUATIONS.md) implemented isolated claim rules and saved exchange stages. The [current source/runtime boundary](SEMUTA_SOURCE_UPDATE_20260907.md) supersedes historical absence claims: bounded preview activation and user-approved full-hand exchange are now connected. Full Semuta and expansion starts remain gated.
## Printed effect and primary evidence
+6
View File
@@ -2,6 +2,12 @@
7 September 2026. Read-only audit of `docs/SEMUTA_DRUG_ENGINE_AUDIT.md` in full, current engine, Truthtrance, bots and room persistence. No new timing or capacity ruling is assumed. No shared source edits. The existing audit's verified physical face and official CHOAM rulebook provide the effect/provenance: one fresh Treachery discard from another player, optional activation, choose one from simultaneous discards, then discard Semuta. Private discard inspection does not become freely available merely by holding Semuta.
30 September follow-up: this is a historical audit. The bounded
neutral reaction is now connected, and the user approved atomic
full-hand exchange; see the [current source/runtime boundary](SEMUTA_SOURCE_UPDATE_20260907.md).
The earlier capacity question below is not still unanswered.
## Decision that cannot be solved by hiding a player name
The following three requirements cannot all be guaranteed by a server-only secret-eligibility check:
+24 -9
View File
@@ -2,6 +2,20 @@
7 September 2026. Read `docs/SEMUTA_DRUG_ENGINE_AUDIT.md` in full, inspected the physical card again, checked current capacity/recovery code, and freshly searched publisher sources. No project runtime edits.
## User-selected full-hand exchange — 30 September 2026
The user selected **Allow exchange**: treat the acquisition and
Semuta disposal as one allowed atomic exchange, accepting its
temporary take-before-discard order at the faction hand limit.
The completed hand has the same count. This applies to ordinary
and Harkonnen limits; reserved mandatory incoming cards still
consume capacity, and an already overfull hand cannot claim.
This is a product ruling, not a newly found publisher FAQ. It
supersedes the earlier unanswered full-hand question and
provisional free-slot guard. Box and special Richese purchase
capacity are separate, unchanged boundaries.
## What is actually confirmed
The readable publisher card face at `/tmp/dune-rules/choam-semuta-reading.png` says to take a Treachery Card immediately after another player discards it, add it to hand, choose one when multiple cards are discarded together, and discard Semuta after use. Canonical card: `richese-semuta-drug`, Special. The [photographed component](https://cdn.anyfinder.eu/assets/QsUJtVktEC87xwK08oLlwnKyaozUWfuaLzG0LFEn67ENEEVgNrDGoITy3608Dqe7) and provenance limits are recorded in `docs/RICHESE_COMPONENTS.md`; this is visible publisher-authored text on an externally hosted photograph, not a newly located official FAQ or verified printing history.
@@ -10,7 +24,7 @@ Fresh official search retrieved [CHOAM/Richese p4](https://www.gf9games.com/dune
Direct official CHOAM PDF and [November 2020 FAQ](https://www.gf9games.com/dune/wp-content/uploads/2020/11/Dune-FAQ-Nov-2020.pdf) opens returned HTTP 403. Indexed publisher search furnished the two passages above. Searches for Semuta, Semuta Drug, full-hand, discard and relevant FAQ terms found no Semuta-specific publisher answer. Some broad searches returned other GF9 games, including the separate movie Dune game; these were excluded. Local Ecaz/Ix text searches found no Semuta reference. I could not freshly retrieve the historical audit's Harkonnen-exchange FAQ passage, and do not promote that earlier citation into independently reverified Semuta evidence.
**No newly found primary source resolves full-hand take-before-discard.** The original audit's material capacity limitation remains. This is distinct from Sapho: Sapho is discarded before its holder later buys an auction card, whereas Semuta's face places acquisition before its disposal instruction.
**No newly found primary source resolves full-hand take-before-discard.** The user-selected atomic exchange above resolves the product policy, not publisher provenance. This is distinct from Sapho: Sapho is discarded before its holder later buys an auction card, whereas Semuta's face places acquisition before its disposal instruction.
## Target and effect contract
@@ -53,7 +67,7 @@ Useful acceptance test: hold public state and clock/event progression fixed; rep
## Smallest unresolved ruling set
**One Semuta-specific question remains:** may a holder already at their faction hand limit use Semuta as an exchange, or must a free slot exist before taking the card because Semuta is discarded afterward? Apply the eventual answer to normal and Harkonnen limits and separately retain capacity reserved for compulsory incoming exchanges. A temporary free-slot guard is defensible as an explicitly unfinished boundary; it must not be presented as wording printed on Semuta.
The earlier full-hand question is now answered by the user-selected exchange above. Normal and Harkonnen completed hands must still fit their limits after accounting for reserved compulsory incoming cards.
The existing unresolved **Guild-stopped shipping-Karama refund** also constrains claiming that exact provisionally refundable card. Do not ask it again as a new Semuta question. Fence its competing custody or resolve the existing underlying ruling; this does not prohibit every shipment-phase Semuta event. Current Box guard in `game/engine.ts` near `:1209` already identifies this composition.
@@ -191,9 +205,10 @@ follow it once. Mandatory winning-card cleanup, if present, is a
separate later batch; optional winner-selected disposal is later
again. No Semuta claim replays combat or combines these events.
Each seat can Continue; only the canonical holder with a free hand
slot and a card discarded by another player can commit. A sole
eligible fresh card transfers automatically in that same action.
Each seat can Continue; a canonical holder at or below the faction
hand limit with capacity for incoming reservations and a card
discarded by another player can commit. A sole eligible fresh card
transfers automatically in that same action.
Multiple eligible public battle cards open a committed, owner-only
inspection and exact one-card selection; the other cards remain in
the pile and Semuta is discarded once. The saved ordinary effect or
@@ -201,10 +216,10 @@ Box parent resumes once. No undisclosed face is projected before
commitment. Other simultaneous discard producers remain unfinished.
This neutral all-seat acknowledgement is a conservative **application
policy**, not a newly discovered publisher timing rule. The explicit
free-slot requirement is a provisional safety guard, not the printed
resolution of full-hand take-before-discard. Normal auction/CHOAM
market, pending gifts, competing responses, completed Ornithopter
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 BG intrusion, Ecaz Ambassador or Moritani Terror,
queued or promise-bound Truthtrance, pending battle rewards and other
typed producers retain their automatic continuation; Semuta cannot
+3 -3
View File
@@ -6554,7 +6554,7 @@ function semutaContext(g: Game, owner: Player): SemutaContext {
batch: pending.batch, event: pending.batch.event, turn: g.turn, phase: g.phase,
owner: owner.id, ownerHand: owner.hand, discard: g.discard,
semutaId: SEMUTA_DRUG_ID, handLimit: handLimit(owner),
incomingReservedSlots: 0, capacityPolicy: 'freeSlot', reservedTargetIds: [],
incomingReservedSlots: 0, reservedTargetIds: [],
};
}
function semutaCandidates(g: Game, owner: Player) {
@@ -6587,8 +6587,8 @@ function projectedSemutaReaction(g: Game, me: Player) {
const held = !!semuta;
const blocked = !held || passed ? null
: transferCardBlock(g, me, semuta!) ??
(me.hand.length >= handLimit(me)
? 'A free hand slot is required before taking a card with Semuta Drug.'
(me.hand.length > handLimit(me)
? 'Resolve your excess hand cards before using Semuta Drug.'
: pending.batch.entries.every(entry => entry.discardedBy === me.id)
? 'No other player discarded a card in this fresh event.'
: null);
+3 -3
View File
@@ -1156,7 +1156,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, 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 Ixian ally replacement also pauses before drawing, without showing its face until a holder commits. Commit held Semuta with a free hand slot; 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, full-hand sequencing 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, 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 Ixian ally replacement also pauses before drawing, without showing its face until a holder commits. 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'
@@ -1166,7 +1166,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 Ixian ally replacement pauses before its draw with a neutral, face-hidden offer. 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. Other Karama uses, other reactive Worthless uses, private discards, nested responses, pending rewards and full-hand exchange 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 Ixian ally replacement pauses before its draw with a neutral, face-hidden offer. 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,
@@ -1184,7 +1184,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, 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 Ixian ally replacement, privacy-neutral acknowledgements, provisional free-slot policy and remaining discard sources.',
'The guide distinguishes clean public ordinary-card, paid Box, Ornithopter, Distrans, Truthtrance, 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 Ixian ally replacement, privacy-neutral acknowledgements, the user-approved atomic full-hand exchange and remaining discard sources.',
},
{
area: 'Verification' as const,
+7 -18
View File
@@ -25,8 +25,6 @@ export type SemutaContext = {
semutaId: string;
handLimit: number;
incomingReservedSlots: number;
/** Mandatory caller policy; neither full-hand ruling is inferred here. */
capacityPolicy: 'exchange' | 'freeSlot';
reservedTargetIds: readonly string[];
};
export type SemutaCommitment = {
@@ -96,23 +94,14 @@ function validate(context: SemutaContext) {
throw new Error(
'Semuta requires a current owner, event and physical card custody.',
);
if (
!count(context.handLimit) ||
!count(context.incomingReservedSlots) ||
!['exchange', 'freeSlot'].includes(context.capacityPolicy)
)
if (!count(context.handLimit) || !count(context.incomingReservedSlots))
throw new Error(
'Semuta requires an explicit capacity policy and nonnegative safe hand limits and reservations.',
);
const occupied = context.ownerHand.length;
const room = context.handLimit - context.incomingReservedSlots;
if (
occupied > room ||
(context.capacityPolicy === 'freeSlot' && occupied >= room)
)
throw new Error(
'The selected Semuta capacity policy leaves no available hand capacity.',
'Semuta requires nonnegative safe hand limits and reservations.',
);
// User-selected exchange: the acquired card replaces Semuta atomically.
// Mandatory incoming cards still require capacity in the completed hand.
if (context.ownerHand.length > context.handLimit - context.incomingReservedSlots)
throw new Error('Semuta exchange exceeds available hand capacity.');
const seen = new Set<string>();
for (const card of [
...Array.from(context.ownerHand),
@@ -206,7 +195,7 @@ export function committedSemutaCandidates(
* Fresh-event transfer only. Engine owns timing, globally unique custody outside
* these supplied zones, activation reservations and persisted commitment. A
* direct selection is also usable when the target face is already authorized.
* Capacity is evaluated under the caller's explicit ruling; no spice is paid.
* Acquisition and disposal form an atomic exchange; no spice is paid.
*/
export function resolveSemutaDrug(
context: SemutaContext,
+8 -11
View File
@@ -26,7 +26,6 @@ function fixture(): SemutaContext {
semutaId: SEMUTA_DRUG_ID,
handLimit: 4,
incomingReservedSlots: 0,
capacityPolicy: 'freeSlot',
reservedTargetIds: [],
batch: {
event: 'fresh-1',
@@ -173,24 +172,22 @@ void test('canonical Semuta identity is required and every supplied physical zon
rejected(c);
}
});
void test('both explicit capacity policies account for incoming reservations without selecting a default ruling', () => {
for (const capacityPolicy of ['exchange', 'freeSlot'] as const)
for (let held = 1; held <= 5; held++)
void test('atomic Semuta exchange preserves normal and Harkonnen capacity and incoming reservations', () => {
for (const limit of [4, 8])
for (let held = 1; held <= limit + 1; held++)
for (let reserved = 0; reserved <= 3; reserved++) {
const c = fixture();
c.handLimit = limit;
c.ownerHand = [semuta(), ...baseDeck().slice(10, 10 + held - 1)];
c.capacityPolicy = capacityPolicy;
c.incomingReservedSlots = reserved;
const legal =
held + reserved + (capacityPolicy === 'freeSlot' ? 1 : 0) <=
c.handLimit;
if (legal) {
if (held + reserved <= limit) {
const result = resolveSemutaDrug(c, target(c));
assert.equal(result.ownerHand.length, held);
assert.ok(result.ownerHand.some(card => card.id === target(c)));
assert.ok(!result.ownerHand.some(card => card.id === SEMUTA_DRUG_ID));
assert.equal(result.discard.filter(card => card.id === SEMUTA_DRUG_ID).length, 1);
} else rejected(c);
}
for (const capacityPolicy of [undefined, null, 'automatic', ''])
rejected({ ...fixture(), capacityPolicy } as SemutaContext);
for (const n of [-1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1]) {
rejected({ ...fixture(), handLimit: n });
rejected({ ...fixture(), incomingReservedSlots: n });
+35 -8
View File
@@ -308,19 +308,46 @@ void test('neutral Semuta passes do not reveal hidden possession and resume a de
assert.deepEqual(normalizeAutomaticGame(reload(declined)), reload(declined));
});
void test('the provisional free-slot guard is private and never spends a full-hand Semuta', () => {
void test('a full-hand Semuta exchanges one card without revealing private capacity', () => {
const { g, semuta, hajr } = fixture();
for (let n = 0; n < 3; n++) player(g, 'r').hand.push(g.deck.shift()!);
const pending = applyAction(g, 'a', { type: 'card', card: hajr.id });
const event = pending.pendingTreacheryDiscard!.batch.event;
const richese = viewGame(pending, 'r').semutaReaction!;
assert.equal(richese.canCommit, false);
assert.match(richese.blocked ?? '', /free hand slot/);
assert.equal(viewGame(pending, 'r').semutaReaction?.canCommit, true);
assert.equal(viewGame(pending, 'r').semutaReaction?.blocked, null);
assert.equal(viewGame(pending, 'a').semutaReaction?.blocked, null);
const before = JSON.stringify(pending);
assert.throws(() => applyAction(pending, 'r', { type: 'semutaCommit', event }));
assert.equal(JSON.stringify(pending), before);
assert.equal(player(pending, 'r').hand.some(card => card.id === semuta.id), true);
const done = applyAction(reload(pending), 'r', { type: 'semutaCommit', event });
assert.equal(player(done, 'r').hand.length, 4);
assert.ok(player(done, 'r').hand.some(card => card.id === hajr.id));
assert.ok(!player(done, 'r').hand.some(card => card.id === semuta.id));
assert.equal(done.discard.filter(card => card.id === semuta.id).length, 1);
assert.equal(done.pendingTreacheryDiscard, null);
assert.deepEqual(inventory(done), inventory(pending));
assert.equal(done.hajr.filter(id => id === 'a').length, 1);
assert.throws(() => applyAction(done, 'r', { type: 'semutaCommit', event }));
});
void test('Harkonnen exchanges Semuta at eight cards while an already overfull hand rejects immutably', () => {
const { g, semuta, hajr } = fixture();
player(g, 'r').hand = [];
g.players[2] = newPlayer('e', 'Harkonnen', 'harkonnen');
player(g, 'e').hand = [semuta, ...g.deck.splice(0, 7)];
player(g, 'r').hand.push(g.deck.shift()!);
const pending = applyAction(g, 'a', { type: 'card', card: hajr.id });
const event = pending.pendingTreacheryDiscard!.batch.event;
assert.equal(viewGame(pending, 'e').semutaReaction?.canCommit, true);
const overfull = reload(pending);
player(overfull, 'e').hand.push(player(overfull, 'r').hand.pop()!);
assert.equal(viewGame(overfull, 'e').semutaReaction?.canCommit, false);
assert.deepEqual(viewGame(overfull, 'a'), viewGame(pending, 'a'));
const before = JSON.stringify(overfull);
assert.throws(() => applyAction(overfull, 'e', { type: 'semutaCommit', event }));
assert.equal(JSON.stringify(overfull), before);
const done = applyAction(reload(pending), 'e', { type: 'semutaCommit', event });
assert.equal(player(done, 'e').hand.length, 8);
assert.ok(player(done, 'e').hand.some(card => card.id === hajr.id));
assert.ok(!player(done, 'e').hand.some(card => card.id === semuta.id));
assert.deepEqual(inventory(done), inventory(pending));
});
void test('a saved neutral reaction cannot be deleted to auto-retire the fresh discard', () => {
+1 -3
View File
@@ -70,9 +70,7 @@ void test('passed, blocked, busy and waiting seats hide or disable unavailable c
const passed = offer({ passed: true, canCommit: true });
assert.match(markup(passed), /You continued/);
assert.doesNotMatch(markup(passed), /Commit Semuta Drug|>Continue</);
const blocked = offer({ blocked: 'Your hand has no free slot.' });
assert.match(markup(blocked), /Your hand has no free slot/);
assert.doesNotMatch(markup(offer()), /Your hand has no free slot/);
const blocked = offer({ blocked: 'Reserved for an unfinished transaction.' });
assert.doesNotMatch(markup(blocked), /Commit Semuta Drug/);
assert.match(markup(offer({ canCommit: true }), true), /disabled=""[^>]*>Commit Semuta Drug/);
assert.match(markup(offer(), true), /disabled=""[^>]*>Continue/);
+3 -1
View File
@@ -1045,7 +1045,7 @@ void test('authenticated proactive Baliset Semuta claim restores only the select
} finally { f.sqlite.close(); }
});
void test('authenticated ordinary Trip Semuta claim returns one force after saved offer', async () => {
void test('authenticated full-hand Trip Semuta exchange returns one force after saved offer', async () => {
const f = unitStore();
try {
const made = await f.rooms.createRoom('Semuta Trip', 'choam', false, ['choam']);
@@ -1073,6 +1073,7 @@ void test('authenticated ordinary Trip Semuta claim returns one force after save
const used = g.deck.splice(index, 1)[0];
g.players[0].hand.push(used);
g.players[1].hand.push(semuta);
g.players[1].hand.push(...g.deck.splice(0, 3));
g.players[1].forces = { 'arrakeen:10': 1 };
g.players[1].reserves = 19;
g.players[2].hand.push(karama);
@@ -1111,6 +1112,7 @@ void test('authenticated ordinary Trip Semuta claim returns one force after save
assert.equal(claimed.players[1].forces['arrakeen:10'], undefined);
assert.equal(claimed.players[1].reserves, 20);
assert.equal(claimed.players[1].hand.filter(card => card.id === used.id).length, 1);
assert.equal(claimed.players[1].hand.length, 4);
assert.equal(claimed.discard.filter(card => card.id === semuta.id).length, 1);
await assert.rejects(f.restart().act(code, auths[1], current.version,
{ type: 'semutaCommit', event }, clock));