From 047011e2438e06e93dc68d423d56135cb883846d Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:48:20 +1100 Subject: [PATCH] Allow Semuta recovery before native Ghola income Reuse the exact ordinary-discard income qualification for admission and parent validation. Preserve physical revival and native conditional income through claim, decline, full-hand exchange and original allow/cancel. --- docs/CURRENT_STATUS.md | 23 ++++ docs/IMPLEMENTATION_STATUS.md | 30 +++++ docs/RULE_DECISIONS.md | 2 +- docs/SEMUTA_DISCARD_CONTINUATIONS.md | 8 +- docs/SEMUTA_SOURCE_UPDATE_20260907.md | 74 +++++++++++ game/engine.ts | 19 +-- game/ordinary-discard-income.ts | 21 +++ game/reference.ts | 14 +- tests/fixture-semuta-ghola-income.ts | 98 ++++++++++++++ tests/semuta-ghola-income-controls.test.tsx | 120 ++++++++++++++++++ tests/semuta-ghola-income.test.ts | 134 ++++++++++++++++++++ 11 files changed, 524 insertions(+), 19 deletions(-) create mode 100644 game/ordinary-discard-income.ts create mode 100644 tests/fixture-semuta-ghola-income.ts create mode 100644 tests/semuta-ghola-income-controls.test.tsx create mode 100644 tests/semuta-ghola-income.test.ts diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index f0c4a8b6..48cfc9e2 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -16,6 +16,29 @@ open mode gates. ## Current checkpoint and work +**Semuta and native Ghola income — 24 October. Prototyped / Partial.** +Semuta now recovers a completed Ghola while its original Tleilaxu one-spice +income is pending. One shared source predicate retains exact native ownership, +recipient, amount and empty passes; other response/parent boundaries remain. +Claim/Continue preserves physical returns and normal revival quotas, then +restores the same allow/cancel response. Tleilaxu self-income remains valid. + +Disjoint source/control lanes, bounded review and owner integration pass +**10 files156/156**, types/lint/build, eight actual gameplay/control/table-rendering +journeys and eight existing-profile continuations (32 accepted proposals). +Real native setup, prior Storm casualty and paid Box recovery supply160 source +actions; Semuta/Karama/full-hand acquisitions are explicitly conserved staging. +Normal/full hand × claim/pass × allow/cancel then resumes real shipment10, +ground20 and end. Controlled own-Tleilaxu and prior leader-death cases are +separate. The committed engine reproduces the old missing offer. The first +private runtime runner selected a noneligible no-Karama responder; its repair +passes with all application source unchanged, retaining the earlier passing +tests/types/lint/build by exact source identity. +[Authority, exact evidence and limits](SEMUTA_SOURCE_UPDATE_20260907.md#ghola-and-conditional-native-income--24-october-2026). +Other producers/parents, complete modes, comprehensive/browser/deployed acceptance +and full AI refinement remain unfinished. No saved-room/D1/reset/restart/deployment +operation. + **Basic native Mirror/Stone — 24 October. Prototyped / Partial.** Bare Basic Ecaz/Moritani families, including selected Ix, now admit the physical Mirror and original Stone through existing native resolution. Preserve fixed diff --git a/docs/IMPLEMENTATION_STATUS.md b/docs/IMPLEMENTATION_STATUS.md index 9a651d84..27826b0b 100644 --- a/docs/IMPLEMENTATION_STATUS.md +++ b/docs/IMPLEMENTATION_STATUS.md @@ -1,5 +1,35 @@ # Dune implementation status +## 24 October 2026 — Semuta before native Ghola income + +A completed Ghola now offers its actual used card while the original Tleilaxu +one-spice income is suspended. Reuse one exact source predicate in fresh-offer +admission and saved-parent validation; no fields or signature formats change. +Claim/pass never repeats physical revival, consumes normal quotas or precredits +income. Restore the original allowed/canceled payment once, including Tleilaxu +self-income. Other pending effects and market parents keep their existing guards. + +Disjoint source/control lanes, bounded review and owner integration pass10 files +156/156, types/lint/build, eight actual gameplay/control/GameTable SSR journeys +and eight minimal-profile continuations (32 accepted proposals). Actual native +setup, real Storm loss and paid Box recovery supply160 source actions; original +Semuta/Karama/full-hand acquisitions are explicitly conserved staging. +Normal/full hand, claim/pass and allow/cancel combinations preserve the source, +then real shipment10/ground20/end finish. Controlled own-Ghola and prior native +leader death separately cover self-income and leader return without also +returning the original Tank. + +A committed-engine probe fails the missing-offer assertion. The first new +runtime runner incorrectly demanded an action from a no-Karama seat; selecting +only actual eligible own-view proposals fixes that private runner. Application +source stays byte-identical, so passing tests/types/lint/build are retained by +source hash and a separate runtime report supplies the final proof. +[Authority, proof records and limits](SEMUTA_SOURCE_UPDATE_20260907.md#ghola-and-conditional-native-income--24-october-2026). +No new publisher timing ruling, strategy/calibration, full-mode/combination or +browser/deployed certificate, comprehensive assurance or saved-room/D1/reset/ +restart/deployment operation. + + ## 24 October 2026 — Basic native Mirror and Stone Bare Basic native E3 with CHOAM/Richese, including selected Ix, now uses original diff --git a/docs/RULE_DECISIONS.md b/docs/RULE_DECISIONS.md index e1519ce0..e2504c6d 100644 --- a/docs/RULE_DECISIONS.md +++ b/docs/RULE_DECISIONS.md @@ -943,7 +943,7 @@ These are unresolved in the cited records. Do not infer an answer from elapsed t | Stone mixed-force allocation timing | The older timing question remains unanswered by a source. Under 14 October choose-recommended, new original outcome-ambiguous Stone/Mirror battles provisionally commit each ambiguous side's undialed physical total after revealed modes/actual withdrawal and decisive Traitor/explosion branches. Storm-order owners choose privately; every original required value stays sealed until all commit. The eventual winner retains all legal typed casualty choices with the count. Ordinary/invariant and unstamped legacy frames keep their existing path. Publisher physical-count text and supplied Advanced physical12/logical10 casualty timing provide the numerical rules, not this online commitment convention. | [Source, versioned procedure and bounded proof](STONE_BURNER_ENGINE_AUDIT.md#outcome-changing-physical-totals--18-october-2026) | | Recruits late accounting and rates | The earlier one/two-further-free question remains unanswered. The prototype preserves paid spice, deducts recorded free usage from the doubled current rate and all returns from the total cap: two-free/one-paid Atreides can take two more free. Clean classic Homeworlds now provisionally recheck native thresholds on separate accepted requests, retaining completed prices/classifications; this is not a numerical user ruling or newly found timing FAQ. Unknown ledgers, pending repricing, later Fremen grants and same-turn replay remain guarded. | [Source alternatives](RECRUITS_RULES.md), [current native runtime](RECRUITS_RUNTIME.md#classic-homeworld-composition--16-october-2026) | | Sapho remaining timing | First versus unspent Advanced Guild priority, partly completed combined turns and other ordered phases remain open. Cyclic current-lot duration and the new shared final battle opportunity are explicit provisional protocols, not publisher rulings. Old battles retain early timing. | [Source audit](JUICE_OF_SAPHO_SOURCE_UPDATE.md), [current aggressor contract](SAPHO_AGGRESSOR.md), [historical battle review](JUICE_OF_SAPHO_BATTLE_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) | +| Semuta Drug | Full-hand exchange is user-approved; neutral all-seat acknowledgements remain the explicit application policy. Completed Ghola may offer its physical card before the original conditional native Tleilaxu one-spice income, including self-income; claim/pass preserves returned pieces and normal quotas, then resumes the same allow/cancel response once. This admits no unrelated response or market parent. Other 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 modifiers, Mentat empty-hand fallback, late/mirror/numeric Diplomat defenses and simultaneous optional timing remain source gaps. Owned multi-pile Smuggler/Sandmaster allocation and printed alternate/hybrid defense roles now have connected, explicitly interpreted paths. The bounded Diplomat retreat prototype chooses printed/copied strength and surviving-loser retreat before casualties; this is not an official priority clarification. | [Material boundaries and retreat](LEADER_SKILLS_RULES.md#bounded-diplomat-retreat-interpretation), [Defense limits](DIPLOMAT_DEFENSE.md#evidence-and-remaining-scope) | | Smuggler collection strength | The older source check/question produced no numerical answer. Under the later choose-recommended instruction, provisionally cap by copied/printed disc plus declared skill bonuses and KH2, retaining reveal piles and delayed survival settlement. Actual Artillery zeroing voids collection; the base Stunner remains a projectile. Sole own Traitor bypasses weapons, so it retains collection. Printed bounty does not change; this is not a publisher ruling. Other exceptional compositions remain separate. | [Source and explicit policy](LEADER_SKILLS_RULES.md#smuggler-modifier-policy--17-october-2026), [runtime contract](SMUGGLER_BATTLE.md#provisional-modifier-policy--17-october-2026) | | Ordinary Guild cancellation | Unaffordable repricing, replacement/retry/decline, consumed opportunity, contributor adjustment and ordering with special shipment-stop. The affordability question was already sent. | [Material timing](GUILD_RATE_KARAMA_RULES.md#material-unresolved-timing) | diff --git a/docs/SEMUTA_DISCARD_CONTINUATIONS.md b/docs/SEMUTA_DISCARD_CONTINUATIONS.md index 1f746c49..c87dcc5c 100644 --- a/docs/SEMUTA_DISCARD_CONTINUATIONS.md +++ b/docs/SEMUTA_DISCARD_CONTINUATIONS.md @@ -182,8 +182,12 @@ resumes once, with the snapshot entitled only to CHOAM. Completed ordinary Ghola disposal may now offer Semuta during battle as well. Revival has already committed; the receipt binds the returned leader/forces and original battle, including a sealed opposing plan. Claim or decline restores -planning without reviving again or changing that plan. This does not admit -competing parents or market-specific Ghola interruptions. +planning without reviving again or changing that plan. The +[conditional native-income follow-up](SEMUTA_SOURCE_UPDATE_20260907.md#ghola-and-conditional-native-income--24-october-2026) +also parks the original one-spice Tleilaxu response from that Ghola, including +self-income, until claim/Continue finishes. It then allows/cancels once; returned +pieces and normal revival allowances stay committed. Other competing parents +and market-specific Ghola interruptions remain outside this admission. Completed paid Box disposal may now offer Semuta during battle as well. Only the used Box is fresh; payment, private acquisition, shuffled pile and original diff --git a/docs/SEMUTA_SOURCE_UPDATE_20260907.md b/docs/SEMUTA_SOURCE_UPDATE_20260907.md index e71ea29e..dd49ba8e 100644 --- a/docs/SEMUTA_SOURCE_UPDATE_20260907.md +++ b/docs/SEMUTA_SOURCE_UPDATE_20260907.md @@ -2,6 +2,80 @@ 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. +## Ghola and conditional native income — 24 October 2026 + +**Prototyped / Partial.** A completed ordinary Ghola can offer its used physical +card to Semuta while its native Tleilaxu one-spice income is pending. The force +or leader return commits first; the same original income response stays parked +until the fresh discard is claimed or every seat continues. Neither claim nor +decline revives again, changes normal revival allowances or pays/refunds spice. +The ordinary response then allows or cancels that one payment under its +existing native controls. Tleilaxu's own Ghola also earns the one-spice reward. + +One shared source predicate governs admission and the saved parent: Ghola with +a seated Tleilaxu has that faction's original revival-income response, the actual +Ghola player as recipient, amount1 and no prior passes. Other ordinary cards, +and Ghola without Tleilaxu, keep their existing no-response boundary. Signature, +control and physical-effect checks remain. No serialized field or signature +format changes; no arbitrary pending response becomes eligible. + +The original Semuta full-hand exchange and single-candidate completion apply. +All seats retain the neutral persistent Continue offer; a legal holder can +Commit, spending its physical Semuta once and acquiring the actual used Ghola. +The already returned piece and original conditional income are independent of +which player takes that card. Other nested parents, CHOAM market Ghola, +unfinished producers and complete-mode/combination acceptance remain separate. +This composes printed Ghola, native income and Semuta with the existing saved +discard protocol; it makes no new publisher timing or disposal ruling. + +### Native Ghola source and proof + +The inherited native Basic Atreides/Emperor/Richese/Tleilaxu programme uses +original setup, actual separate paid desert shipments, the later Storm casualty, +settled Emperor bribe and original cache Box. Its real paid Box recovery reaches +a clean pre-Ghola boundary after160 actions. Only then are the original cache +Semuta and original deck Karama transferred into Richese/Emperor hands as +explicit conserved rule-case acquisitions. Full-hand cases move three original +ordinary deck cards into Richese hand. No response, receipt, wallet, force, +phase or completed revival is installed in that source. + +Nine engine and three actual-source control cases cover the fresh neutral offer, +claim/pass × allowed/canceled payment, approved full-hand exchange, original +event/busy callbacks, stale/tampered rejection and once-only returns/normal +quotas. Controlled prior acquisition/casualty separately exercises Tleilaxu +using its own Ghola; controlled prior leader death proves recovery of a leader +does not also return the separate Tank. These controlled variants are explicit, +not natural death/acquisition or complete-game claims. + +The affected **10-file156/156** union, npm run check:quick and production build +pass. Eight actual-control/GameTable SSR programmes cover normal/full hand, +claim/pass and native allow/cancel, then real city shipment10, ground20 and end. +Eight existing-profile continuations accept32 legal proposals from eligible +own views; all claim Ghola and allow the original income. Human/runtime counter +branches separately prove actual cancellation. No strategy tuning or relative +strength calibration occurs. + +A committed-engine loader reproduces the old missing-offer assertion at +19e554666ee4bf55dd33d162a6a1f987a699ed03. The initial private runtime probe asked +a no-Karama seat for an optional income response, for which its policy correctly +offered no action. The corrected runner uses eligible projected controllers. +No application/helper/component/test/doc source changed between those probes, +so the passing affected tests/types/lint/build are retained by exact identity. +The initial failure and separate repaired runtime report remain preserved. + +Private artifacts: /home/quintine/.local/share/dune/checkpoints/20261024-semuta-ghola-income/, +final-verification.json (explicitly composed from retained frozen-integrated/ +report.json and runtime-eligible/report.json), runtime-smoke.json, baseline-result.json, +private HTML/logs, verified-files.json and documentation-overlay.json. +Before final prose: base19e554666ee4bf55dd33d162a6a1f987a699ed03, +source tree6e423ffb032bb8a35993de3061cd09fda0fde08c4ae56d772afc492b4fb1183f, +2665 tracked/nonignored files unchanged throughout both reports. Only four +checkpoint-prose/reference overlays follow; local links/evidence and reference +syntax/keys are checked separately. Framework aliases/CSS stub provide rendering +smoke, not browser/visual/deployed acceptance. Wider producers, parent/module +combinations, complete modes and comprehensive assurance remain open. +No saved-room/D1/reset/restart/deployment operation occurs. + ## Fresh Basic starts and Ambassador discards — 24 October 2026 Fresh public **bare Basic Richese previews** now initialize Semuta before the diff --git a/game/engine.ts b/game/engine.ts index 897e3779..a3ab5cc9 100644 --- a/game/engine.ts +++ b/game/engine.ts @@ -1,3 +1,4 @@ +import { ordinaryDiscardIncomeMatches } from './ordinary-discard-income'; import { gholaForcePreparations } from './ghola-force-preparation'; import { earlyAuctionFunding } from './early-auction-funding'; import { recordMovementDiscardKnowledge, knownMovementDiscards, recordMovementBoxKnowledge, recordMovementClaimKnowledge, clearMovementDiscardKnowledge, type MovementDiscardKnowledge } from './movement-discard-knowledge'; @@ -6440,16 +6441,8 @@ function treacheryDiscardIntegrity(g: Game) { !c.resume.decision && !c.resume.pendingKarama && !c.resume.phaseOpening, 'The ordinary card has an unexpected suspended decision.', ); - const income = c.resume.response, - tleilaxu = byFaction(g, 'tleilaxu'); requireRule( - c.effect === 'ghola' && tleilaxu - ? income?.kind === 'revivalIncome' && - income.owner === tleilaxu.id && - income.recipient === c.player && - income.amount === 1 && - income.passed.length === 0 - : !income, + ordinaryDiscardIncomeMatches(g, c), 'The consumed card no longer matches its pending revival income.', ); requireRule( @@ -7779,9 +7772,11 @@ function semutaOfferSupported( continuation.kind === 'karamaEmperorShareDiscard' || continuation.kind === 'karamaIncomeDiscard' || continuation.kind === 'karamaShipmentDiscard' || - ((continuation.kind === 'ornithopterDiscard' && continuation.source === 'cunning' - ? continuation.resume.response?.kind === 'nexusGuildCunning' - : !continuation.resume.response) && !continuation.resume.decision && + ((continuation.kind === 'ordinaryCardDiscard' + ? ordinaryDiscardIncomeMatches(g, continuation) + : continuation.kind === 'ornithopterDiscard' && continuation.source === 'cunning' + ? continuation.resume.response?.kind === 'nexusGuildCunning' + : !continuation.resume.response) && !continuation.resume.decision && !continuation.resume.pendingKarama && (continuation.kind === 'amalDiscard' ? continuation.resume.phaseOpening?.passed.length === 0 diff --git a/game/ordinary-discard-income.ts b/game/ordinary-discard-income.ts new file mode 100644 index 00000000..b291984c --- /dev/null +++ b/game/ordinary-discard-income.ts @@ -0,0 +1,21 @@ +import type { Game } from './engine'; + +type OrdinaryDiscard = Extract< + NonNullable['continuation'], + { kind: 'ordinaryCardDiscard' } +>; + +/** Only Ghola creates this conditional income after its physical effect. The + * same source qualification governs the fresh offer and its saved parent. */ +export function ordinaryDiscardIncomeMatches( + game: Pick, + continuation: Pick, +): boolean { + const income = continuation.resume.response; + const tleilaxu = game.players.find(player => player.faction === 'tleilaxu'); + return continuation.effect === 'ghola' && tleilaxu + ? income?.kind === 'revivalIncome' && income.owner === tleilaxu.id && + income.recipient === continuation.player && income.amount === 1 && + income.passed.length === 0 + : !income; +} diff --git a/game/reference.ts b/game/reference.ts index feeb07b6..a67161f9 100644 --- a/game/reference.ts +++ b/game/reference.ts @@ -1300,6 +1300,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'The native Advanced BG extra-charity cancellation cost also offers printed Karama after the genuine claim allowance is consumed but before denial; claim/pass withholds the same extra allowance and cannot renew that turn’s claim. Wealthy Basic BG retains its original ordinary-charity guard. Allied Emperor gift and extra-revival cancellation costs likewise offer after original native setup, actual casualty/alliance and declaration: recovering the card does not transfer the canceled gift, return a counter or award extra revival. Original ally, populations, wallets, Tanks and native closing controls remain committed.', 'Original native Homeworld battle-reward cancellation costs also offer printed Karama: Advanced Harkonnen capture, native CHOAM Auditor and quoted force-payment income. Recovery preserves the accepted battle, paid support, leader death and original casualty/cleanup order, then completes the same denial. It cannot sample or transfer a captive, pay/inspect an audit, refund support or credit canceled income. An audit cancellation leaves any earlier allowed force-payment income intact. Existing exact roster, private-custody and source guards remain.', 'An ordinary Ghola played during battle also offers its consumed card after revival. Claim or Continue preserves the revived leader or forces and the opposing sealed plan, then normal planning resumes. It cannot revive again by resuming the receipt.', + 'A completed Ghola can also be claimed while its native Tleilaxu one-spice income is waiting. The piece has already returned. Claim or Continue restores that same allow-or-cancel response without another revival or advance payment; normal revival allowances stay unchanged. This includes Tleilaxu using its own Ghola. Other pending effects retain their existing timing.', 'A completed paid Nullentropy Box search during battle offers only the used Box. Claim or Continue preserves the two-spice payment, private recovered card, already-shuffled pile and sealed opposing plan. The recovered card may be used in normal legal planning; the search is not repeated.', 'A clean Distrans transfer during battle also offers only the used Distrans. The private card has already reached its recipient; claim or Continue cannot repeat the gift or change a sealed plan. A recipient holding Semuta may claim the used Distrans and keep the already-transferred card.', 'Reactive Baliset and La La La also offer their used card after the accepted cost. The original uncommitted move or free-revival request is already stopped: claim or Continue installs the original restriction without moving forces, consuming movement, reviving or charging the denied request. Another legal move or paid revival remains available. Movement-card, Ambassador, Nexus and other competing parents remain separate.', @@ -1315,7 +1316,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, proactive or reactive La La La/Baliset and ordinary Trip to Gamont Worthless use, accepted printed Jubba before storm protection, Ix-deck Thumper/Amal and three battle discard stages, including before native Advanced Harkonnen capture. 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, other private discards, nested responses and other pending rewards remain unfinished. Printed native Guild permission/rate costs now use a dedicated unpaid shipment continuation for reserve, cross/return and typed Homeworld sources; claim/pass applies the same denial/repricing once without delivery replay or invented funding. Printed shipment-price activation now has its own continuation: claim/decline creates the same original price once. Recovered physical cost retains the beneficiary/creator but loses provisional refund eligibility even after rediscard; the same recovered card can pay a real stop without being returned to its former owner. Printed native special costs now resume one committed native effect through effect/complete/cashout stages. Original Emperor post-revival disposal is preserved; CHOAM selected cards are a later private simultaneous batch after one credit. Existing native once-use, joint ordering and typed parent remain. Native BG physical Worthless cost now precedes the single original conversion emitter; the separately printed cancellation cost retains that pending use and still denies it. All four use bindings, incoming reservations and older passes remain. Allowed converted prices carry claimed-cost provenance through actual later rediscard/refund. Fresh private Discovery composition now connects both physical Shrine-role costs without changing original effects or printed identity. Fresh bare public Basic starts now initialize Semuta with selected native families. Clean CHOAM/Ixian Ambassador private batches, including BG copies, retain the original entry, already-earned credit and one later replacement draw. Older games and optional/Advanced scopes remain separate; nested originating-card coverage remains unfinished.", + "Completed Ghola now offers Semuta before its exact conditional native one-spice income, including self-income. Claim/pass preserves returned pieces and normal quotas, then restores the original allow/cancel response once. Other parent guards remain. 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, proactive or reactive La La La/Baliset and ordinary Trip to Gamont Worthless use, accepted printed Jubba before storm protection, Ix-deck Thumper/Amal and three battle discard stages, including before native Advanced Harkonnen capture. 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, other private discards, nested responses and other pending rewards remain unfinished. Printed native Guild permission/rate costs now use a dedicated unpaid shipment continuation for reserve, cross/return and typed Homeworld sources; claim/pass applies the same denial/repricing once without delivery replay or invented funding. Printed shipment-price activation now has its own continuation: claim/decline creates the same original price once. Recovered physical cost retains the beneficiary/creator but loses provisional refund eligibility even after rediscard; the same recovered card can pay a real stop without being returned to its former owner. Printed native special costs now resume one committed native effect through effect/complete/cashout stages. Original Emperor post-revival disposal is preserved; CHOAM selected cards are a later private simultaneous batch after one credit. Existing native once-use, joint ordering and typed parent remain. Native BG physical Worthless cost now precedes the single original conversion emitter; the separately printed cancellation cost retains that pending use and still denies it. All four use bindings, incoming reservations and older passes remain. Allowed converted prices carry claimed-cost provenance through actual later rediscard/refund. Fresh private Discovery composition now connects both physical Shrine-role costs without changing original effects or printed identity. Fresh bare public Basic starts now initialize Semuta with selected native families. Clean CHOAM/Ixian Ambassador private batches, including BG copies, retain the original entry, already-earned credit and one later replacement draw. Older games and optional/Advanced scopes remain separate; nested originating-card coverage remains unfinished.", }, { area: 'Player controls' as const, @@ -1327,7 +1328,7 @@ export const RULE_TOPICS: RuleTopic[] = [ area: 'AI' as const, status: 'Partial' as const, detail: - "All four AI profiles use their own projected hand to commit or pass the exact current event, and can choose only privately projected committed candidates. This is minimal legal participation, not tuned strategy. For both new native permission and rate cost parents, all four actual original-state profiles recover the exact cost through legal actions and preserve the original denial/repricing. No tuning or calibration. All four original-state profiles claim the price cost; a subsequent real owner action executes the retained bank-paid shipment. This is legal source participation, not a new strategy preference or calibration. All four existing profiles recover the original Guild stop cost via legal private actions, and the native prevention then executes once. No new preference, tuning or calibration. For each native BG cost and printed counter-cost family, all four existing profiles recover only the current fresh card through their private legal action. Native response allowance/denial remains separate; subsequent owner actions exercise the real retained source. No tuning or calibration. Thirty-two controlled legal-policy probes cover both physical Shrine-role events in both modes; no calibration. Four existing profiles also complete actual public Basic BG-copy CHOAM Ambassador reactions through twenty legal proposals; no new strategy or calibration.", + "Eight conditional Ghola-income continuations accept32 legal proposals across normal/full hands and all four existing profiles; all recover the card and allow the real income. No strategy tuning. All four AI profiles use their own projected hand to commit or pass the exact current event, and can choose only privately projected committed candidates. This is minimal legal participation, not tuned strategy. For both new native permission and rate cost parents, all four actual original-state profiles recover the exact cost through legal actions and preserve the original denial/repricing. No tuning or calibration. All four original-state profiles claim the price cost; a subsequent real owner action executes the retained bank-paid shipment. This is legal source participation, not a new strategy preference or calibration. All four existing profiles recover the original Guild stop cost via legal private actions, and the native prevention then executes once. No new preference, tuning or calibration. For each native BG cost and printed counter-cost family, all four existing profiles recover only the current fresh card through their private legal action. Native response allowance/denial remains separate; subsequent owner actions exercise the real retained source. No tuning or calibration. Thirty-two controlled legal-policy probes cover both physical Shrine-role events in both modes; no calibration. Four existing profiles also complete actual public Basic BG-copy CHOAM Ambassador reactions through twenty legal proposals; no new strategy or calibration.", }, { area: 'Documentation' as const, @@ -1339,7 +1340,7 @@ export const RULE_TOPICS: RuleTopic[] = [ area: 'Verification' as const, status: 'Partial' as const, detail: - "Genuine setup, custody, hidden-hand parity, saved JSON, authenticated SQLite continuation and legal AI participation cover clean 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, a private normal-auction Ixian ally replacement and three battle discard stages. Other private producers, Karama uses, other reactive Worthless uses, pending rewards, full games and live deployment remain unverified. Earlier thirteen-file158/158 passes, including12 new cases. Thirty-two original-action runtime/policy paths and four original-action390px journeys pass; two separately controlled cross/typed active-price probes retain the original independent grant. No worker/D1/real-room/deployed assurance or complete-mode claim. Earlier eighteen-file234/234 passes, including8 new cases. Fourteen original-action runtime/policy paths and four actual original-action390px journeys pass; two explicitly controlled extra-stop-card acquisitions prove unclaimed refunds. The original same-card stop uses no extra card or staged acquisition. No worker/D1/saved/deployed or complete-mode assurance. Earlier sixteen-file224/224 passes with13 new behavior cases. Twenty-six runtime/policy paths pass:8 original-action Guild paths including4 profiles,18 explicitly controlled native-effect paths. Four actual390px journeys pass (2 original/2 controlled). Existing income/refund consumers use actual neutral passes. No worker/D1/saved/deployed or complete-mode claim. Earlier nineteen-file238/238 passes with8 new cases. Twenty-six original-action runtime/policy paths and four actual original-action390px journeys pass. One controlled acquisition/phase probe uses a REAL subsequent CHOAM rediscard and Guild stop without force/wallet staging or duplicate refund. Separate2 controlled Kull parent probes and3 repaired focused cases pass. No worker/D1/saved/deployed certification. Eight new Shrine cases pass in the57-case affected union, with eight controlled runtime paths and two actual controlled390px journeys; no natural draw/entry, recovery or deployed acceptance. Fresh Basic/Ambassador evidence passes twelve-file142/142, types/lint/build, ten original-action handler/table-rendering programmes and four existing profiles with twenty legal proposals. Public setup, actual one-spice Semuta auction, card purchase, placement/shipment and two actual ordinary movement entries retain35 main plus10 cache. Only unopened positions and original scalar/packet lotteries are selected. Full-hand/own-card edges separately stage conserved card transfers. Stale battle acknowledgements/module-rejection expectations and fixture types were repaired; representative old failures reproduced against the previous engine. Framework aliases/CSS stub; no browser, visual, saved-room or deployed acceptance. Wider nested/provenance scope and complete modes remain open.", + "Current conditional-Ghola proof passes10 files156/156, types/lint/build and eight actual-control/GameTable SSR journeys. Eight existing profiles accept32 proposals. Native160-action source plus explicitly conserved card staging covers claim/pass, full hands and allow/cancel; separate controlled self-income/leader cases retain physical identity and normal quotas. A private runner eligibility repair retains earlier passing checks by unchanged source identity. Full modes, wider parents, browser/deployed and comprehensive assurance remain open. The following counts are historical family evidence. Genuine setup, custody, hidden-hand parity, saved JSON, authenticated SQLite continuation and legal AI participation cover clean 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, a private normal-auction Ixian ally replacement and three battle discard stages. Other private producers, Karama uses, other reactive Worthless uses, pending rewards, full games and live deployment remain unverified. Earlier thirteen-file158/158 passes, including12 new cases. Thirty-two original-action runtime/policy paths and four original-action390px journeys pass; two separately controlled cross/typed active-price probes retain the original independent grant. No worker/D1/real-room/deployed assurance or complete-mode claim. Earlier eighteen-file234/234 passes, including8 new cases. Fourteen original-action runtime/policy paths and four actual original-action390px journeys pass; two explicitly controlled extra-stop-card acquisitions prove unclaimed refunds. The original same-card stop uses no extra card or staged acquisition. No worker/D1/saved/deployed or complete-mode assurance. Earlier sixteen-file224/224 passes with13 new behavior cases. Twenty-six runtime/policy paths pass:8 original-action Guild paths including4 profiles,18 explicitly controlled native-effect paths. Four actual390px journeys pass (2 original/2 controlled). Existing income/refund consumers use actual neutral passes. No worker/D1/saved/deployed or complete-mode claim. Earlier nineteen-file238/238 passes with8 new cases. Twenty-six original-action runtime/policy paths and four actual original-action390px journeys pass. One controlled acquisition/phase probe uses a REAL subsequent CHOAM rediscard and Guild stop without force/wallet staging or duplicate refund. Separate2 controlled Kull parent probes and3 repaired focused cases pass. No worker/D1/saved/deployed certification. Eight new Shrine cases pass in the57-case affected union, with eight controlled runtime paths and two actual controlled390px journeys; no natural draw/entry, recovery or deployed acceptance. Fresh Basic/Ambassador evidence passes twelve-file142/142, types/lint/build, ten original-action handler/table-rendering programmes and four existing profiles with twenty legal proposals. Public setup, actual one-spice Semuta auction, card purchase, placement/shipment and two actual ordinary movement entries retain35 main plus10 cache. Only unopened positions and original scalar/packet lotteries are selected. Full-hand/own-card edges separately stage conserved card transfers. Stale battle acknowledgements/module-rejection expectations and fixture types were repaired; representative old failures reproduced against the previous engine. Framework aliases/CSS stub; no browser, visual, saved-room or deployed acceptance. Wider nested/provenance scope and complete modes remain open.", evidence: [ 'tests/basic-semuta-ambassador.test.ts', 'tests/fixture-basic-semuta-ambassador.ts', @@ -1347,6 +1348,10 @@ export const RULE_TOPICS: RuleTopic[] = [ 'tests/semuta-recovery.test.ts', 'tests/bot-semuta.test.ts', 'tests/semuta-reaction-controls.test.tsx', + 'game/ordinary-discard-income.ts', + 'tests/fixture-semuta-ghola-income.ts', + 'tests/semuta-ghola-income.test.ts', + 'tests/semuta-ghola-income-controls.test.tsx', 'tests/semuta-drug.test.ts', 'tests/treachery-discard-continuations.test.ts', 'tests/multiplayer-discard-continuations.test.ts', @@ -2644,6 +2649,7 @@ export const RULE_TOPICS: RuleTopic[] = [ 'A Basic ground-movement promise can also require Ghola force preparation. The guide returns physical Tanks to reserves, then preserves an unused affordable shipment and one later matching ground move. Ghola itself does not fulfill movement or consume normal revival allowances. A known discarded Ghola may be recovered with the paid Box and played again.', 'Returned forces go to reserves. A leader revived with this card can fight again this turn, including in another territory. Atreides can also select a dead Kwisatz Haderach; revive it separately from its accompanying leader. Returning a controlled foreign ghola does not transfer it back to its original faction. Additional advanced and expansion interactions remain under audit.', 'An actual Ecaz native Ghola return uses that disc in its current ordinary cohort without consuming the ordinary phase slot. A repeat waits for the remaining native cohort and another opening. Independent Duke Ghola preserves a satisfied five-Tanks opening and returns him set aside; only Ecaz may revive him.', + 'Semuta may recover the used Ghola before its native Tleilaxu one-spice income resolves. Keep the returned piece and normal revival allowance; claim or Continue then restores the original income response. Taking the card never repeats its revival.', 'A saved play preserves the revived pieces, any earned technology income and the discarded card. Recovery restores any pending Tleilaxu income response without reviving or paying twice. Private preparation guidance waits for recovery to finish.', ], checklist: [ @@ -2652,7 +2658,7 @@ export const RULE_TOPICS: RuleTopic[] = [ status: 'Partial', detail: 'Ordinary and typed forces, controlled leaders and Kwisatz retain original use and custody. Actual Ecaz native Ghola/foreign returns now record completed native cohort identity; independent Duke resurrection preserves its five-Tanks opening without aligning death counters. Ecaz-only living set-aside custody remains; broader combinations and assurance stay unfinished.', - evidence: ['game/ghola-force-preparation.ts', 'game/engine.ts', 'game/ecaz-duke-revival.ts', 'game/ecaz-revival-cohort.ts', 'game/revival.ts'], + evidence: ['game/ordinary-discard-income.ts', 'game/ghola-force-preparation.ts', 'game/engine.ts', 'game/ecaz-duke-revival.ts', 'game/ecaz-revival-cohort.ts', 'game/revival.ts'], }, { area: 'Player controls', diff --git a/tests/fixture-semuta-ghola-income.ts b/tests/fixture-semuta-ghola-income.ts new file mode 100644 index 00000000..931311e1 --- /dev/null +++ b/tests/fixture-semuta-ghola-income.ts @@ -0,0 +1,98 @@ +import assert from 'node:assert/strict'; +import { viewGame, type Action, type Game } from '../game/engine'; +import { SEMUTA_DRUG_ID } from '../game/semuta-drug'; +import { + createTruthMovementBoxGholaFixture, actTruthMovementGhola, + advanceTruthMovementGhola, truthMovementGholaClean, truthMovementGholaInventory, + truthMovementGholaPlayer, type TruthMovementGholaStep, +} from './fixture-truth-movement-ghola'; + +export const semutaGholaIncomePlayer = truthMovementGholaPlayer; +export const semutaGholaIncomeInventory = truthMovementGholaInventory; +export const semutaGholaIncomeClean = truthMovementGholaClean; +export const actSemutaGholaIncome = actTruthMovementGhola; +export type SemutaGholaIncomeStep = TruthMovementGholaStep; + +/** Pass only the current fresh group; do not automatically allow its suspended + * income or any later response. Every acknowledgement is a real native action. */ +export function passSemutaGholaIncome(source: Game): Game { + const pending = source.pendingTreacheryDiscard; + assert.equal(pending?.reaction?.stage, 'offer'); + const event = pending!.batch.event; + let game = source; + for (const p of source.players) { + if (!game.pendingTreacheryDiscard?.reaction || + game.pendingTreacheryDiscard.reaction.stage !== 'offer') break; + if (!game.pendingTreacheryDiscard.reaction.passed.includes(p.id)) + game = actSemutaGholaIncome(game, p.id, { type: 'semutaPass', event }); + } + return game; +} +export function claimSemutaGholaIncome(source: Game, actor: string, card: string): Game { + const event = source.pendingTreacheryDiscard!.batch.event; + let game = actSemutaGholaIncome(source, actor, { type: 'semutaCommit', event }); + if (game.pendingTreacheryDiscard?.reaction?.stage === 'select') + game = actSemutaGholaIncome(game, actor, { type: 'semutaSelect', event, card }); + return game; +} +/** Complete only actual response acknowledgements. Printed cancellation is an + * explicit caller action, not a fixture-installed denied receipt. */ +export function allowSemutaGholaIncome(source: Game): Game { + let game = source; + for (let count = 0; game.response && count < game.players.length; count++) { + const player = game.players.find(p => !game.response!.passed.includes(p.id)); assert.ok(player); + game = actSemutaGholaIncome(game, player.id, { type: 'passResponse' }); + } + assert.equal(game.response, null); return game; +} + +/** Genuine native source/casualty/Box acquisition, followed by explicitly + * controlled conserved Semuta and Karama acquisitions. No response, receipt, + * force, wallet, phase or finished revival is installed. */ +export function createSemutaGholaIncomeFixture(options: { fullHand?: boolean } = {}) { + const original = createTruthMovementBoxGholaFixture({ tleilaxu: true }); + const owner = original.target, claimant = 'richese', incomeOwner = 'tleilaxu', + counterOwner = original.asker; + const actions: SemutaGholaIncomeStep[] = [...original.actions]; + const originalInventory = semutaGholaIncomeInventory(original.game); + let game = actSemutaGholaIncome(original.game, owner, original.boxStep.action, actions); + const search = viewGame(game, owner).nullentropy?.search; + if (search) { + assert.ok(search.cards.some(card => card.id === original.ghola)); + game = actSemutaGholaIncome(game, owner, + { type: 'decision', event: search.event, card: original.ghola }, actions); + } + // A sole eligible card is acquired automatically by the original Box rule. + game = advanceTruthMovementGhola(game, semutaGholaIncomeClean, actions); + assert.ok(semutaGholaIncomePlayer(game, owner).hand.some(card => card.id === original.ghola)); + assert.equal(semutaGholaIncomePlayer(game, owner).spice, 11); + assert.equal(game.pendingNullentropy, null); + assert.equal(game.semutaPreview, true); + assert.deepEqual(semutaGholaIncomeInventory(game), originalInventory); + const afterBox = structuredClone(game); + + const cacheIndex = game.richeseCache!.findIndex(card => card.id === SEMUTA_DRUG_ID); + assert.ok(cacheIndex >= 0, 'Acquire the original physical Semuta from its native cache'); + const semuta = game.richeseCache!.splice(cacheIndex, 1)[0]; + semutaGholaIncomePlayer(game, claimant).hand.push(semuta); + const counterIndex = game.deck.findIndex(card => card.effect === 'karama'); assert.ok(counterIndex >= 0); + const counter = game.deck.splice(counterIndex, 1)[0]; + semutaGholaIncomePlayer(game, counterOwner).hand.push(counter); + if (options.fullHand) while (semutaGholaIncomePlayer(game, claimant).hand.length < 4) { + const index = game.deck.findIndex(card => ['projectile', 'poison', 'shield', 'snooper', 'worthless'].includes(card.kind)); + assert.ok(index >= 0); semutaGholaIncomePlayer(game, claimant).hand.push(game.deck.splice(index, 1)[0]); + } + assert.equal(semutaGholaIncomePlayer(game, claimant).hand.length, options.fullHand ? 4 : 1); + assert.ok(game.players.every(p => p.hand.length <= 4)); + assert.deepEqual(semutaGholaIncomeInventory(game), originalInventory); + assert.ok(semutaGholaIncomeClean(game)); + const gholaAction: Action = { type: 'card', card: original.ghola, amount: 1, elite: 0 }; + return { game, source: original, afterBox, owner, target: owner, claimant, incomeOwner, counterOwner, + counter: counter.id, semuta: semuta.id, ghola: original.ghola, gholaAction, + originalInventory, actions, sourceActionCount: actions.length, fullHand: !!options.fullHand, + staging: [...original.staging, + 'Actually pay two spice with the original held Box and recover the original publicly spent Ghola; native sole-choice completion or the original private search event owns that acquisition. Finish the Box discard through actual neutral passes before staging any Semuta holder.', + 'At the clean pre-Ghola boundary only, explicitly conserve the original cache Semuta into Richese hand and the original deck Karama into Emperor hand. The latter keeps the real Tleilaxu income response conditional. Optional full-hand cases move three original ordinary deck cards to Richese. No physical card is duplicated and no pending control, income receipt, wallet, army, phase, clock or revived outcome is installed. Future Ghola, Semuta and income actions remain caller choices.'], + }; +} +export type SemutaGholaIncomeFixture = ReturnType; diff --git a/tests/semuta-ghola-income-controls.test.tsx b/tests/semuta-ghola-income-controls.test.tsx new file mode 100644 index 00000000..359696f1 --- /dev/null +++ b/tests/semuta-ghola-income-controls.test.tsx @@ -0,0 +1,120 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createElement, type ReactElement } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { SemutaReaction } from '../components/semuta-reaction'; +import { viewGame, type Action, type Game } from '../game/engine'; +import { + createSemutaGholaIncomeFixture, actSemutaGholaIncome as act, + passSemutaGholaIncome, allowSemutaGholaIncome, + semutaGholaIncomePlayer as player, +} from './fixture-semuta-ghola-income'; + +type Props = Parameters[0]; +type Control = { children?: unknown; disabled?: boolean; onClick: () => void }; +function controls(node: unknown): Control[] { + if (Array.isArray(node)) return node.flatMap(controls); + if (!node || typeof node !== 'object' || !('props' in node)) return []; + const props = (node as ReactElement<{ children?: unknown; disabled?: boolean; onClick?: () => void }>).props; + return [...(props.onClick ? [{ ...props, onClick: props.onClick }] : []), ...controls(props.children)]; +} +const markup = (props: Props) => renderToStaticMarkup(createElement(SemutaReaction, props)); +function click(game: Game, actor: string, label: 'Continue' | 'Commit Semuta Drug'): Action { + const sent: Action[] = [], props = { game: viewGame(game, actor), busy: false, + act: (action: Action) => { sent.push(action); } }; + const control = controls(SemutaReaction(props)).find(button => button.children === label); + assert.ok(control, `The actual fresh discard offers ${label}`); + assert.equal(control.disabled, false); + assert.match(markup(props), /Fresh Treachery discard/); + assert.match(markup(props), /Continue when you are ready/); + const busy = controls(SemutaReaction({ ...props, busy: true })).find(button => button.children === label); + assert.ok(busy); assert.equal(busy.disabled, true); + busy.onClick(); assert.deepEqual(sent, [], 'Busy callbacks cannot commit or acknowledge'); + control.onClick(); assert.equal(sent.length, 1); + assert.deepEqual(sent[0], { type: label === 'Continue' ? 'semutaPass' : 'semutaCommit', + event: props.game.semutaReaction!.event }); + return sent[0]; +} +function parkedIncome(game: Game) { + const continuation = game.pendingTreacheryDiscard?.continuation; + assert.ok(continuation?.kind === 'ordinaryCardDiscard'); + assert.equal(continuation.effect, 'ghola'); + const income = continuation.resume.response; + assert.ok(income?.kind === 'revivalIncome'); + return structuredClone(income); +} + +void test('real Ghola Continue remains readable and leaves income parked until every other seat responds', () => { + const f = createSemutaGholaIncomeFixture(); + const originalIncome = player(f.game, f.incomeOwner).spice; + const reserves = player(f.game, f.owner).reserves; + let game = act(f.game, f.owner, f.gholaAction); + const income = parkedIncome(game), event = game.pendingTreacheryDiscard!.batch.event; + assert.equal(player(game, f.owner).reserves, reserves + 1); + assert.equal(player(game, f.owner).tanks, 0); + assert.equal(player(game, f.incomeOwner).spice, originalIncome); + assert.equal(game.response, null); + const before = structuredClone(game); + for (let reading = 0; reading < 2; reading++) { + const html = markup({ game: viewGame(game, f.owner), busy: false, act() {} }); + assert.match(html, />Continue<\/button>/); + assert.match(html, /Continue when you are ready/); + } + assert.deepEqual(game, before, 'Reading cannot acknowledge the card or allow native income'); + game = act(game, f.owner, click(game, f.owner, 'Continue')); + assert.deepEqual(parkedIncome(game), income); + assert.equal(game.pendingTreacheryDiscard!.batch.event, event); + assert.equal(game.response, null); + const waited = markup({ game: viewGame(game, f.owner), busy: false, act() {} }); + assert.match(waited, /You continued. Waiting for other seats/); + assert.doesNotMatch(waited, /