Skip to content

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

Open
nishu-builder wants to merge 4 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 4 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, optional, variable, Morph, and graveyard-based costs.
    • Prevented invalid cast suggestions when targets or payment choices affect final costs.
    • Preserved required interactive prompts and rejected previews without changing game state.
    • Improved AI casting decisions involving sacrificial mana sources.
  • Improvements

    • Enhanced priority actions, splice offers, face-down Morph plays, Sneak, and Web-Slinging.
    • Improved handling of alternate casting costs and intermediate casting steps.

@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

Warning

Review limit reached

@nishu-builder, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c3cb3973-8c3c-42ab-94bb-65bdbeebe59f

📥 Commits

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

📒 Files selected for processing (3)
  • crates/engine/src/ai_support/candidates.rs
  • crates/engine/src/game/casting.rs
  • crates/engine/src/game/engine.rs
📝 Walkthrough

Walkthrough

Automatic casting now computes effective costs and payment modes for priority, Sneak, and Web Slinging candidates. Validation handles target-dependent costs, interactive payment states, pending-cast provenance, and atomic mana affordability. The change adds legality instrumentation and broad regression coverage.

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 effective costs, cost filters, Assist, automatic payment, pending-cast stability, Defiler reductions, and atomic affordability.
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 derive payment modes from prepared costs and available mana sources. Announcement conversion preserves those modes.
Legality phases and clone accounting
crates/engine/src/ai_support/filter.rs, crates/engine/src/ai_support/mod.rs, crates/engine/src/game/perf_counters.rs, crates/engine/src/game/mana_abilities.rs
Legality phases record state clones, payment wrappers, probes, post-application calls, and uncached source collection.
Casting continuation and offer-side regressions
crates/engine/src/game/*tests.rs, crates/engine/tests/integration/*, crates/engine/data/mtgjson-vintage
Tests cover interactive costs, alternative and free casts, sacrificial mana, Morph, splice, casting permissions, pending-cast authorities, and fixture data updates.

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

Sequence Diagram(s)

sequenceDiagram
  participant CandidateGeneration
  participant SimulationFilter
  participant Casting
  participant ManaPayment
  participant GameState
  CandidateGeneration->>Casting: prepare effective spell cost
  Casting->>ManaPayment: probe payment mode and feasibility
  ManaPayment->>GameState: simulate taps and payment choices
  ManaPayment-->>Casting: payment mode or rejection
  Casting->>SimulationFilter: apply candidate and inspect pending root
  SimulationFilter-->>CandidateGeneration: retain or reject candidate
Loading

Possibly related PRs

Suggested labels: quality

Suggested reviewers: matthewevans, lgray

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.55% 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 and concisely describes the main change: validating offered casts with the auto-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

@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
@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
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