Yield Rework V2 — configurable interrupts, APINA, suggestions, settings UI#10606
Yield Rework V2 — configurable interrupts, APINA, suggestions, settings UI#10606MostCromulent wants to merge 13 commits intoCard-Forge:masterfrom
Conversation
…ttings UI Builds on the per-PCH YieldController MVP shipped in master. Adds the deferred YieldRework feature set on top of that architecture without introducing new ProtocolMethod entries — everything rides the existing YieldUpdate envelope. Features - Six configurable interrupt prefs that control when an active autopass (EOT, marker) is cancelled: OPPONENT_SPELL, ATTACKERS, TARGETING, MASS_REMOVAL, TRIGGERS, REVEAL. First two default ON to preserve master's hardcoded behavior; the rest default OFF. - APINA (Auto-Pass If No Actions) — per-tick predicate gated by the AvailableActions heuristic in forge-ai, with configurable timeout. - Smart suggestions — InputPassPriority offers a stack-yield prompt when there are spells on the stack and no playable response, and a no-actions marker prompt on the player's own turn with no playable cards. Each has a configurable decline scope (NEVER / ALWAYS / STACK / TURN). - Speed settings — opt-in skip of inter-phase and post-resolve delays. - Desktop UI: VYield dock-tab panel (Auto-Pass toggle + Settings buttons) and VYieldSettings dialog. Three configurable shortcuts: Ctrl+Y opens settings, F2 toggles APINA, ESC clears the active yield. - Mobile UI: VYieldOptions scrollable settings dialog + VGameMenu entries for Yield Options and the Auto-Pass toggle. Architecture - All new state lives on per-PCH YieldController; each client's preferences govern only that client's host-side proxy. - Three new YieldUpdate variants (SetAutoPassUntilEndOfTurn, SetYieldBoolPref, SetYieldStringPref) plus pref-overlay extensions to the SeedFromClient snapshot. Zero new ProtocolMethod entries. - Single-yield invariant: at most one of EOT/marker/stack-yield is active at a time (setters enforce). Stack-yield is deliberately immune to event-driven interrupts — only stack-empty turns it off, since its whole point is "ride through stack additions." - YieldController.apply(YieldUpdate) is the single dispatch point for wire envelopes; PCH and NetGameController applyYieldUpdate are thin delegators. - Type-safe domain enums: DeclineScope (NEVER/ALWAYS/STACK/TURN) and SuggestionType (STACK_YIELD/NO_ACTIONS, owns allowed scopes + scope FPref) replace the previous stringly-typed code. - PCH.tryAutoPassNow runs after every yield-state change and re-evaluates mayAutoPass at the current input, so toggles fire on the current prompt (no priority-window lag). Hardcoded interrupt sites deleted from MagicStack, PCH.declareAttackers, and HostedMatch — the event-driven handler covers the same conditions, now governed by per-PCH prefs. Also lower DragCell minimum height to 50px so the dragged-out yield-options tab can shrink to fit a single row of buttons. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6e214be to
86c2e37
Compare
|
I notice you still have something checking state despite the yield interrupts. I don't think this is necessary and will just cause the state-scanner to re-prompt on every priority pass while the interrupt state persists. |
|
Also, I recall we talked about removing the smart suggestion prompts? Are they actually useful enough to justify as a feature? |
Previously APINA + RESPECTS_INTERRUPTS evaluated `shouldInterruptYield` on every priority tick — meaning a Wrath sitting on the stack caused APINA to re-prompt every priority window for as long as the spell stayed there. Yields had the right semantic (event-driven, one prompt per event); APINA duplicated the classifiers in a per-tick state-scanner that produced the wrong UX. APINA now shares the yield event path. Each event handler (`onSpellAbilityCast`, `onAttackersDeclared`, `maybeInterruptOnReveal`) calls a unified `applyInterrupt()` that clears any interruptible yield and sets a transient `autoPassInterrupted` flag when APINA + RESPECTS_INTERRUPTS are on. The flag is cleared by `noteMayAutoPassResult` once the user has been prompted, so APINA resumes on the next priority window without re-prompting. `shouldInterruptYield` and `hasMassRemovalOnStack` are deleted — both were only reachable from the APINA path. Side effect: REVEAL now applies to APINA. The state-scanner had no REVEAL check, so users with INTERRUPT_ON_REVEAL enabled were silently missing that interrupt under APINA; the event handler covers it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
this was for APINA, have updated so interrupts apply on event driven basis there too. much cleaner architecture now
i didn't think we want stack yield to be interrupted? i.e. when you click 'yield to entire stack' you're saying "I don't want to interact with this stack in any way / I have no instants I can play, don't bother me again". e.g. you're in a commander game and your opponents are interacting on the stack but you don't want/cant participate. if stack yield interrupts you get bothered every time opponents interact when you've already opted out. if we really we want this i'd suggest we add a 'Stack yield respects interrupt' option to the settings, same as the APINA respects, so at least its optional
in discussion on Discord, TRTs view was worth keeping because its useful for players who don't want full automation and want to use Forge to eg. train for playing paper games |
|
I think we still want stack interrupts, since yielding the stack is saying “I don’t want to interact with the stack in its current state” but you yield and then say, a mass removal spell is added, you might want to reconsider. You can always just turn off interrupts too. I don’t really see stack yield as “special” in that it should ignore interruption. Example: play a spell, 10 prowess triggers appear, you have a counterspell but you don’t want to counter your own spell, but that means it doesn’t autopass. You don’t want to click through each trigger, so you yield the stack. The next player plays their own counterspell on your spell. You had targeting interruptions on, but it was ignored and you never had a chance to respond to them countering your spell. |
The dedicated yield options panel was a tab in the prompt cell hosting just two controls (Auto-Pass toggle + Settings dialog launcher). Fold both into the existing dock — Auto-Pass becomes an icon with a goldenrod- highlighted toggled state, and the previously-dormant cog button now opens VYieldSettings. Promote BUTTON_DOCK to its own match.xml cell directly above the prompt panel, aligning the prompt top with the hand top. Drop the in-dock Revert/Open/Save Layout buttons (still available from the menu bar's Layout menu). Update the Advanced Yield Options wiki page to match the new access points. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The stack-item context menu now offers two choices on desktop and mobile. "Yield to stack" auto-passes until the stack empties but backs off when any of the existing yield-interrupt prefs trip (opponent spell, targeting, mass removal, triggers, reveal). "Resolve entire stack" preserves today's fire-and-forget behavior — only stack-empty turns it off. The "you cannot respond to the stack" suggestion defaults to the interruptible variant. Implemented as a single boolean flag (stackYieldRespectsInterrupts) paired with the existing autoPassUntilStackEmpty state on YieldController, with the flag carried over the wire as a third component on YieldUpdate.StackYield. Reuses the existing applyInterrupt() path — no new prefs introduced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapse the dedicated passPriorityUntilEndOfTurn ProtocolMethod into the existing YieldUpdate envelope so all yield-related wire traffic rides one ProtocolMethod pair, matching the V2 single-surface principle. The four call sites (CDock, GameMenu, KeyboardShortcuts, MatchScreen) now go through a YieldController.endTurn helper that mirrors the existing toggleAutoPassNoActions shape. Removing the isYieldActive early-return in tryAutoPassNow is safe because the mayAutoPass check below already encompasses it — mayAutoPass is shouldAutoYield || isAutoPassingNoActions, and shouldAutoYield is true exactly when a yield is active. The early-return only diverged from mayAutoPass in one direction: when a yield had just been activated, it blocked the OK click that the original passPriority(true) always made unconditionally. Dropping it lets the envelope path reproduce that behavior on the current InputPassPriority instead of waiting for the next cycle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snapshot building (snapshotBoolPrefs / snapshotStringPrefs) was returning a copy of the local override map, which is empty at game start because the override map only fills when the user toggles a setting mid-game. The host's proxy of a remote player would receive an empty seed, then fall through to the host's FModel for every read — silently substituting the host's preferences for the client's. Defaults-only players didn't notice because the two default-on interrupts matched between unconfigured host and client; anyone who customized their settings would see them ignored until they re-toggled each pref mid-match. Fix: enumerate the synced yield FPrefs (SYNCED_BOOL_PREFS / SYNCED_STRING_PREFS) and read each effective value from FModel when building the snapshot, so the proxy is seeded with the client's actual values from turn 1. This makes the override map's role coherent: populated only on the host's proxy of a remote player, via applyClientSeed and the SetYieldBoolPref/SetYieldStringPref envelopes. Local controllers (host's own PCH and client's NetGameController) always read through FModel fallback — the dialog already writes there, so the parallel local store was dead weight. Drop the yieldController.setBoolPref/setStringPref calls from PCH.setYieldBoolPref/StringPref and the equivalent in NetGameController; PCH.setYieldBoolPref still fires tryAutoPassNow because toggling APINA may flip mayAutoPass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses review comment on <!-- -->Card-Forge#10606. When a yield is active, mayAutoPass() short-circuits via shouldAutoYield() and never consults the available-actions field, so computing it is wasted work. Match the gate already used by chooseSpellAbilityToPlay. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactor of #9643 (Yield Rework v1) on top of the per-PCH
YieldControllerMVP shipped in #10555.Re-implements the deferred YieldRework feature set against the MVP's architecture (single
YieldUpdatewire envelope, per-PCH controller, atomic seed) without adding new ProtocolMethod entries.Architectural reference doc attached separately covers the design in detail.
Deferred features included
OPPONENT_SPELL,ATTACKERS,TARGETING,MASS_REMOVAL,TRIGGERS,REVEAL. First two default ON to preserve master's existing hardcoded interrupts; the rest default OFF.forge.ai.AvailableActionsheuristic with a per-PCH timeout budget. SeparateRESPECTS_INTERRUPTSopt-in (default OFF).InputPassPriorityoffers Accept/Decline prompts when conditions match: stack-yield when there's no playable response to the stack, marker-on-upkeep when no actions exist on your turn. Each suggestion type has a configurable decline scope (NEVER/ALWAYS/STACK/TURN).YieldUpdateenvelope variants. Atomic seed at game start carries the full overlay map in a single message.VYieldSettingsdialog +VYielddock-tab panel (Auto-Pass toggle button + settings shortcut), mobileVYieldOptionsscrollable dialog. Game-menu entries on both platforms. Three configurable desktop shortcuts (Ctrl+Y / F2 / ESC).Hardcoded
autoPassCancel()sites inMagicStack,PCH.declareAttackers, andHostedMatchdeleted — the event-driven handler covers them, governed by per-PCH prefs. Default-ON pref values keep observable behavior identical for unconfigured users.🤖 Generated with Claude Code