Skip to content

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

Open
nishu-builder wants to merge 2 commits into
phase-rs:mainfrom
nishu-builder:fix/offer-side-payment-preview
Open

fix(engine): verify offered casts with the auto-payment authority#7007
nishu-builder wants to merge 2 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

  • Bug Fixes

    • Improved automatic mana payment for complex spells, including Assist, alternative costs, optional costs, variable costs, Morph, and graveyard-based payments.
    • Prevented invalid cast suggestions when targets or payment choices can change the final cost.
    • Preserved required interactive prompts instead of prematurely completing payment.
    • Improved AI casting decisions when mana sources require sacrificing permanents.
  • Improvements

    • Improved support for priority actions, splice offers, and face-down Morph plays.
    • Added safeguards to ensure rejected previews do not alter game state.
    • Improved handling of Sneak, Web-Slinging, and other alternate casting costs.

@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

📝 Walkthrough

Walkthrough

Automatic casting now evaluates effective costs, sacrificial mana, target-dependent modifiers, Assist, interactive payment states, and pending-cast provenance. The change adds phase-specific legality counters and regression coverage for candidate filtering, payment continuation, Morph, splice, and offer-side casting.

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
Shared validation handles cost filters, Assist, effective costs, automatic payment, pending-cast stability, and atomic affordability checks.
Legality phases and candidate payment modes
crates/engine/src/ai_support/*, crates/engine/src/game/perf_counters.rs, crates/engine/src/game/mana_abilities.rs
Candidate generation and validation record phase-specific clone activity. Candidates select payment modes from effective costs and available mana sources.
Casting continuation regressions
crates/engine/src/game/casting_costs.rs, crates/engine/src/game/casting_tests.rs, crates/engine/src/game/splice_tests.rs
Tests cover automatic casts that continue through modal, additional-cost, Assist, Harmonize, collect-evidence, offering, replacement, graveyard-payment, and splice states.
Offer-side and priority integration
crates/engine/tests/integration/*, crates/engine/src/game/engine.rs
Integration tests cover alternative costs, free casts, interactive payments, Morph, casting permissions, pending-cast authorities, and face-down priority announcements.

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

Sequence Diagram(s)

sequenceDiagram
  participant AI
  participant SimulationFilter
  participant Casting
  participant ManaPayment
  participant GameState
  AI->>SimulationFilter: generate and validate cast candidate
  SimulationFilter->>GameState: clone and apply candidate
  SimulationFilter->>Casting: inspect pending spell root
  Casting->>ManaPayment: evaluate automatic payment
  ManaPayment->>GameState: simulate taps and payment choices
  ManaPayment-->>Casting: payable, deferred, or rejected
  Casting-->>SimulationFilter: post-origin validation result
  SimulationFilter-->>AI: retain or reject candidate
Loading

Possibly related PRs

Suggested labels: quality

Suggested reviewers: matthewevans

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: validating offered casts with the automatic-payment authority.
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 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/offer-side-payment-preview
🧪 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

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

Labels

bug Bug fix needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants