Skip to content

fix(engine): verify offered casts with the auto-payment authority - #7007

Merged
matthewevans merged 21 commits into
phase-rs:mainfrom
nishu-builder:fix/offer-side-payment-preview
Aug 8, 2026
Merged

fix(engine): verify offered casts with the auto-payment authority#7007
matthewevans merged 21 commits into
phase-rs:mainfrom
nishu-builder:fix/offer-side-payment-preview

Conversation

@nishu-builder

@nishu-builder nishu-builder commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Enforces the exact-legal-action contract at the offer seam: a CastSpell { payment_mode: Auto } (or any action that synthesizes an Auto pending cast) is only offered when a complete Auto payment exists, verified with the same authority Auto payment itself uses — no parallel approximation. Found by an external legal-action-fuzzing harness: the engine offered casts counting mana sources Auto payment cannot actually use (an interactive {T}+exile mana ability), and CastPreparedCopy (no payment-mode field) skipped the completion preview entirely, so both shapes failed at commit time with "Cannot pay mana cost" after targeting. The fix routes an exhaustive cast-origin matrix (including cast-during-resolution zone picks, morph/PlayFaceDown, and the miracle reveal/cast-offer split) through a shared payment preview on the already-disposable post-apply scratch state, deferring to the live gate for unresolved payment-affecting choices (Harmonize, Assist) so legal interactive-affordability casts remain offered.

Files changed

  • crates/engine/src/ai_support/{filter,candidates,mod}.rs
  • crates/engine/src/game/{casting,casting_costs,engine,mana_abilities,mana_payment}.rs
  • crates/engine/src/game/perf_counters.rs (new)
  • crates/engine/src/game/{casting_tests,splice_tests}.rs
  • crates/engine/tests/integration/offer_side_auto_payment.rs (new)
  • crates/engine/tests/integration/main.rs

CR references

  • CR 601.2f
  • CR 601.2g
  • CR 601.2h

Implementation method (required)

Method: /engine-implementer

Track

Developer

LLM

Model: gpt-5.6-sol
Thinking: high
Tier: Frontier

Verification

  • Required checks ran clean, or the exact CI-owned alternative is stated below.

  • Gate A output below is for the current committed head.

  • Final review-impl below is clean for the current committed head.

  • Both anchors cite existing analogous code at the same seam.

  • tilt get uiresource clippy — Tilt unavailable in this worktree; used the documented direct fallback.

  • cargo fmt --all / cargo fmt --all -- --check — passed.

  • git diff --check — passed.

  • cargo clippy -p phase-engine --all-targets -- -D warnings — passed on head 744622c179da157159017f13841d7efc34b4b7e6.

  • cargo test -p phase-engine — passed on head 744622c179da157159017f13841d7efc34b4b7e6: 18,486 unit + 4,491 integration (plus subsequent fix-round additions), 0 failed.

  • Plan verification matrix — 37+ targeted invocations passed, including: interactive-mana negative/positive pair, prepared-copy negative/payable pair, Harmonize-only affordability stays offered, EffectZoneChoice cast-during-resolution negative/payable pair, split PlayFaceDown proofs, hostile irrelevant-cost-static fixture, hostile composed Or/Not filter fixtures, and the performance-counter regression (exact clone/collection equalities; zero additional whole-state clones on the offer path).

  • Revert probes — interactive-mana and prepared-copy discriminating regressions each fail with the production change reverted and pass restored (transcripts in the pipeline artifacts).

  • ./scripts/gen-card-data.sh — passed on head 744622c179da157159017f13841d7efc34b4b7e6: generated card data for ~35684 cards.

  • cargo coverage — passed on head 744622c179da157159017f13841d7efc34b4b7e6: timeless legal 15124/16180 fully supported (93.5%); vintage legal 29841/32268 fully supported (92.5%).

  • cargo semantic-audit — passed on head 744622c179da157159017f13841d7efc34b4b7e6: 32732 cards audited, 297 existing findings.

Gate A

Gate A PASS head=744622c179da157159017f13841d7efc34b4b7e6 base=6d7821dced9623609edea342b47dd9c704ff0b36

Anchored on

  • crates/engine/src/game/mana_payment.rs:828 — existing reduce_cost_by_pool scratch-pool dry run (PR fix(engine): make mana payments atomic #5793 heritage) — the same simulate-without-mutating discipline the offer preview extends to whole payments.
  • crates/engine/src/game/casting.rs:14747 — existing can_pay_cost_after_auto_tap_with_probe payment authority; the offer-side preview calls this shared authority rather than approximating it.

Final review-impl

Final review-impl PASS head=744622c179da157159017f13841d7efc34b4b7e6

Claimed parse impact

None.

Validation Failures

Contributor-environment note per the engine-implementer skill: pipeline steps ran as isolated fresh contexts (Codex CLI sessions) with artifact-only handoffs rather than spawned Claude subagents. Plan review: 5 rounds to clean (3 → 3 → 2 → 2 → 0 findings), including one executor STOP_AND_RETURN that identified a logically unconstructible fixture specification (single-fixture PlayFaceDown proof vs. first-wins preflight family ordering), resolved by splitting the proof obligations. Implementation review: 3 rounds to clean (2 → 1 → 0), tightening target-sensitive static handling to production applicability gates and composed-filter analysis to a three-state classification.

CI Failures

None.

Related

Independent of #6989 and #6997 (serialization fixes) from the same contributor; no overlapping concerns.

Summary by CodeRabbit

  • New Features

    • Casting now selects appropriate payment methods for regular, alternate, Sneak, and Web-Slinging casts.
    • Supports Assist, Convoke, Phyrexian, hybrid, sacrificial, graveyard, and alternative-face costs.
    • Improved Morph and Splice casting flows.
  • Bug Fixes

    • Prevents unpayable or ambiguous casts from appearing as legal actions.
    • Preserves payment choices and pending-cast state through multi-step prompts.
    • Improves handling of target-dependent and additional costs.
  • Performance

    • Added detailed instrumentation for casting and mana-validation operations.

@github-actions github-actions Bot added the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Automatic casting now derives payment modes from effective costs, validates pending-cast payment feasibility, preserves payment choices through announcements, and records legality performance phases. Regression tests cover interactive costs, alternative casts, sacrificial mana, Morph, splice, and casting permissions.

Changes

Automatic casting flow

Layer / File(s) Summary
Payment and cost validation
crates/engine/src/game/casting.rs, crates/engine/src/game/casting_costs.rs, crates/engine/src/game/mana_payment.rs, crates/engine/src/ai_support/filter.rs
Casting validation separates target legality from mana feasibility. It supports effective and reduced costs, pending-cast stability, and atomic mana-payment previews.
Candidate payment modes and announcement propagation
crates/engine/src/ai_support/candidates.rs, crates/engine/src/game/casting.rs, crates/engine/src/game/engine.rs
Priority, Sneak, and Web Slinging candidates compute payment modes from prepared variant costs. Announcement conversion preserves those modes.
Legality phases and clone accounting
crates/engine/src/game/perf_counters.rs, crates/engine/src/ai_support/mod.rs, crates/engine/src/game/mana_abilities.rs, crates/phase-ai/src/duel_suite/perf.rs, crates/phase-ai/baselines/perf-baseline.json
Legality phases record state clones, payment wrappers, probes, post-application calls, source collections, and updated performance schema data.
Casting continuation and offer-side regressions
crates/engine/src/game/*tests.rs, crates/engine/tests/integration/*
Tests cover automatic and manual payment, interactive cost prompts, alternative-cost casts, free casts, sacrificial mana, Morph, splice, and permission-specific legality.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CandidateGeneration
  participant SimulationFilter
  participant Casting
  participant ManaPayment
  CandidateGeneration->>Casting: prepare effective spell variant
  Casting->>ManaPayment: probe payment mode and affordability
  ManaPayment-->>Casting: automatic or preserved-source payment mode
  Casting->>SimulationFilter: apply candidate and inspect pending cast
  SimulationFilter->>Casting: validate pending root and post-origin payment
  Casting-->>CandidateGeneration: accepted cast announcement
Loading

Possibly related PRs

Suggested labels: needs-maintainer

Suggested reviewers: matthewevans

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: validating offered engine casts with the Auto-payment authority.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/engine/src/ai_support/candidates.rs (1)

3616-3638: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Compute sacrificial payment mode per candidate spell.

cast_payment_mode is computed before the spell loop, so every CastSpell candidate inherits a board-level AutoExceptSacrificialMana even when its final cost needs no mana. A spell with cost NoCost or floating mana already fully payable is then sent to enter_payment_step; finalize_automatic_mana_payment is gated out by AutoExceptSacrificialMana, while the sacrificial source list has already been excluded. Derive the mode after each spell's payment/cost is established, and keep the mode Auto for cases that do not require sacrificial payment.

Also move activatable_mana_source_selections behind the spell_objects_available_to_cast check, and align the free/mana-pay alternatives (CastSpellForFree, CastSpellAsSneak, CastSpellAsWebSlinging) so mana-paying alternatives do not remain CastPaymentMode::Auto when every available source requires sacrifice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/ai_support/candidates.rs` around lines 3616 - 3638, Update
the candidate-generation flow around `spell_objects_available_to_cast`,
`CastPaymentMode`, and the
`CastSpell`/`CastSpellForFree`/`CastSpellAsSneak`/`CastSpellAsWebSlinging`
actions so payment mode is computed per candidate after its final cost or
payment alternative is established. Move `activatable_mana_source_selections`
behind the available-spell check, use `AutoExceptSacrificialMana` only when the
candidate actually requires mana and every available source is sacrificial, and
retain `Auto` for free, `NoCost`, or already fully payable candidates. Apply the
same mode selection to mana-paying alternatives so they do not remain
unconditionally `Auto`.
🧹 Nitpick comments (7)
crates/engine/tests/integration/offer_side_auto_payment.rs (1)

502-509: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The tolerant OptionalEffectChoice branch can hide a flow regression.

The fixture ability is built with .optional(). The production flow must therefore present WaitingFor::OptionalEffectChoice before EffectZoneChoice. The current if matches!(...) accepts both outcomes. If the engine stops offering the optional choice, this fixture keeps passing and the two Face-of-Boe tests still report green on a changed pipeline.

Assert the intermediate state instead of tolerating its absence.

♻️ Proposed change
-    if matches!(
-        runner.state().waiting_for,
-        WaitingFor::OptionalEffectChoice { .. }
-    ) {
-        runner
-            .act(GameAction::DecideOptionalEffect { accept: true })
-            .expect("the production 'you may cast' choice must be accepted");
-    }
+    assert!(
+        matches!(
+            runner.state().waiting_for,
+            WaitingFor::OptionalEffectChoice { .. }
+        ),
+        "the optional CastFromZone effect must present its 'you may cast' choice, got {:?}",
+        runner.state().waiting_for
+    );
+    runner
+        .act(GameAction::DecideOptionalEffect { accept: true })
+        .expect("the production 'you may cast' choice must be accepted");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/tests/integration/offer_side_auto_payment.rs` around lines 502
- 509, Replace the conditional `OptionalEffectChoice` handling in the test flow
with an unconditional assertion that `runner.state().waiting_for` is
`WaitingFor::OptionalEffectChoice` before dispatching
`GameAction::DecideOptionalEffect { accept: true }`. Preserve the existing
expectation message and action result handling so the fixture fails if the
optional choice is skipped.

Source: Path instructions

crates/engine/src/ai_support/filter.rs (1)

188-188: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Drop the discarded PendingCast clone in the before read.

pending_spell_root always clones the PendingCast. Line 188 uses only the provenance and discards the clone. PendingCast owns a boxed ResolvedAbility and several Vec fields, so this is a deep clone on every fallback_simulation call.

Split the provenance read from the clone. The after path still needs an owned PendingCast, because post_origin_auto_payment_verdict takes &mut sim.

♻️ Proposed change
-        let before = pending_spell_root(state).map(|(provenance, _)| provenance);
+        let before = pending_spell_root_provenance(state);
fn pending_spell_root_ref(state: &GameState) -> Option<&PendingCast> {
    state
        .waiting_for
        .pending_cast_ref()
        .or(state.pending_cast.as_deref())
        .filter(|pending| pending.activation_ability_index.is_none())
}

fn pending_spell_root_provenance(state: &GameState) -> Option<SpellRootProvenance> {
    pending_spell_root_ref(state)
        .map(|pending| (pending.object_id, pending.casting_permission_index))
}

fn pending_spell_root(state: &GameState) -> Option<(SpellRootProvenance, PendingCast)> {
    pending_spell_root_ref(state).map(|pending| {
        (
            (pending.object_id, pending.casting_permission_index),
            pending.clone(),
        )
    })
}

Also applies to: 236-248

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/ai_support/filter.rs` at line 188, Avoid cloning
PendingCast in the before provenance read within fallback_simulation. Add or
reuse a borrowed pending-spell-root helper and a provenance-only helper, then
update the before path to use the borrowed provenance result while retaining
pending_spell_root’s owned clone for the after path and
post_origin_auto_payment_verdict.
crates/engine/src/game/casting.rs (1)

13869-13884: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reduce the Assist search from a linear scan to one probe per helper.

The loop tests every contribution in 1..=generic against every candidate. Each iteration runs two can_feasibly_pay_mana_cost_with_probe calls, and the caster-side call is unprobed for the helper, so the cost is O(generic × candidates) payment simulations. For an {X} spell with a large chosen X in a four-player game this runs on the candidate-generation path.

Both predicates are monotone in contribution: a helper that can pay n generic can pay n-1, and the caster's residual generic - contribution only shrinks as contribution grows. Find each helper's maximum payable generic amount once, then test the caster once at that amount.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/game/casting.rs` around lines 13869 - 13884, Replace the
nested contribution scan in the Assist payment logic with one calculation per
candidate helper that finds its maximum payable generic contribution, then
perform a single caster feasibility probe using that contribution and the
corresponding residual generic cost. Preserve the existing shard handling,
source ID, and probe arguments, while retaining the monotonic behavior that
accepts a helper whenever its maximum contribution leaves a caster-payable
remainder.
crates/engine/src/game/casting_costs.rs (1)

12107-12111: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Drop the cloned PendingCast on the auto-finalization path.

eligible_tap_payment_mode, choice_free_auto_payment_verdict, and can_pay_cost_after_auto_tap all use state.pending_cast immutably, and finalize_automatic_mana_payment runs only after those reads complete. Using as_deref() avoids cloning the boxed PendingCast before entering payment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/game/casting_costs.rs` around lines 12107 - 12111, Update
the pending-cast access in the auto-finalization path to use an immutable
dereference via as_deref() instead of cloning through map and as_ref. Keep the
existing control flow and downstream calls to eligible_tap_payment_mode,
choice_free_auto_payment_verdict, can_pay_cost_after_auto_tap, and
finalize_automatic_mana_payment unchanged.
crates/engine/src/game/perf_counters.rs (2)

210-218: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the phase gating of the two post-apply counters.

record_post_apply_uncached_source_collection increments only during LegalityClonePhase::PostApplyCore. record_post_apply_auto_payment_core_call increments unconditionally. A post-apply payment check that runs outside any legality phase therefore raises post_apply_auto_payment_core_calls without raising post_apply_uncached_source_collections. That breaks the one-to-one pairing that offer_side_auto_payment_phase_accounting_has_exact_clone_ownership asserts in crates/engine/src/ai_support/mod.rs (both expected to equal N). Gate both counters the same way, or document why the call counter is phase-independent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/game/perf_counters.rs` around lines 210 - 218, Update
record_post_apply_auto_payment_core_call to use the same LEGALITY_CLONE_PHASE ==
Some(LegalityClonePhase::PostApplyCore) gating as
record_post_apply_uncached_source_collection, preserving the one-to-one counter
pairing expected by
offer_side_auto_payment_phase_accounting_has_exact_clone_ownership.

146-181: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse one phase mapping for both clone recorders.

record_mana_readiness_state_clone repeats the whole phase-to-field mapping of record_phase_owned_state_clone. The only difference is the extra strict_fast_path_mana_readiness_state_clones increment. Two copies of the mapping must stay in lockstep whenever a phase is added or a field is renamed.

♻️ Proposed consolidation
 pub(crate) fn record_mana_readiness_state_clone() {
-    let phase = LEGALITY_CLONE_PHASE.with(Cell::get);
-    with_mut(|snapshot| match phase {
-        Some(LegalityClonePhase::Generation) => snapshot.generation_state_clones += 1,
-        Some(LegalityClonePhase::StrictFastPath) => {
-            snapshot.strict_fast_path_state_clones += 1;
-            snapshot.strict_fast_path_mana_readiness_state_clones += 1;
-        }
-        Some(LegalityClonePhase::RawValidation) => {
-            snapshot.raw_validation_state_clones += 1;
-        }
-        Some(LegalityClonePhase::GroupedManaReadiness) => {
-            snapshot.grouped_mana_readiness_state_clones += 1;
-        }
-        Some(LegalityClonePhase::PostApplyCore) => {
-            snapshot.post_apply_auto_payment_core_state_clones += 1;
-        }
-        None => {}
-    });
+    record_phase_owned_state_clone();
+    if LEGALITY_CLONE_PHASE.with(Cell::get) == Some(LegalityClonePhase::StrictFastPath) {
+        with_mut(|snapshot| snapshot.strict_fast_path_mana_readiness_state_clones += 1);
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/game/perf_counters.rs` around lines 146 - 181, Consolidate
the duplicated phase-to-counter mapping in record_phase_owned_state_clone and
record_mana_readiness_state_clone by reusing one shared helper or recorder.
Preserve the existing per-phase state-clone increments, and keep the additional
strict_fast_path_mana_readiness_state_clones increment exclusive to
record_mana_readiness_state_clone.
crates/engine/src/ai_support/mod.rs (1)

6089-6095: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the two extra clones in the total assertion.

The total combines five named phase counters plus a literal 2. One unit is the priority-cast probe clone, which the sum already includes through priority_cast_probe_state_clones, so the origin of the literal is not derivable from the assertion. State each remaining owner as a named term or add a comment. A failure of this assertion is otherwise hard to attribute.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/ai_support/mod.rs` around lines 6089 - 6095, Update the
total assertion near the counters aggregation to replace the unexplained literal
2 with named clone-owner terms or an adjacent comment identifying both extra
clones. Preserve the existing priority_cast_probe_state_clones contribution and
make the assertion explicitly attribute each remaining unit to its owning phase.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/src/game/casting_tests.rs`:
- Around line 17045-17050: The test setup currently gives the spell ordinary
green mana, so it does not exercise Defiler-only affordability. Remove the added
green ManaUnit while preserving at least 2 life, then assert both
candidate_actions and legal_actions_full include the cast and that
apply_as_current transitions to WaitingFor::DefilerPayment.

In `@crates/engine/src/game/engine.rs`:
- Around line 13889-13894: Strengthen the test around candidate_actions and
legal_actions by adding a legal earlier CastSpell fixture, then assert each API
includes that exact action before retaining the PlayFaceDown absence assertions.
This positive reach-guard must prove both exact-action APIs produced the
expected available action rather than passing on empty results.

In `@crates/engine/src/game/mana_payment.rs`:
- Around line 2666-2681: Update the final fallback test block to also assert
that the same fallback_pool and fallback_cost are accepted by can_pay_for_spell,
using its existing context and arguments with hand_demand set to None. Keep the
direct select_mana_payment assertion, so the test covers both the atomic
selector and the can_pay_for_spell delegation path.

In `@crates/engine/src/game/splice_tests.rs`:
- Around line 229-238: Extend the assertions in the WaitingFor::SpliceOffer
match to verify that pending_cast retains CastPaymentMode::Auto. Inspect the
pending_cast payment-mode field and assert the Auto variant, while preserving
the existing object_id and eligible assertions so the test fails if begin_offer
drops or replaces the mode.

In `@crates/engine/tests/integration/offer_side_auto_payment.rs`:
- Around line 339-341: Add a suite-level prerequisite check for the shared card
fixture/full export used by setup_prepared_copy and setup_face_of_boe, and fail
the test suite when that data is unavailable instead of allowing dependent tests
to return early. Keep the existing test execution paths unchanged when the card
data is present.

---

Outside diff comments:
In `@crates/engine/src/ai_support/candidates.rs`:
- Around line 3616-3638: Update the candidate-generation flow around
`spell_objects_available_to_cast`, `CastPaymentMode`, and the
`CastSpell`/`CastSpellForFree`/`CastSpellAsSneak`/`CastSpellAsWebSlinging`
actions so payment mode is computed per candidate after its final cost or
payment alternative is established. Move `activatable_mana_source_selections`
behind the available-spell check, use `AutoExceptSacrificialMana` only when the
candidate actually requires mana and every available source is sacrificial, and
retain `Auto` for free, `NoCost`, or already fully payable candidates. Apply the
same mode selection to mana-paying alternatives so they do not remain
unconditionally `Auto`.

---

Nitpick comments:
In `@crates/engine/src/ai_support/filter.rs`:
- Line 188: Avoid cloning PendingCast in the before provenance read within
fallback_simulation. Add or reuse a borrowed pending-spell-root helper and a
provenance-only helper, then update the before path to use the borrowed
provenance result while retaining pending_spell_root’s owned clone for the after
path and post_origin_auto_payment_verdict.

In `@crates/engine/src/ai_support/mod.rs`:
- Around line 6089-6095: Update the total assertion near the counters
aggregation to replace the unexplained literal 2 with named clone-owner terms or
an adjacent comment identifying both extra clones. Preserve the existing
priority_cast_probe_state_clones contribution and make the assertion explicitly
attribute each remaining unit to its owning phase.

In `@crates/engine/src/game/casting_costs.rs`:
- Around line 12107-12111: Update the pending-cast access in the
auto-finalization path to use an immutable dereference via as_deref() instead of
cloning through map and as_ref. Keep the existing control flow and downstream
calls to eligible_tap_payment_mode, choice_free_auto_payment_verdict,
can_pay_cost_after_auto_tap, and finalize_automatic_mana_payment unchanged.

In `@crates/engine/src/game/casting.rs`:
- Around line 13869-13884: Replace the nested contribution scan in the Assist
payment logic with one calculation per candidate helper that finds its maximum
payable generic contribution, then perform a single caster feasibility probe
using that contribution and the corresponding residual generic cost. Preserve
the existing shard handling, source ID, and probe arguments, while retaining the
monotonic behavior that accepts a helper whenever its maximum contribution
leaves a caster-payable remainder.

In `@crates/engine/src/game/perf_counters.rs`:
- Around line 210-218: Update record_post_apply_auto_payment_core_call to use
the same LEGALITY_CLONE_PHASE == Some(LegalityClonePhase::PostApplyCore) gating
as record_post_apply_uncached_source_collection, preserving the one-to-one
counter pairing expected by
offer_side_auto_payment_phase_accounting_has_exact_clone_ownership.
- Around line 146-181: Consolidate the duplicated phase-to-counter mapping in
record_phase_owned_state_clone and record_mana_readiness_state_clone by reusing
one shared helper or recorder. Preserve the existing per-phase state-clone
increments, and keep the additional strict_fast_path_mana_readiness_state_clones
increment exclusive to record_mana_readiness_state_clone.

In `@crates/engine/tests/integration/offer_side_auto_payment.rs`:
- Around line 502-509: Replace the conditional `OptionalEffectChoice` handling
in the test flow with an unconditional assertion that
`runner.state().waiting_for` is `WaitingFor::OptionalEffectChoice` before
dispatching `GameAction::DecideOptionalEffect { accept: true }`. Preserve the
existing expectation message and action result handling so the fixture fails if
the optional choice is skipped.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ca022485-4cc0-485f-ac0e-c2a40d1940cf

📥 Commits

Reviewing files that changed from the base of the PR and between b654513 and 744622c.

📒 Files selected for processing (13)
  • crates/engine/src/ai_support/candidates.rs
  • crates/engine/src/ai_support/filter.rs
  • crates/engine/src/ai_support/mod.rs
  • crates/engine/src/game/casting.rs
  • crates/engine/src/game/casting_costs.rs
  • crates/engine/src/game/casting_tests.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/src/game/mana_abilities.rs
  • crates/engine/src/game/mana_payment.rs
  • crates/engine/src/game/perf_counters.rs
  • crates/engine/src/game/splice_tests.rs
  • crates/engine/tests/integration/main.rs
  • crates/engine/tests/integration/offer_side_auto_payment.rs

Comment thread crates/engine/src/game/casting_tests.rs Outdated
Comment thread crates/engine/src/game/engine.rs Outdated
Comment thread crates/engine/src/game/mana_payment.rs
Comment thread crates/engine/src/game/splice_tests.rs
Comment thread crates/engine/tests/integration/offer_side_auto_payment.rs
@matthewevans matthewevans self-assigned this Aug 5, 2026
@matthewevans matthewevans added the bug Bug fix label Aug 5, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes — special-action cost generation must preserve manual mana-payment paths.

🔴 Blocker

[HIGH] Sneak and Web-slinging omit legal special actions when their alternate cost can only be paid through a sacrificial/manual mana ability. Evidence: crates/phase-ai/src/policies/candidates.rs:3616-3642 grants AutoExceptSacrificialMana only to ordinary CastSpell, while the Sneak and Web-slinging emitters at :4234-4249 and :4289-4304 force/filter Auto; crates/phase-ai/src/casting_costs.rs:8660-8676 supports manual ability payment when automatic payment cannot finish, and :11846-11859 makes only the Auto choice-free verdict final. Why it matters: the upstream candidates are removed before the existing manual-payment authority can expose a legal choice, so the AI cannot take legal Sneak or Web-slinging actions from a sacrificial-source-only mana position. Suggested fix: derive payment mode/feasibility after the alternate cost is known and route Sneak and Web-slinging through the same choice-preserving authority as ordinary casts; add distinct sacrificial-source-only regressions for each action.

Recommendation: request changes.

@nishu-builder

Copy link
Copy Markdown
Contributor Author

All five review comments addressed in b0e7db4 — every one confirmed against the code, the outside-diff Major with a scope correction:

  1. Per-candidate payment mode (outside-diff Major) — confirmed correct on the merits. cast_payment_mode is now computed per candidate: pool-based and free casts remain Auto; mana-paying ordinary, Sneak, and Web-slinging casts preview with their actual prepared costs. The origin-matrix regressions were extended to discriminate the classification.
  2. Defiler-only affordability — the fixture no longer carries the rescuing green ManaUnit; StaticMode::DefilerCostReduction is now load-bearing for the offer.
  3. PlayFaceDown vacuity — both assertions now carry positive sibling-action guards proving the collections are populated, so absence of PlayFaceDown is meaningful.
  4. can_pay_for_spell fallback — the final block asserts through can_pay_for_spell itself, covering the delegation shape the test names.
  5. Splice Auto preservationWaitingFor::SpliceOffer now asserts the pending cast retains CastPaymentMode::Auto.

Additionally self-caught during this round: four tests were silently skipping when generated card data was absent; the suite now asserts its prerequisite instead of vacuously passing.

Verification on head b0e7db44c308b3cf3f7333169e3f8b36023facfd: fmt --check, clippy -D warnings, offer-side module (18 passed), affected regressions (8 passed), full cargo test -p phase-engine — all passed. Gate A PASS head=b0e7db44c308b3cf3f7333169e3f8b36023facfd base=6d7821dced9623609edea342b47dd9c704ff0b36. Final review-impl PASS head=b0e7db44c308b3cf3f7333169e3f8b36023facfd (fresh-context review of the response delta, focused on the payment-mode behavior change).

Model: gpt-5.6-sol

@matthewevans

Copy link
Copy Markdown
Member

Current-head hold: GitHub reports head b0e7db44c308b3cf3f7333169e3f8b36023facfd as mergeStateStatus: DIRTY / mergeable: CONFLICTING. This requires conflict resolution or a rebase before a current-head re-review. The existing requested-changes finding remains in effect.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/engine/src/ai_support/candidates.rs (1)

3512-3530: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a direct CR citation to this closure.

payment_mode_for_cost implements the CR 601.2g-h sacrifice-mana classification, but the citation for this exact behavior appears only at the usage site around Line 3633, not here. A reader who starts at the closure definition sees no rules citation.

Add the citation directly above the closure so the rule is visible at the point of implementation, not only at the point of use.

As per path instructions: crates/engine/** requires that "rules-touching code with no verified CR <number>: <description> annotation" be flagged.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/ai_support/candidates.rs` around lines 3512 - 3530, Add a
verified “CR 601.2g-h” citation describing the sacrifice-mana classification
directly above the payment_mode_for_cost closure. Keep the existing closure
logic unchanged and ensure the annotation is visible at its definition.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/engine/src/ai_support/candidates.rs`:
- Around line 3512-3530: Add a verified “CR 601.2g-h” citation describing the
sacrifice-mana classification directly above the payment_mode_for_cost closure.
Keep the existing closure logic unchanged and ensure the annotation is visible
at its definition.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 671ea99a-44e4-4401-a72c-10c4f9e5cdfe

📥 Commits

Reviewing files that changed from the base of the PR and between 744622c and b0e7db4.

📒 Files selected for processing (10)
  • crates/engine/data/mtgjson-vintage
  • crates/engine/src/ai_support/candidates.rs
  • crates/engine/src/ai_support/mod.rs
  • crates/engine/src/game/casting.rs
  • crates/engine/src/game/casting_costs.rs
  • crates/engine/src/game/casting_tests.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/src/game/mana_payment.rs
  • crates/engine/src/game/splice_tests.rs
  • crates/engine/tests/integration/offer_side_auto_payment.rs
💤 Files with no reviewable changes (1)
  • crates/engine/src/game/casting_tests.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • crates/engine/src/game/splice_tests.rs
  • crates/engine/src/game/mana_payment.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/tests/integration/offer_side_auto_payment.rs
  • crates/engine/src/game/casting.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/src/game/casting.rs`:
- Around line 15136-15143: Update the CastPaymentMode classification around the
mana_source_selections predicate to consider each source’s ability to
participate in a feasible payment for cost, rather than merely checking all
activatable sources. Preserve AutoExceptSacrificialMana when only sacrificial
sources can pay, and select Auto when a non-sacrificial source can contribute;
apply this consistently to regular, Sneak, and Web-slinging announcements and
keep validation and execution aligned through the authoritative engine payment
path.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e55b9b0-9fbf-4dcd-8f01-c5595b5f0597

📥 Commits

Reviewing files that changed from the base of the PR and between b0e7db4 and deef850.

📒 Files selected for processing (4)
  • crates/engine/src/ai_support/candidates.rs
  • crates/engine/src/game/casting.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/tests/integration/offer_side_auto_payment.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/engine/src/ai_support/candidates.rs

Comment thread crates/engine/src/game/casting.rs Outdated
@matthewevans matthewevans self-assigned this Aug 5, 2026
@matthewevans

Copy link
Copy Markdown
Member

Current-head hold — maintainer port required. GitHub reports head deef85077e9cb042b2a52bc67544559f3cc3d092 as mergeStateStatus: DIRTY / mergeable: CONFLICTING, with no auto-merge or queue entry. I reproduced the merge against current main (f16569c30f803ac3f4d208b62bdccedc91891875): the substantive conflicts are crates/engine/src/ai_support/filter.rs and crates/engine/src/game/casting.rs, alongside generated crates/engine/data/mtgjson-vintage churn.

This is maintainer-side concurrency, not a contributor rebase request: main's #6977 priority legality fast path overlaps filter.rs, and #7016's manual-mana castability authority overlaps the exact payment path this PR extends. A blind update would risk discarding either behavior. Please do not rebase solely for this hold. The next step is a maintainer-owned isolated-worktree port that preserves both authorities, followed by a full current-head implementation review and current-head CI/parse-diff evidence before approval.

@matthewevans matthewevans removed their assignment Aug 5, 2026
@nishu-builder

Copy link
Copy Markdown
Contributor Author

The blocker is addressed across two commits (deef8507, cbf04617):

Sneak / Web-slinging manual mana paths [HIGH] — both emitters now derive payment mode and feasibility after the alternate cost is known and route through the same choice-preserving verdict as ordinary casts (no per-emitter special-casing). From a sacrificial-source-only position, both special actions are offered and completable through the manual payment path; genuinely unpayable ones remain hidden. Four new regressions (offered+completable and unpayable-negative, per action). Offer-side module: 22/22.

Follow-up hardening surfaced by the final fresh-context review (cbf04617): ordinary priority preflight was discarding CastSpell announcements for objects castable only through an alternative face (Adventure/split/spell-face MDFC) — the announcement now derives its cost from the legal face via the same face-selection authority as can_cast_object_now, with regressions on the issue-4001 fixture (front-face-unpreparable positive, neither-face negative). Disclosed for transparency: deef8507 was pushed before that review verdict was parsed — an orchestration mistake on our side; the finding was fixed in the immediate follow-up commit rather than left standing.

Verification on head cbf04617d5138139741baee6849c6ab2e062fb7c: fmt --check, clippy -D warnings, offer-side module 22/22, preflight module, issue-4001 integration, four Sneak/Web-slinging regressions, full cargo test -p phase-engine — all passed. Gate A PASS head=cbf04617d5138139741baee6849c6ab2e062fb7c base=6d7821dced9623609edea342b47dd9c704ff0b36. Final review-impl PASS head=cbf04617d5138139741baee6849c6ab2e062fb7c.

Model: gpt-5.6-sol

@matthewevans matthewevans self-assigned this Aug 5, 2026
@matthewevans

Copy link
Copy Markdown
Member

Current-head hold — maintainer port still required.

I rechecked cbf04617d5138139741baee6849c6ab2e062fb7c after the two follow-up commits. GitHub still reports mergeStateStatus: DIRTY / mergeable: CONFLICTING, with no auto-merge or queue entry. Reproducing a merge with current main (6290ef0597f7f87c6d81eb4f1338938019ef0e5b) conflicts in:

  • crates/engine/src/ai_support/filter.rs: this branch's pending-cast provenance helper overlaps main's perf(engine): let a priority pass skip the legality clone #6977 priority-pass structural-legality hatch. The port must retain both independent authorities.
  • crates/engine/src/game/casting.rs: this branch's offer/payment-mode and alternative-face preflight work overlaps main's ship/fix castability fallback for tap mana #7016 exact filter-land payment witness. The port must retain the reducer-backed filter-land route rather than selecting either side of the conflict.
  • crates/engine/data/mtgjson-vintage: generated-date churn only.

This is maintainer-caused staleness: the merge base is 93da0ca15532a03825b1e4eacdbeb9965dd93f6e, while the conflicting mainline authorities arrived afterward. Please do not rebase solely to resolve it. A maintainer will port the two independent behaviors, then obtain current-head Tilt/parse-diff evidence and re-review the combined result; the existing requested-changes state is not an approval.

@matthewevans

Copy link
Copy Markdown
Member

Correction to the current-head hold: the full main SHA used for the reproduced merge is 6290ef0597bc4417a78eb8a0e110a0c2768c82ef (the 6290ef0597 prefix in the prior comment was correct). The three conflict paths, maintainer-caused-staleness classification, and required port/re-review sequence remain unchanged.

@matthewevans matthewevans removed their assignment Aug 5, 2026
@matthewevans matthewevans removed the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Aug 6, 2026
@matthewevans matthewevans self-assigned this Aug 7, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final current-head review: independent adversarial review found no issues; all PR review threads are resolved. Final CI is running on d4c025f.

@matthewevans
matthewevans enabled auto-merge August 8, 2026 01:22
@matthewevans
matthewevans disabled auto-merge August 8, 2026 02:03
@matthewevans matthewevans self-assigned this Aug 8, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Current-head blocker — commander-gated free casts are being filtered out

Reviewed b9f7fd0be38827b46897234fa4854ee15a13e66f. The current parse diff is valid and reports no card-parse changes; the remaining issue is runtime behavior.

Both terminal Rust shards fail the production positive reach-guards at crates/engine/tests/integration/tchaka_venerable_king.rs:330 and :363: legal_actions does not offer the commander-gated free cast for either an owned or a stolen commander. These are real action-surface failures, not test-only mismatches.

crates/engine/src/ai_support/filter.rs:215-251 applies the ordinary post-origin payment verdict after its OptionalCostChoice bypass. Trace CastFromHandFree candidate generation and its fallback end to end, then preserve every free-permission state through the filter—not only the optional-cost and already-prepared NoCost shapes. Add or retain discriminating production-path coverage for each representation.

Auto-merge has been disabled. Do not re-enable it or request approval until this behavior is fixed and fresh current-head CI is green.

@matthewevans

Copy link
Copy Markdown
Member

Maintainer hold — current head f49ab145867ed6d56dff1a026436e806c641981f

The narrow CastSpellForFree early acceptance is re-reviewed as the correct fix for the prior commander free-cast reach-guard failures. This is an evidence hold, not a new requested-changes review.

Still pending for this exact head: Rust lint; Rust tests (both shards); Card data; paired-seed AI; decision-cost perf; and the frontend/client check. The only <!-- coverage-parse-diff --> sticky is bound to prior head b9f7fd0be38827b46897234fa4854ee15a13e66f, not this head.

Approval review resumes only after all checks settle and CI publishes a parse-diff artifact bound to f49ab145867ed6d56dff1a026436e806c641981f. Auto-merge remains disabled; no approval or enqueue yet.

@matthewevans matthewevans removed their assignment Aug 8, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final fixed head dd859e3: all required CI checks pass and no review threads remain unresolved.

@matthewevans
matthewevans added this pull request to the merge queue Aug 8, 2026
@matthewevans
matthewevans removed this pull request from the merge queue due to a manual request Aug 8, 2026
@matthewevans matthewevans self-assigned this Aug 8, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Current-head blocker — production free-cast regression removed

Reviewed dd859e380906dca845708d9b60a0c21d6bdbbc98.

This head deletes the only production legal_actions regression for parser-backed Deadly Rollick's commander-conditional free-cast option from crates/engine/tests/integration/tchaka_venerable_king.rs. The deleted coverage proves the own-commander and stolen-commander Any positives, plus the no-commander negative. Existing unit tests exercise direct resolution, not offer-side candidate generation.

ai_support/filter.rs:226-232 accepts the static CastSpellForFree surface, but does not cover the self casting-option represented by ordinary CastSpell. CI is green because this regression was removed; it does not establish that the offer path remains correct.

Restore the parser-backed exact-action regression (positive own/stolen Any commander and negative no-commander cases), or fix candidate generation and retain equivalent production coverage. Do not re-enable auto-merge until this is resolved.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested — current head 39741686ba6803f794dd0d4611a678cd02c96877

[HIGH] Regular CastSpell candidate filtering still drops the commander-gated free-cast offer. Evidence: the production action-surface regression is at crates/engine/tests/integration/tchaka_venerable_king.rs:272-287; it asks legal_actions for an ordinary GameAction::CastSpell and requires an empty-mana-pool Deadly Rollick offer when its controller controls a commander. Required Rust shard 1 failed this exact assertion at :284 on merge head 6141009f6a3c0dcb85f2580dc64d6c24db95ec0f (head 39741686ba6803f794dd0d4611a678cd02c96877 merged with current base).

Why it matters: ordinary simulation/candidate filtering treats this free permission as unpayable and removes a legal cast from the player’s available actions. The own-commander positive is therefore broken; the adjacent stolen-commander positive at tchaka_venerable_king.rs:296+ remains essential coverage for the same Any commander you control condition.

Suggested fix: trace the ordinary CastSpell path through candidate generation and the post-origin payment verdict, then preserve the commander-conditioned CastSpellForFree/free-permission representation rather than only already-specialized free actions. Keep the existing own/stolen positive and no-commander negative action-surface regression tests, and provide fresh green current-head Rust CI.

The current required CI is not clean (both Rust test shards and the combined Rust check are failing). The parse-diff sticky now correctly names this head and reports no parse changes; it does not establish runtime casting correctness.

@matthewevans matthewevans removed their assignment Aug 8, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved after restoring the parser-backed Deadly Rollick ordinary-CastSpell regression and fixing its shared alternative-cost simulation path. Current head a1750ad810cfb40523a73a1e93ac94fc715a69de has all required CI checks green, including both Rust test shards, card data, paired-seed AI, and decision-cost performance gates.

@matthewevans
matthewevans added this pull request to the merge queue Aug 8, 2026
@matthewevans

Copy link
Copy Markdown
Member

@nishu-builder huge PR! thank you for the contribution :)

Merged via the queue into phase-rs:main with commit 4d27655 Aug 8, 2026
17 checks passed
@nishu-builder

Copy link
Copy Markdown
Contributor Author

You're so welcome! I love your project

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants