Skip to content

fix(ai): address community tactical decision reports#6637

Merged
matthewevans merged 8 commits into
mainfrom
ship/ai-community
Jul 25, 2026
Merged

fix(ai): address community tactical decision reports#6637
matthewevans merged 8 commits into
mainfrom
ship/ai-community

Conversation

@matthewevans

@matthewevans matthewevans commented Jul 25, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Improved AI decision-making for combat withdrawals, vehicle crewing, counter costs, payment choices, and fast-priority actions.
    • Added support for planeswalkers with X loyalty, including correct entry counters and copy/transform behavior.
    • Improved mulligan evaluation for modal double-faced cards and additional land sources.
  • Bug Fixes

    • Land sequencing now considers only currently legal alternative land plays.
    • Corrected AI handling of self-bouncing lands, counter replacements, and combat outcomes.
    • Updated legacy card data handling for loyalty values.

@matthewevans
matthewevans enabled auto-merge July 25, 2026 20:30
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

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: 8e62bba2-3218-4825-8429-f051a9047c3a

📥 Commits

Reviewing files that changed from the base of the PR and between 471e7b1 and c2c8622.

📒 Files selected for processing (17)
  • crates/engine/src/ai_support/combat_withdrawal.rs
  • crates/engine/src/game/augment.rs
  • crates/engine/src/game/effects/become_copy.rs
  • crates/engine/src/game/effects/change_zone.rs
  • crates/engine/src/game/effects/token_copy.rs
  • crates/engine/src/game/game_object.rs
  • crates/engine/tests/fixtures/integration_cards.json
  • crates/engine/tests/integration/tamiyo_inquisitive_student_flip.rs
  • crates/phase-ai/src/policies/land_sequencing.rs
  • crates/phase-ai/src/policies/mulligan/aggro_keepables.rs
  • crates/phase-ai/src/policies/mulligan/cedh_keepables.rs
  • crates/phase-ai/src/policies/mulligan/mod.rs
  • crates/phase-ai/src/policies/mulligan/ramp_keepables.rs
  • crates/phase-ai/src/policies/payment_selection.rs
  • crates/phase-ai/src/policies/self_cost.rs
  • crates/phase-ai/src/policies/self_cost_value.rs
  • crates/phase-ai/src/search.rs
📝 Walkthrough

Walkthrough

This PR adds printed-loyalty tracking and X-loyalty counter handling, read-only combat and counter previews, new combat and crew AI policies, MDFC-aware mulligan logic, candidate-aware decision scoring, and recent-thread filtering for Discord exports.

Changes

Engine state and combat analysis

Layer / File(s) Summary
Printed loyalty propagation
crates/engine/src/types/*, crates/engine/src/game/*, crates/engine/tests/integration/*
Printed loyalty is stored, copied, snapshotted, restored, and used for X-based entry counters.
Combat and counter previews
crates/engine/src/game/combat_damage.rs, crates/engine/src/game/effects/counters.rs
Read-only APIs assess fixed combat outcomes and counter replacement results.
Combat withdrawal facts
crates/engine/src/ai_support/*
The engine validates current target prompts and exact removal abilities before returning combat-pair facts.

AI policy behavior

Layer / File(s) Summary
Tactical policies
crates/phase-ai/src/policies/combat_withdrawal.rs, crates/phase-ai/src/policies/crew_timing.rs, crates/phase-ai/src/policies/registry.rs
Combat withdrawal and crew timing policies are implemented, tested, and registered.
Mulligan classification
crates/phase-ai/src/policies/mulligan/*
Mulligan evaluators classify land sources, spell faces, and MDFCs through shared helpers.
Decision scoring
crates/phase-ai/src/policies/payment_selection.rs, crates/phase-ai/src/policies/self_cost*.rs, crates/phase-ai/src/policies/land_sequencing.rs, crates/phase-ai/src/search.rs
Payment, self-cost, land sequencing, and large-board fast-priority decisions use legality-aware scoring.

Supporting updates

Layer / File(s) Summary
Fixtures and scenario expectations
crates/engine/src/**, crates/engine/tests/integration/**, crates/phase-ai/fixtures/*, crates/phase-ai/tests/*
Fixtures initialize printed loyalty and scenario expectations reflect updated action selection.
Discord archive filtering
scripts/export-discord-threads.mjs
Archived thread export is limited to the preceding fourteen days.

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

Possibly related PRs

Suggested labels: bug

Suggested reviewers: kiannidev, ntindle

🚥 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 matches the main theme of the changes: AI tactical decision/report handling and related community scenario updates.
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
  • Commit unit tests in branch ship/ai-community

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

@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR

✓ No card-parse changes detected.

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

Caution

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

⚠️ Outside diff range comments (3)
crates/engine/src/game/effects/become_copy.rs (1)

145-150: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Add production-pipeline coverage for SetStartingLoyalty.

The changed tests only update BackFaceData literals; they do not verify that this path seeds the overridden loyalty counters on entry or that a later copy observes the fixed value instead of the source’s X provenance. Add an integration test under crates/engine/tests/integration/ and register it in tests/integration/main.rs.

🤖 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/effects/become_copy.rs` around lines 145 - 150, Add an
integration test under crates/engine/tests/integration/ covering
SetStartingLoyalty through the production copy pipeline: verify the override
initializes loyalty counters on entry and that a subsequent copy observes the
fixed loyalty value rather than inheriting the source’s X provenance. Register
the new test module or case in tests/integration/main.rs, using existing
integration-test patterns and symbols.

Source: Path instructions

crates/engine/src/game/augment.rs (1)

383-392: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add a local CR citation for printed_loyalty. This rules-sensitive assignment should carry a CR <number>: <description> note explaining why the merged permanent inherits the host’s printed_loyalty.

🤖 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/augment.rs` around lines 383 - 392, Add a local rules
citation comment immediately above the printed_loyalty assignment in the
CopiableValues construction, using the required “CR <number>: <description>”
format and explaining why the merged permanent inherits the host’s
printed_loyalty. Leave the assignment and surrounding fields unchanged.
crates/engine/src/game/game_object.rs (1)

2327-2333: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve printed-loyalty provenance during base synchronization.

revert_layered_characteristics_to_base calls sync_missing_base_characteristics, but that helper does not backfill base_printed_loyalty. A reachable object with printed_loyalty = Some(...) and base_printed_loyalty = None is therefore reset to None here, losing the X-loyalty/printed provenance during layer or zone reversion.

Add the same fallback used for loyalty in sync_missing_base_characteristics.

Proposed fix
         if self.base_loyalty.is_none() && self.loyalty.is_some() {
             self.base_loyalty = self.loyalty;
         }
+        if self.base_printed_loyalty.is_none() && self.printed_loyalty.is_some() {
+            self.base_printed_loyalty = self.printed_loyalty.clone();
+        }
🤖 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/game_object.rs` around lines 2327 - 2333, Update
sync_missing_base_characteristics to backfill base_printed_loyalty from
printed_loyalty when the base value is absent, matching the existing loyalty
fallback. Ensure revert_layered_characteristics_to_base preserves
printed-loyalty provenance for reachable objects during synchronization.
🧹 Nitpick comments (6)
crates/phase-ai/tests/community_scenarios.rs (1)

49-54: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hardcoded scenario id duplicates the data-driven spec mechanism.

The fixture already carries expected_action_type per scenario; a special-cased spec.id branch inside the generic loop means the next scenario needing a stricter assertion adds another if. Since action_type(action) on Line 55 already checks CastSpell for this spec via expected_action_type, this assertion is largely redundant — if the intent is the stronger "must not be the land fast path" claim, encode it as an optional field on CommunityScenario (e.g. forbid_action_type) and drop the id branch. At minimum give the bare assert! a message naming spec.id.

🤖 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/phase-ai/tests/community_scenarios.rs` around lines 49 - 54, The
generic community-scenario loop should not special-case the
"saheeli-legend-loop" identifier. Extend CommunityScenario with an optional
forbidden action-type field, populate it for this fixture, and validate it
alongside the existing action_type(action) expectation so future scenarios reuse
the same mechanism; remove the hardcoded id branch.
crates/phase-ai/src/policies/payment_selection.rs (1)

112-125: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant kind check; the parameter is already destructured from ctx.state.

Line 115 already binds kind: PayCostKind::Discard from ctx.state.waiting_for, and cards != selected pins the candidate; !matches!(kind, PayCostKind::Discard) on Line 123 re-tests the caller-passed kind, which the caller obtained from ctx.decision.waiting_for. Either drop the kind parameter entirely (derive it from state) or drop the destructure — carrying both invites the two to silently disagree.

🤖 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/phase-ai/src/policies/payment_selection.rs` around lines 112 - 125,
Remove the redundant kind validation in the payment-selection logic around the
PayCostKind::Discard destructuring. Use a single source of truth: either remove
the kind parameter and derive it from ctx.state.waiting_for, or stop
destructuring kind there; ensure candidate, player, and discard-state validation
remain consistent without allowing two kind values to diverge.
crates/phase-ai/src/search.rs (1)

594-599: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

actions.contains is a linear scan per candidate, and the baseline only covers CastSpell.

Two smaller points on the same expression chain: the membership test is O(candidates × actions) on the path that exists precisely because the board is large, and PlayLand now carries a 0.0 baseline where the previous heuristic explicitly prioritized the land drop — so a high-mana-value spell can outrank the land drop on this path with nothing but LandSequencingPolicy's 0.0/-1.5 to differentiate. Confirm that is intended, since prefer_land_drop no longer participates here.

🤖 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/phase-ai/src/search.rs` around lines 594 - 599, Update the candidate
filtering around the actions collection to use constant-time membership checks,
such as a set, instead of repeatedly scanning actions. Extend the baseline
calculation in the map closure to preserve the previous land-drop heuristic for
PlayLand, and verify the resulting ordering remains intentional now that
prefer_land_drop is not used on this path.
crates/phase-ai/src/policies/crew_timing.rs (1)

67-91: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Enumerates and re-simulates every legal crew subset instead of short-circuiting.

crew_has_exact_combat_use collects all engine-legal CrewVehicle subset actions for this vehicle and clone+simulates each one via crew_subset_has_exact_combat_use until it finds a working subset. On boards with several creatures able to satisfy the crew cost, the number of legal subsets can grow combinatorially, and the worst case — no subset yields combat use, which is precisely the scenario this policy exists to flag (e.g. real_vehicle_postcombat_crew_is_not_treated_as_an_attack_use) — pays the full enumeration + per-subset clone/fast-forward cost with no early exit.

Since whether the crewed Vehicle can attack/block afterward generally depends only on it being crewed (not on which specific creatures supplied the power), a single arbitrary legal subset should be sufficient to answer the question, avoiding the combinatorial blowup.

♻️ Proposed short-circuit using a single legal subset
     legal_actions_full(&selection_state)
         .0
         .iter()
-        .filter(|action| {
+        .find(|action| {
             matches!(
                 action,
                 GameAction::CrewVehicle {
                     vehicle_id: candidate_vehicle,
                     creature_ids,
                 } if *candidate_vehicle == vehicle_id && !creature_ids.is_empty()
             )
         })
-        .any(|subset| crew_subset_has_exact_combat_use(&selection_state, actor, vehicle_id, subset))
+        .is_some_and(|subset| crew_subset_has_exact_combat_use(&selection_state, actor, vehicle_id, subset))

As per path instructions, "Combination generators should short-circuit infeasible cases before enumerating."

🤖 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/phase-ai/src/policies/crew_timing.rs` around lines 67 - 91, Update
crew_has_exact_combat_use to select one matching legal CrewVehicle action for
vehicle_id with a non-empty creature_ids list, then pass only that subset to
crew_subset_has_exact_combat_use. Remove the all-subsets iteration so the
function avoids enumerating and re-simulating every legal crew combination while
preserving the existing false result when no legal subset exists.

Source: Path instructions

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

58-75: 🎯 Functional Correctness | 🔵 Trivial | ⚖️ Poor tradeoff

Only WaitingFor::TargetSelection is handled; triggered remove-from-combat prompts fall through.

The sibling accessor added in ai_support/mod.rs deliberately covers both TargetSelection and TriggerTargetSelection, but this facade destructures only the former, so any triggered "remove target creature from combat" (a whole card class — e.g. ETB/attack triggers that fog a blocker) always yields None and the new combat-withdrawal policy never evaluates it. It fails closed, so this is a coverage gap rather than a bug, but the pattern-matching asymmetry with current_target_selection_targets will read as an oversight later.

TriggerTargetSelection has no pending_cast, so extending it means sourcing the ability/source from the trigger fields — worth doing deliberately or documenting the exclusion in the module doc comment.

🤖 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/combat_withdrawal.rs` around lines 58 - 75,
Extend the combat-withdrawal matching around the current
WaitingFor::TargetSelection destructure to deliberately handle
TriggerTargetSelection as well, sourcing the ability and source identity from
its trigger fields so the same validation applies to triggered
remove-from-combat prompts. Keep compatibility with
current_target_selection_targets, and ensure unrelated waiting states still
return None.
crates/engine/src/game/effects/counters.rs (1)

2814-2954: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test covers the Unsupported arm.

Applied / Transformed / Prevented / ChoiceRequired / stale-incarnation are all exercised, but the event-class-rewrite path (ReplacementResult::Execute(_)Unsupported) is never reached. That arm is the one whose contract consumers are explicitly told not to conflate with None, so it's the arm most likely to regress silently — a replacement that redirects AddCounter into another ProposedEvent would start returning Applied-shaped nonsense if the arm ordering ever changed.

🤖 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/effects/counters.rs` around lines 2814 - 2954, Add a
focused test for preview_counter_addition that installs a replacement for
AddCounter rewriting it to a different ProposedEvent, then assert it returns
Some(CounterAdditionPreview::Unsupported) rather than None or an Applied-shaped
result. Keep the test focused on the event-class rewrite path represented by
ReplacementResult::Execute(_) and verify the target/state setup required to
reach that arm.
🤖 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/ai_support/combat_withdrawal.rs`:
- Around line 439-456: Update the combat state setup in the test around
combat_withdrawal_fact_for_current_target to represent a genuinely unblocked
attacker: clear blocker_to_attacker alongside blocker_assignments and set the
combatant’s blocked flag to false before installing target selection. Keep the
existing NoCombatPair assertion unchanged.

In `@crates/engine/src/game/effects/change_zone.rs`:
- Line 9498: Update the regression fixture’s printed-loyalty field from None to
Some(3) to represent the back face’s printed loyalty and ensure the test
exercises printed-loyalty propagation rather than the legacy loyalty fallback.

In `@crates/engine/src/game/effects/token_copy.rs`:
- Around line 492-496: Update the nearby copy-token regression tests to assert
both printed_loyalty and base_printed_loyalty equal
Some(PrintedLoyalty::Fixed(1)), covering the standard and liminal paths
identified by the related test sections. Ensure the assertions execute through
the production token-copy pipeline and fail if printed-loyalty propagation or
its base value regresses.

In `@crates/engine/src/game/game_object.rs`:
- Around line 188-189: Document the printed-loyalty fields in BackFaceData,
GameObject, and base_printed_loyalty with comments referencing CR 306.5b and
306.5c: printed loyalty is the face value that seeds loyalty counters on entry,
while live loyalty is the counter-derived battlefield value. Keep the existing
field types and serde attributes unchanged.

In `@crates/engine/tests/integration/tamiyo_inquisitive_student_flip.rs`:
- Around line 91-95: Update the planeswalker back-face fixture in the relevant
transform test so its `printed_loyalty` field is populated with 2, matching
`loyalty`. Keep the fixture production-shaped and ensure the test exercises the
printed-loyalty validation path rather than allowing an absent value.

In `@crates/phase-ai/src/policies/land_sequencing.rs`:
- Around line 322-336: Update
unavailable_non_bounce_land_in_hand_is_not_an_alternative to obtain its play
candidates through the production engine::ai_support candidate-generation path,
using a real play restriction or an exhausted
lands_played_this_turn/max_lands_per_turn state. Avoid manually passing a
candidate list that omits restricted; assert the verdict using the
engine-derived legal actions so the regression verifies production legality
filtering.

In `@crates/phase-ai/src/policies/mulligan/cedh_keepables.rs`:
- Around line 143-146: Exclude land sources that also have a spell face from the
land counts used by the mulligan thresholds, mirroring land_only_count and
has_spell_face. Update count_lands_in_hand in
crates/phase-ai/src/policies/mulligan/cedh_keepables.rs (lines 143-146) before
the land_count > 4 ForceMulligan check; apply the same filtering in
crates/phase-ai/src/policies/mulligan/aggro_keepables.rs (lines 58-63) for the
lands >= 5 flooded penalty and
crates/phase-ai/src/policies/mulligan/ramp_keepables.rs (lines 64-69) for the
ramp_count == 0 && land_count >= 3 penalty.

In `@crates/phase-ai/src/policies/payment_selection.rs`:
- Around line 176-191: Restrict playable_lands_after_stack_clears to clear the
forecast stack only when the sole stack entry is controlled by the specified
player; otherwise return no playable lands. Preserve the existing bounded
forecast behavior for an empty stack or a single player-controlled entry, while
continuing to reject stacks with multiple entries.
- Around line 783-813: Add positive reach-guards to the negative assertions in
production_discard_probe_does_not_treat_one_of_two_retained_lands_as_final,
multi_card_discard_does_not_penalize_a_land_when_another_land_survives_the_candidate,
and the related tests through the indicated range. Before asserting the detector
is false, verify the sibling replay reaches the tested path by asserting
playable_lands_after_stack_clears is non-empty; initialize phase and
active_player consistently with the production scenario when required.
- Around line 127-165: Reorder discard_spends_last_playable_land so it returns
false before the selected-payment and sibling probes when selected contains no
land, moving the existing is_land check ahead of apply_as_current_for_simulation
and playable_lands_after_stack_clears. Also reuse cached sibling-outcome results
for candidates sharing the same waiting_for context, rather than recomputing
full state clones for every candidate.

In `@crates/phase-ai/src/policies/self_cost_value.rs`:
- Around line 122-142: The doc comment for counter_replenishment_verdict
contradicts its match behavior. Update the comment to state that Prevented and
Unsupported receive the bounded penalty, while Applied, ChoiceRequired, and
Transformed remain neutral; describe Unsupported as the redirected or
unsupported event-class case rather than a completely rewritten counter event.
- Around line 1343-1386: Strengthen the table-driven test around the installed
replacement functions applied, transformed, and choice_required: invoke
self_counter_cost_preview directly for each corresponding setup and assert the
expected Applied, Transformed, or ChoiceRequired preview outcome, rather than
only checking the self_cost_value_na verdict reason. Keep the prevented row’s
penalized verdict assertion, and ensure the test still obtains each replacement
through the normal production setup so it exercises the previously unverified
preview-mismatch failure path.

In `@crates/phase-ai/src/policies/self_cost.rs`:
- Around line 112-120: The self-cost detection logic around the AbilityCost
match must also handle a single RemoveCounter component wrapped in
AbilityCost::Composite. Unwrap Composite only when it contains exactly one cost,
recursively apply the existing RemoveCounter matching logic to that component,
and continue rejecting composites with multiple costs or unrelated cost shapes.

In `@crates/phase-ai/src/search.rs`:
- Around line 4241-4248: The test
large_board_main_phase_fast_action_uses_bounded_policy_scoring must explicitly
validate the large-board gate. Assert after make_state and creature setup that
state.objects.len() meets LARGE_BOARD_FAST_PRIORITY_OBJECTS, and add a sibling
case with fewer objects that invokes the same fast-action path and asserts None,
isolating behavior below the threshold.
- Around line 591-619: Update fast_priority_action’s candidate-selection
pipeline to apply the same gate_candidates safety filtering used by
score_candidates_core before scoring or selecting an action. Ensure
cancelled_casts, pending_activations, MAX_ACTIVATIONS_PER_SOURCE_PER_TURN, and
MAX_CASTS_OF_SAME_CARD_PER_TURN are enforced on this bounded path, preferably by
reusing gate_candidates or extracting a shared predicate used by both paths.
- Around line 90-94: The large-board fast-path threshold currently counts all
state objects, causing ordinary games to bypass normal search; change it to use
a battlefield-sized measure or restore a genuinely pathological threshold, and
apply that same measure at both call sites in search.rs (lines 305 and 574). In
the bounded candidate-selection path around the fast-priority logic, reuse the
shared gating/filter predicate from score_candidates_core before selecting an
action so cancelled casts, pending activations, and per-turn activation or
same-card cast caps remain enforced.

---

Outside diff comments:
In `@crates/engine/src/game/augment.rs`:
- Around line 383-392: Add a local rules citation comment immediately above the
printed_loyalty assignment in the CopiableValues construction, using the
required “CR <number>: <description>” format and explaining why the merged
permanent inherits the host’s printed_loyalty. Leave the assignment and
surrounding fields unchanged.

In `@crates/engine/src/game/effects/become_copy.rs`:
- Around line 145-150: Add an integration test under
crates/engine/tests/integration/ covering SetStartingLoyalty through the
production copy pipeline: verify the override initializes loyalty counters on
entry and that a subsequent copy observes the fixed loyalty value rather than
inheriting the source’s X provenance. Register the new test module or case in
tests/integration/main.rs, using existing integration-test patterns and symbols.

In `@crates/engine/src/game/game_object.rs`:
- Around line 2327-2333: Update sync_missing_base_characteristics to backfill
base_printed_loyalty from printed_loyalty when the base value is absent,
matching the existing loyalty fallback. Ensure
revert_layered_characteristics_to_base preserves printed-loyalty provenance for
reachable objects during synchronization.

---

Nitpick comments:
In `@crates/engine/src/ai_support/combat_withdrawal.rs`:
- Around line 58-75: Extend the combat-withdrawal matching around the current
WaitingFor::TargetSelection destructure to deliberately handle
TriggerTargetSelection as well, sourcing the ability and source identity from
its trigger fields so the same validation applies to triggered
remove-from-combat prompts. Keep compatibility with
current_target_selection_targets, and ensure unrelated waiting states still
return None.

In `@crates/engine/src/game/effects/counters.rs`:
- Around line 2814-2954: Add a focused test for preview_counter_addition that
installs a replacement for AddCounter rewriting it to a different ProposedEvent,
then assert it returns Some(CounterAdditionPreview::Unsupported) rather than
None or an Applied-shaped result. Keep the test focused on the event-class
rewrite path represented by ReplacementResult::Execute(_) and verify the
target/state setup required to reach that arm.

In `@crates/phase-ai/src/policies/crew_timing.rs`:
- Around line 67-91: Update crew_has_exact_combat_use to select one matching
legal CrewVehicle action for vehicle_id with a non-empty creature_ids list, then
pass only that subset to crew_subset_has_exact_combat_use. Remove the
all-subsets iteration so the function avoids enumerating and re-simulating every
legal crew combination while preserving the existing false result when no legal
subset exists.

In `@crates/phase-ai/src/policies/payment_selection.rs`:
- Around line 112-125: Remove the redundant kind validation in the
payment-selection logic around the PayCostKind::Discard destructuring. Use a
single source of truth: either remove the kind parameter and derive it from
ctx.state.waiting_for, or stop destructuring kind there; ensure candidate,
player, and discard-state validation remain consistent without allowing two kind
values to diverge.

In `@crates/phase-ai/src/search.rs`:
- Around line 594-599: Update the candidate filtering around the actions
collection to use constant-time membership checks, such as a set, instead of
repeatedly scanning actions. Extend the baseline calculation in the map closure
to preserve the previous land-drop heuristic for PlayLand, and verify the
resulting ordering remains intentional now that prefer_land_drop is not used on
this path.

In `@crates/phase-ai/tests/community_scenarios.rs`:
- Around line 49-54: The generic community-scenario loop should not special-case
the "saheeli-legend-loop" identifier. Extend CommunityScenario with an optional
forbidden action-type field, populate it for this fixture, and validate it
alongside the existing action_type(action) expectation so future scenarios reuse
the same mechanism; remove the hardcoded id branch.
🪄 Autofix (Beta)

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: 6a998eb5-6c87-472d-a3ac-34449307b83f

📥 Commits

Reviewing files that changed from the base of the PR and between 28920af and 471e7b1.

📒 Files selected for processing (77)
  • crates/engine/src/ai_support/candidates.rs
  • crates/engine/src/ai_support/combat_withdrawal.rs
  • crates/engine/src/ai_support/mod.rs
  • crates/engine/src/game/augment.rs
  • crates/engine/src/game/casting_tests.rs
  • crates/engine/src/game/combat_damage.rs
  • crates/engine/src/game/day_night.rs
  • crates/engine/src/game/effects/become_copy.rs
  • crates/engine/src/game/effects/change_zone.rs
  • crates/engine/src/game/effects/counters.rs
  • crates/engine/src/game/effects/flip_coin.rs
  • crates/engine/src/game/effects/flip_permanent.rs
  • crates/engine/src/game/effects/prepare.rs
  • crates/engine/src/game/effects/set_room_door_lock.rs
  • crates/engine/src/game/effects/token.rs
  • crates/engine/src/game/effects/token_copy.rs
  • crates/engine/src/game/effects/transform_effect.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/src/game/engine_debug.rs
  • crates/engine/src/game/engine_mdfc_land_tests.rs
  • crates/engine/src/game/engine_tests.rs
  • crates/engine/src/game/flip.rs
  • crates/engine/src/game/game_object.rs
  • crates/engine/src/game/layers.rs
  • crates/engine/src/game/printed_cards.rs
  • crates/engine/src/game/replacement.rs
  • crates/engine/src/game/specialize.rs
  • crates/engine/src/game/stack.rs
  • crates/engine/src/game/transform.rs
  • crates/engine/src/game/triggers.rs
  • crates/engine/src/game/zone_pipeline.rs
  • crates/engine/src/game/zones.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/src/types/card.rs
  • crates/engine/src/types/game_state.rs
  • crates/engine/src/types/layers.rs
  • crates/engine/tests/integration/azors_gateway_transform_condition.rs
  • crates/engine/tests/integration/combo_infinite_pile.rs
  • crates/engine/tests/integration/craft_tithing_blade_transform.rs
  • crates/engine/tests/integration/integration_adventure.rs
  • crates/engine/tests/integration/issue_2425_fable_chapter_iii_transform.rs
  • crates/engine/tests/integration/issue_4001_frolicking_familiar_adventure_instant.rs
  • crates/engine/tests/integration/issue_4239_nissa_steward_x_loyalty.rs
  • crates/engine/tests/integration/issue_5326_avatar_aang_transform.rs
  • crates/engine/tests/integration/issue_691_sheoldred_saga_lore.rs
  • crates/engine/tests/integration/kamigawa_flip_cards.rs
  • crates/engine/tests/integration/main.rs
  • crates/engine/tests/integration/room_door_lock_unlock.rs
  • crates/engine/tests/integration/rules/battle.rs
  • crates/engine/tests/integration/specialize_runtime.rs
  • crates/engine/tests/integration/std_s07_batch5b.rs
  • crates/engine/tests/integration/stolen_goodies_zero_targets.rs
  • crates/engine/tests/integration/tamiyo_inquisitive_student_flip.rs
  • crates/engine/tests/integration/wedding_announcement_transform.rs
  • crates/phase-ai/fixtures/scenarios/community-scenarios.json
  • crates/phase-ai/src/config.rs
  • crates/phase-ai/src/policies/combat_withdrawal.rs
  • crates/phase-ai/src/policies/crew_timing.rs
  • crates/phase-ai/src/policies/land_sequencing.rs
  • crates/phase-ai/src/policies/mod.rs
  • crates/phase-ai/src/policies/mulligan/aggro_keepables.rs
  • crates/phase-ai/src/policies/mulligan/aristocrats_keepables.rs
  • crates/phase-ai/src/policies/mulligan/cedh_keepables.rs
  • crates/phase-ai/src/policies/mulligan/keepables_by_land_count.rs
  • crates/phase-ai/src/policies/mulligan/landfall_keepables.rs
  • crates/phase-ai/src/policies/mulligan/mod.rs
  • crates/phase-ai/src/policies/mulligan/plus_one_counters_keepables.rs
  • crates/phase-ai/src/policies/mulligan/ramp_keepables.rs
  • crates/phase-ai/src/policies/mulligan/spellslinger_keepables.rs
  • crates/phase-ai/src/policies/mulligan/tokens_wide_keepables.rs
  • crates/phase-ai/src/policies/payment_selection.rs
  • crates/phase-ai/src/policies/registry.rs
  • crates/phase-ai/src/policies/self_cost.rs
  • crates/phase-ai/src/policies/self_cost_value.rs
  • crates/phase-ai/src/search.rs
  • crates/phase-ai/tests/community_scenarios.rs
  • scripts/export-discord-threads.mjs

Comment thread crates/engine/src/ai_support/combat_withdrawal.rs
Comment thread crates/engine/src/game/effects/change_zone.rs Outdated
Comment thread crates/engine/src/game/effects/token_copy.rs
Comment thread crates/engine/src/game/game_object.rs
Comment thread crates/engine/tests/integration/tamiyo_inquisitive_student_flip.rs
Comment thread crates/phase-ai/src/policies/self_cost_value.rs Outdated
Comment thread crates/phase-ai/src/policies/self_cost.rs Outdated
Comment thread crates/phase-ai/src/search.rs Outdated
Comment thread crates/phase-ai/src/search.rs Outdated
Comment thread crates/phase-ai/src/search.rs
@matthewevans
matthewevans added this pull request to the merge queue Jul 25, 2026
Merged via the queue into main with commit e50ae6b Jul 25, 2026
18 checks passed
@matthewevans
matthewevans deleted the ship/ai-community branch July 25, 2026 22:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant