19 KiB
Automatic allowance of uncancelable powers
Source audit and implementation checkpoint, 2026-09-06. This follows the updated product instruction to assume allowance when a player has no choice to block; it does not establish a new printed timing rule or change which faction powers Karama can cancel. Code line anchors describe the audit snapshot and may move as the coordinating agent integrates changes.
Current implementation checkpoint
The response loop is implemented for existing supported response continuations. This is partial overall coverage, not completion of all automatic actions, all successful-power notices, or full expansion rules.
karamaSpendingBlockandresponseCancelCardsshare physical-custody checks. The power owner has no cancellation option. Basic BG Worthless does not count; Advanced BG Worthless does, except while conversion is already pending. Sealed/prescience-bound/retention-reserved cards are excluded, and later usable cards remain selectable.settleAutomaticResponsesdrains an unopposed response on the authoritative working clone, repeats continuations, and stops at actual cancellation options, Truthtrance, phase opening or completion. Explicit pass history remains authoritative: a previous passer can still cancel while another real choice holds the window open. No synthetic per-player automatic passes are recorded.responseControlscontains only the viewer’scancelCardsandhasPassed; projectedresponse.passedlikewise contains only that viewer’s explicit ID, if any. UI and bots use these controls. No public eligibility list is added. Aggregate timing inference remains unavoidable under immediate allowance.applyActionreconciles the initiating voluntary action before automatic consequences.normalizeAutomaticGameclones state and settles restored responses separately.setAutopilotretains its early return and does not itself award a power’s reward.continueRoomAutomaticnormalizes legacy pending responses with up to four database CAS attempts; a lost race rereads authority, and an exhausted retry budget leaves a later authenticated request able to resume.viewGame/readSeatViewdo not award these effects. Route-local promise coalescing shares one continuation for overlapping GET/POST wakeups and removes its gate after success or rejection. Database CAS still protects different isolates; this is not a durable unattended worker guarantee.- Newly received tagged notices queue in sequence for approximately 1.5 seconds each. Turning notices off clears active/pending feedback and consumes later events; enabling or reloading does not replay missed/history events. The queue is bounded at 250 including an active notice, preserving the newest pending events on overflow. These timers never delay authoritative game state. Currently tagged successes are automatic battle casualties, Ecaz Ambassador placement, Emperor auction income and Harkonnen bonus draws; the draw’s card face stays private. All other untagged successful powers remain incomplete notice coverage.
Remaining eligibility boundary
Promise feasibility is deliberately conservative: physically spendable Karama candidates remain offered until the existing authoritative promise reconciliation can accept or reject the actual spend. It is not yet a complete promise-aware legality projection. This can leave a redundant choice, but does not silently discard a potentially legal cancellation. Active overlays retain priority; no RNG or power continuation is executed speculatively to filter a candidate.
Verification now present
tests/automatic-responses.test.ts: 15 named engine regressions for chained income/draw, exact-once reload, viewer-local controls, late cancellation after explicit pass, BG conversion, reservations, nested restoration, overlays, conservative promises/Voice, hand exchange, summoned worm, setup and autopilot separation.tests/automatic-response-recovery.test.ts: 7 named tests using production room/route modules and actual SQLite CAS. They cover read-only private snapshots, exact-once income and bonus, competing workers, takeback winning a race, bounded failed CAS/recovery, a real cancelable window remaining untouched, identical coalesced GET promises, and restart after completion/rejection.- Existing Moritani, Duke and Ecaz regressions retain actual opposing cancellation cards when testing a response window; no-choice cases now verify immediate settlement. Notice timing/reduced-motion and real-time hosted behavior need their own browser evidence; engine/persistence tests do not certify them.
The in-app automatic-responses checklist links the current implementation, player-control, AI, documentation and verification boundaries. The original audit below records the pre-implementation findings and rationale; its historical line anchors and descriptions of old UI/public passes are superseded by this checkpoint.
Pre-implementation behavior and relevant boundaries (historical)
| Source | Observation |
|---|---|
game/engine.ts:1642–1648 |
Auction continuations separately offer Emperor income and Harkonnen's bonus card with empty passed arrays. A chain of effects needs repeated automatic settlement, not one special-case pass. |
game/engine.ts:1675–1725 |
karamaCard recognizes real Karama and Advanced Bene Gesserit Worthless conversion. spendKarama separately rejects retained battle-cleanup cards, sealed plan cards and the card committed to prescience. Conversion consumes the Worthless card and suspends the original use in pendingKarama. |
game/engine.ts:3499–3528 |
reconcileBattlePromises can reject a voluntary card spend that makes its actor's Truthtrance battle promise impossible. Simple kind matching alone does not certify a legal cancellation. |
game/engine.ts:4544–4552 |
Allowing Worthless conversion completes its saved use. Canceling a conversion intended to cancel another power restores the original response object and its explicit pass history. |
game/engine.ts:5518, 6372 |
Harkonnen exchange suspends and later restores response/decision state. Returning different cards can change the restored window's cancellation eligibility. |
game/engine.ts:5561–5633 |
Public applyAction owns the disposable working-game boundary and post-action setup, advisor and promise reconciliation. setAutopilot deliberately returns before gameplay processing. |
game/engine.ts:5655–5741 |
advanceBots is an early inner no-op; Truthtrance and phase opening precede ordinary response dispatch. Some explicitly permitted faction/card actions also precede the response branch. |
game/engine.ts:5741–5764 |
Response completion currently requires every player to appear in passed. Cancellation rejects the response owner, but does not reject an otherwise legal canceler merely because they passed earlier. Preserve that later-cancel right while the window remains open. |
game/engine.ts:8269–8287 |
viewGame spreads the response, including all passed IDs, publicly; only selected other fields are redacted. Do not put private eligibility results into this array. |
game/bots.ts:696–727 |
Bots choose a Karama candidate using type and retention reservation, then fall back to passing. They should consume the same projected legal candidate IDs as the UI. |
components/game-table.tsx:1496–1521 |
The UI renders Karama-like hand cards, disabling retained cards, and always renders an Allow button. Its current filter does not cover every server spending restriction. |
db/rooms.ts:339–382 |
An ordinary action is applied before a version/credential-fenced state write. A response chain belongs inside that same atomic commit. GET projection must not execute effects or create unpersisted rewards. |
Audited engine contract and rationale
- Extract a pure shared predicate for spending a particular physical card as Karama. It must use the actual current hand, real Karama versus Advanced BG Worthless capability, Moritani retention reservation, sealed Battle Plan weapon/defense/leader IDs, prescience binding, and any pending-conversion restriction. Do not select the first matching card and reject it before considering a later usable card. Use this predicate in the dispatcher and candidate enumeration.
- Define
responseCancelCards(g, player, response)as exact current legal candidate IDs. The response owner always gets none. An allied opponent remains legally eligible; a bot's preference not to cancel an ally is strategy, not legality. An earlier explicit pass must not remove that player's cancellation options before the window closes. - Keep
response.passedfor explicit player choices only. Derive waiting responders from players who have at least one current cancellation option and have not explicitly passed. Do not persist a second list identifying ineligible players. Automatic allowance is the absence of a required input, not a fabricated player action or public pass announcement. - Add a central mutating
settleAutomaticResponses(g)operating only on the authoritative working clone. While an active response has no remaining required responders, call its existingfinishResponse(g, false)continuation and inspect the next response. Stop at a response needing a real choice, an active overlay, an actual decision without a response, or the end of the chain. Use a defensive bounded iteration limit and a diagnostic for an unchanged continuation; do not silently leave an uncancelable window stalled. - Recompute eligibility on every current/restored window. A canceled Worthless conversion has already consumed cards; its restored outer response may now auto-complete. Harkonnen exchange may give a previously ineligible player a usable card. Treat the restored explicit pass list as authoritative, but never carry forward inferred automatic passes.
- On manual
passResponse, use the same derived completion condition rather than comparing the length ofpassedwith player count. A canceler who already passed may still cancel while another player's decision holds the window open. Do not automatically select a card or choose cancellation on anyone's behalf.
Truthtrance and feasibility
The physical spending predicate supplies necessary conditions, not complete proof that every cancellation is legal. A BG Worthless card may be necessary to satisfy an unresolved battle promise even when it is not sealed or committed to prescience. The current final promise reconciliation can reject that spend. Conversely, canceling Voice may itself change the legal plan set.
Reuse the existing pure plan-feasibility machinery on an isolated, explicitly modeled hypothetical state where needed. Distinguish a proven unavailable card from an unresolved feasibility check; do not infer impossibility by running applyAction or finishResponse as a speculative predicate. Those can draw cards, shuffle, resolve RNG, change phases and execute other powers even if their game object was cloned. A conservative candidate when feasibility is not yet proven avoids silently forfeiting a legitimate cancellation, but may leave a redundant choice; document and test that limit until the exact shared predicate is available.
An already active Truthtrance overlay must stop automatic response settlement. Its priority and question/answer steps are actual player choices. Phase opening likewise takes dispatcher precedence; do not infer that every canceler is ineligible merely because the current overlay temporarily prevents the cancel action. Do not create new confirmation windows for players merely holding a Truthtrance card.
Do not blanket-stop on g.decision: normal response windows may coexist with suspended decisions or pending revival data. Follow the dispatcher's actual precedence, rather than interpreting every pending object as an action lock. A Fremen summoned-worm suspension and Harkonnen hand exchange have no active outer g.response; leave their saved window untouched until their own continuation restores it.
Wrapper placement and persistence
Keep the setAutopilot early return unchanged: toggling delegation is a custody/control operation and must not resolve game powers or give cards. Run ordinary inner actions and their current immediate checks on the working clone before draining new uncancelable responses.
Apply the original action's voluntary-promise validation before attributing later automatic effects. Reconcile effects completed by the automatic drain as automatic/opposing consequences, rather than blaming the original actor's earlier card action for an opposing power's subsequent changes. The current reconciliation API takes (actor, action); explicitly represent that distinction in a helper/parameter instead of pretending another player sent a card or pass action. Newly completed setup, advisor and battle continuations may require repeating the relevant existing invariants before another drain iteration. Stop under a newly opened Truthtrance/phase-opening overlay.
Do not run this mutation inside viewGame. A pure private GET must not draw a Harkonnen card differently for each viewer, reroll an effect, or show a reward which has not committed. Old persisted all-human rooms already waiting on an uncancelable window need one bounded authoritative normalization through the existing version/credential-fenced write path or a controlled migration/worker path. A projection-only change does not durably unstick such a room. Ordinary newly created windows should settle in the transaction that creates them.
Emit a cosmetic public event after a power actually succeeds. A declaration, canceled conversion, canceled effect or stale no-op is not successful execution. The event should carry an opaque unique event/sequence ID, public faction and approved short action name. It must not include a drawn card's identity, unrevealed replacement Face Dancer, private revival price, hidden placement source, or automatic-pass reason. The 1.5-second notice belongs solely to presentation and cannot delay state completion or require acknowledgement.
Private projection and unavoidable timing inference
Recommended viewer-local fields are response.cancelCards: string[] and response.hasPassed: boolean, or an equivalent separate private response-controls object. Keep enough public effect identity to explain the proposed power, with existing redactions intact. Remove public inferred pass lists, eligible-player counts, waiting-player names, and reasons such as “no Karama in hand.” Owners and other ineligible viewers see only a neutral statement that the response is pending, with no Allow button. Eligible viewers see their exact legal cards and an optional Allow action.
An unresolved pending window versus immediate completion still leaks the aggregate fact that a blocking option remains somewhere. In a two-player game, or when all other hands are already known, an observer may infer which opponent has one. Immediate auto-allow, no redundant prompts and perfect concealment of this timing fact cannot all be provided simultaneously. Avoid worsening that unavoidable inference with per-player status or card counts. Artificial deterministic waiting for every player would conflict with the current instruction and is not proposed here.
Focused acceptance cases
- Harkonnen bonus with nobody able to cancel: exactly one bonus draw and one successful event; no response survives and no pass actions are needed.
- Emperor income followed by Harkonnen bonus: drain both if uncancelable, stop at the first actual cancellation choice, and never repeat income/draw on retry or reload.
- One eligible opponent: only that viewer gets cancel IDs and Allow; owner and empty-handed players need no input. An eligible ally is not auto-passed because a bot would prefer cooperation.
- An explicit passer can still cancel while a different eligible player holds the window open. If nobody remains required, the power completes immediately.
- Basic BG Worthless only: no cancellation option. Advanced BG with a usable Worthless: conversion option exists. A retained/sealed/prescience-bound Worthless does not count; a later usable card still does.
- Worthless conversion canceled: consume the actual spent cards once, restore the exact outer response and its explicit passes, recompute eligibility, and auto-complete it if no required responder remains.
- Nested conversion: do not offer an impossible second conversion while one is pending; ordinary real Karama can still cancel a different faction's conversion when otherwise legal.
- Truthtrance overlay active: no reward, draw, response completion or automatic event until it resolves. Promise-bound cards and a Voice cancellation which changes feasibility need dedicated tests.
- Harkonnen exchange and summoned-worm suspend/restore paths: no premature outer completion; eligibility reflects the restored current hands and bindings.
- JSON round-trip before/after automatic settlement: exact card/spice conservation, no RNG duplication and no replayed notification on first client mount.
- Two concurrent final actions: one version-fenced commit, one stale rejection; no duplicate bonus, income, discard or event.
- Viewer projection: inspect every faction's response view and assert only its own legal cancel IDs/explicit-pass state appears; preserve private prices, drawn cards, replacement identities and unrelated hand privacy.
setAutopilotwhile an uncancelable response exists preserves the response, cards, RNG and game phase exactly; the separate authoritative progression path owns settlement.
The original read-only audit changed no runtime files or live rooms. The current checkpoint above records the subsequent coordinated implementation; this documentation update itself changes no runtime behavior.