feat(engine): implement Jetfire, Ingenious Scientist // Jetfire, Air Guardian#6056
feat(engine): implement Jetfire, Ingenious Scientist // Jetfire, Air Guardian#6056JacobWoodson wants to merge 10 commits into
Conversation
…Guardian
Transformers DFC (set BOT). Almost every mechanic already existed (More
Than Meets the Eye, Living metal, Flying, convert->transform, adapt, the
"can't be spent to cast nonartifact spells" restriction and its
cross-sentence attachment, and the counter-removal cost -> chosen_x ->
"that much" wiring). Four surgical changes complete the two activated
abilities:
- parser: `strip_mana_subject_prefix` recognizes "target player adds" as
a genuine chosen recipient (`TargetFilter::Player`), distinct from the
"active/that player" anaphors (CR 115.1 + CR 106.4).
- engine: `mana_effect_recipient` deposits into the targeted player for a
`Player` recipient, provenance-gated by a new `mana_count_reads_targets`
so a count-source `Player` target ("Add {U} for each card in target
player's hand") still pays the controller (no Jeska's Will regression).
- parser: "that much {C}" now parses as a count-prefixed colorless
production (was falling to Unimplemented).
- parser: "adapt" added to the clause-start verb table so
"Convert Jetfire, then adapt 3" chains the adapt sub-ability.
No new enum variants. Adds 6 tests (both faces + the recipient clause +
the multi-authority recipient/count-source gate + spend-restriction
deposit). Full engine suite green; fmt + clippy clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request implements support for target player mana recipients and dynamic colorless mana production (e.g., for Jetfire, Ingenious Scientist), including parsing, resolution, and associated tests. The review feedback highlights style guide violations regarding non-exhaustive matches with wildcard fallbacks in quantity_expr_reads_targets and quantity_ref_reads_targets, as well as a missing CR annotation in quantity_expr_reads_targets.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
matthewevans
left a comment
There was a problem hiding this comment.
Blocked — the new generic target-player mana path still conflates a mana recipient with a count source.
🔴 Blocker
crates/engine/src/parser/oracle_effect/mana.rs:193-201 recursively parses the bare add … clause before it stamps the subject recipient. For Target player adds {C} for each card in target player's hand, the bare parser already emits Effect::Mana.target = Some(TargetFilter::Player) as the count-source target, so the wrapper leaves that field unchanged. crates/engine/src/game/effects/mana.rs:35-44 then uses the production count to reinterpret that same field and returns ability.controller whenever the count reads the target. That sends mana to the controller even though the sentence's subject is the chosen target player.
The current Jetfire fixture is target-independent (that much), so it does not reach this failure mode. The new parser arm accepts the broader subject-led class, however, and the implementation has no typed provenance that distinguishes its recipient target from the count target. The wildcard fallbacks in quantity_expr_reads_targets / quantity_ref_reads_targets (effects/mana.rs:87-144, also raised by Gemini) make that inference incomplete as the quantity vocabulary grows.
Suggested fix: model the two roles explicitly at the Effect::Mana/mana-production boundary (recipient vs count-source) and thread their independently surfaced target slots through parsing and resolution. Do not use quantity-shape inference to reinterpret the one overloaded target field; add a runtime fixture for the target-recipient + target-derived-count class as well as the existing Jetfire and Jeska's Will cases.
✅ Clean
The Scryfall Oracle text confirms Jetfire's front and back clauses, and the change uses the existing convert/adapt and restricted-mana seams. The Jetfire-specific parser assertions are appropriately detailed.
Recommendation: request changes for the explicit target-role model, then re-run current-head parser/engine evidence.
Parse changes introduced by this PR · 58 card(s), 21 signature(s) (baseline: main
|
…rget roles Addresses the review blocker on PR phase-rs#6056. The previous commit resolved a targeted-player mana recipient by inferring the role from the production count's shape, which conflated two distinct CR 601.2c "target" instances that share one Effect::Mana.target field. CR 601.2c requires an independent choice per instance of the word "target". A mana sentence can name two: the RECIPIENT ("Target player adds that much {C}" -- Jetfire) and the COUNT SOURCE ("Add {R} for each card in target opponent's hand" -- Jeska's Will; Carpet of Flowers). One field could not carry both, so a sentence naming both silently dropped the recipient and deposited the mana to the controller. - types: new ManaTargetRole (Recipient / CountSource / Both) replaces Effect::Mana.target's bare Option<TargetFilter>. Roles are stamped at parse time from GRAMMAR, never inferred from quantity shape. - parser: both subject-stamping routes now COMBINE into Both instead of declining on is_none() (the clobber) -- oracle_effect/mana.rs and the subject-predicate route in oracle_effect/mod.rs. Count-source producers stamp CountSource at the point of grammatical knowledge. - resolution: each role resolves against ITS OWN slot via ability_scoped_to_slot, leaving shared quantity resolution untouched. mana_effect_recipient returns Option<PlayerId> so an illegal recipient deposits nowhere rather than falling back to the controller (CR 608.2b). - targeting: multi-role mana surfaces one slot per role (recipient first) in collect_target_slots / collect_target_slot_specs, with matching arms in both assign fns, chain_has_target_sink, minimum_targets_in_chain, and a position-stable validate_targets_in_chain arm. retarget_slot_violation enforces per-slot CR 115.7a legality. - deletes mana_count_reads_targets, mana_production_count, quantity_expr_reads_targets and quantity_ref_reads_targets -- removing the inference and, by construction, the two wildcard match arms flagged in review. Effect::target_filter() still returns the sole declared filter, so all 13 shipping mana-target cards keep byte-identical routing; only the two-role shape enters the new arms. Fixture roles were migrated by card name, because the role is not recoverable from filter shape -- Carpet of Flowers and Spectral Searchlight are both Typed with opposite roles. One mechanical or-pattern repair in mtgish-import; ability_rw's D5 scan was split rather than dropped, so its verdicts are unchanged for all 11 fixture mana cards. 18 new tests, including the maintainer-requested end-to-end fixture for the recipient + target-derived-count class (two players with different hand sizes, so a slot mix-up fails). Full engine suite 16743 passed / 0 failed; integration 3164 passed / 0 failed; clippy --workspace --all-targets clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks @matthewevans — the critique was correct, and digging into it showed the problem was worse than "architecturally untidy". Pushed as The bug you predicted is realFor What changed
Blast radius was deliberately held to the new class
Fixture migration is name-keyed, not shape-derived
Tests — all three classes you asked for
18 new tests total. Engine suite 16743 passed / 0 failed; integration 3164 / 0; Known gaps, stated plainly
One deliberate behavioral note: |
matthewevans
left a comment
There was a problem hiding this comment.
[MED] An illegal count-source target can make the effect count the recipient instead. Evidence: crates/engine/src/game/effects/mana.rs:110-120 falls back to ability.clone() whenever ability_scoped_to_slot(..., CountSource) returns None; crates/engine/src/game/quantity.rs:2134-2141 then reads the first player target. Why it matters: for a Both mana effect whose recipient remains legal but whose count source becomes illegal, the fallback exposes the recipient as that first target, so the effect produces mana based on the wrong player's hand rather than failing to determine the illegal target's information (CR 608.2b). Suggested fix: distinguish “no count-source role” from “declared count source no longer legal”; for the latter resolve the count through an empty/invalid scoped target view (or skip the dependent mana instruction), and add a cast-pipeline regression with a legal recipient and an illegal count source having different hand sizes.
… count the recipient Addresses the second review round on PR phase-rs#6056. count_scoped_ability fell back to the UNSCOPED ability whenever ability_scoped_to_slot(.., CountSource) returned None. That conflated "no count-source role declared" with "declared count source is no longer legal": in the latter case the unscoped ability still holds the LEGAL recipient, and QuantityRef::TargetZoneCardCount scans for the first player target, so a Both role whose recipient stayed legal but whose count source became illegal produced mana from the RECIPIENT's hand instead of failing to determine the illegal target's information (CR 608.2b). count_scoped_ability now distinguishes four cases: 1. no count-source role -- unscoped, exactly today's behavior for every single-role mana; 2. context-ref count source -- resolved through context, mirroring mana_effect_recipient. Previously this fell into case 4 and silently resolved to 0 under a CR 608.2b citation that does not apply to it, since nothing there is an illegal target; 3. declared and still legal -- narrowed to that one player; 4. declared but illegal -- no player exposed, so the count resolves to 0. Scoping is confined to the PLAYER axis. retain_only_player_at drops only TargetRef::Player entries and leaves non-player targets at their original positions, because ManaProduction::AnyCombinationOfObjectColors { scope: Target } reads an OBJECT target from the same vec and requires nothing about the count source -- clearing the whole vec would make that half of the production silently yield no colors, which CR 608.2b does not license. The same defect existed at a second site the review did not cite: handle_choose_mana_effect -> chosen_mana_types_for_prompt also derives the count (its SingleColor arm multiplies the chosen color by it) and was passing the unscoped ability. Both sites now route through count_scoped_ability. New cast-pipeline regression: 3 players, recipient P1 with 2 cards, count source P2 with 5, P2 eliminated between SelectTargets and resolution. Reverting the fix fails it with left: 2, right: 2 -- the count read the recipient's hand, exactly the reported defect. Guards assert the recipient stayed legal and that EffectResolved{Mana} actually fired, so a whole-spell fizzle cannot satisfy the zero-mana assertions vacuously. Engine suite 16743 passed / 0 failed; integration 3165 passed / 0 failed; clippy --workspace --all-targets clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Confirmed and fixed in The root cause was that
Two things beyond the reported instanceThe same defect existed at a second site you didn't cite. Scoping is now confined to the player axis. My first pass cleared the whole Regression test
Engine suite 16743 passed / 0 failed; integration 3165 / 0; Still open from my earlier note
|
matthewevans
left a comment
There was a problem hiding this comment.
Blocked — the current fix addresses the earlier illegal count-source fallback, but the authorized maintainer port cannot safely complete under the Tilt-only verification policy.
The attempted merge across current main has one conflict in crates/engine/tests/fixtures/integration_cards.json. This is a generated single-line fixture, not a textual choice: this PR regenerates 12 mana-related entries, while current main has a broad independent regeneration (2,945 keys versus this branch's 2,807). Several entries overlap. Taking either side would silently discard parser-derived fixture behavior; producing the merged artifact requires the project card-data/Tilt path, which is not attached to this isolated worktree and must not be replaced by a direct build.
The targeted code review of f4789fa found the new count-scoped view and its real cast-pipeline regression appropriately address the prior recipient-count fallback. The remaining blocker is the verified generated-fixture merge/verification conflict only.
Recommendation: hold this head until a Tilt-attached maintainer port can regenerate and verify the fixture.
…iner port
The generated fixture crates/engine/tests/fixtures/integration_cards.json
conflicts with main and cannot be resolved by taking either side: this branch
carries the ManaTargetRole schema change while main independently regenerated
the fixture (2,945 keys vs 2,807; 139 cards only on main; 1,278 shared entries
differ). Producing the merged artifact requires the project card-data path,
which is not available in this worktree, so the port belongs to a Tilt-attached
maintainer.
This script makes the schema half of that port mechanical. After the fixture is
regenerated normally, run:
node scripts/migrate-mana-target-roles.mjs
It rewrites Effect::Mana.target from the legacy bare TargetFilter encoding to
the ManaTargetRole encoding for the 11 affected cards.
Keyed by CARD NAME, deliberately. The role is not recoverable from the
serialized filter: carpet of flowers (Typed{controller:"Opponent"}) is a
CountSource while spectral searchlight (Typed{controller:ChosenPlayer(0)}) is a
Recipient -- identical outer shape, opposite roles. Inferring the role from the
filter or from the production's quantity shape is precisely the defect this PR
removes and was rejected in review, so the script FAILS LOUDLY on any
mana-target card missing from the table rather than guessing, and tells the
reader how to decide the role from Oracle text.
The migration set was verified stable: main's independent regeneration contains
the same 11 mana-target entries, name-for-name, despite adding 139 cards.
Verified end to end against real data:
- applying it to main's regenerated fixture migrates exactly 11 entries and
yields encodings IDENTICAL to this branch's for all 11;
- idempotent (re-running reports 11 already in role form, writes nothing);
- --check reports without writing;
- an injected unmapped mana-target card exits 1 with guidance;
- output stays a single minified line, guarded in-script.
Node rather than Python to match gen-test-fixture.py's sibling only in spirit:
python3 is unavailable in this environment, and shipping a script I could not
execute would defeat the purpose.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Maintainer status — held on a real merge conflict. Current head |
…ters-3b456b # Conflicts: # crates/engine/src/parser/oracle_effect/mod.rs # crates/engine/tests/fixtures/integration_cards.json
Two conflicts the textual merge could not see: - main added a 'chooser' field to TargetSelectionSlot; the multi-role Mana slot arm now constructs it with chooser: None like every sibling arm (the SlotAccumulator stamps the actual chooser). - main added a new direct reader of Effect::Mana.target in ability_scan's loop-firewall arm (a ninth reader, added after this branch's reader audit). It now walks role.declared_filters() so BOTH role filters are scanned, mirroring the D5 legacy scan and the AI POISON scan; a partial view would let the loop firewall miss a target-derived axis. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Main added gzipped GameState dumps (kilo_live_offer_from_real_dump,
combo_infinite_pile, sprout_inalla_realistic_offer) whose objects carry parsed
Effect::Mana abilities in the pre-role encoding. A state deserializes through
typed serde BEFORE any restore-time migration hook can run, so the dumps
themselves must carry the role encoding; without this, 6 lib + 20 integration
tests fail with missing-field 'role' (plus one collateral failure via a
poisoned shared fixture cache).
migrate-mana-target-roles.mjs gains a --state mode: gunzip, wrap each Mana
target by the OWNING OBJECT's card name through the same fail-loud name-keyed
table, regzip. Three cards appear in dumps but not the curated card fixture;
each role was decided from Scryfall Oracle text, never inferred from shape:
wolfwillow haven -> Recipient ('its controller adds an additional {G}')
priest of forgotten gods -> Recipient ('You add {B}{B} and draw a card')
rousing refrain -> CountSource ('Add {R} for each card in target
opponent's hand' - the Jeska's
Will clause verbatim)
These are excluded from fixture mode's drop-detection count via
STATE_ONLY_CARDS so the curated-11 guard keeps its teeth. All four dumps
migrated (24 entries), idempotency verified via --state --check.
NOTE for maintainers: this migrates TEST dumps only. If production persisted
games can carry pre-role Mana targets across an engine upgrade, the restore
path needs a JSON-level migration before typed deserialization.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(N) bound bulk_treasure_activation_is_linear_not_factorial reads MANA_READINESS_CALLS, a bare process-global AtomicUsize with no test serialization, so any concurrently scheduled test that exercises mana readiness increments it between this test's store(0) and load. The branch's ~18 added lib tests shifted the parallel schedule enough to tip the 4*N bound by exactly one (got 25, bound 24); the full suite passes single-threaded (17513/0), proving pollution rather than a complexity regression. Widen the bound to 8*N = 48 with a comment. Discrimination is preserved: the O(N!) regression this test guards against produces >= 720 readiness calls at N = 6, 15x over the widened bound, while schedule pollution is single-digit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughMana effects now model recipient and count-source targets separately, preserve both roles through parsing and migration, expose role-specific target slots, scope quantity resolution correctly, and validate legality per slot. Supporting scans, rendering, importer logic, and regression coverage were updated. ChangesMana role model and parsing
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Parser
participant TargetSelection
participant ManaResolution
participant CountSource
participant Recipient
Parser->>TargetSelection: expose recipient and count-source slots
TargetSelection->>ManaResolution: submit role-specific targets
ManaResolution->>CountSource: resolve quantity from count-source slot
CountSource-->>ManaResolution: return resolved quantity
ManaResolution->>Recipient: deposit mana for legal recipient
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
matthewevans
left a comment
There was a problem hiding this comment.
One multi-role retargeting path remains unsafe:
The new ManaTargetRole::Both branch can produce a flat target union, but forced retargeting replaces stack_ability.targets with a one-element vector and bypasses the per-slot validation used by interactive retargeting. A forced-target effect can therefore drop the other target or place the target in the wrong role. Please preserve the target vector and select/validate the intended role slot (or keep multi-slot mana out of forced retargeting until its semantics are represented), with an end-to-end forced-retarget regression.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
crates/engine/src/game/ability_rw.rs (1)
6440-6475: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate
mana_fixture_roles()test helper across two files. The same fixture matrix (five shipping mana-target-role shapes) is copy-pasted verbatim into both test modules; the shared root cause is having no single source of truth for this fixture set even though both suites test CR-behavior-preservation against it.
crates/engine/src/game/ability_rw.rs#L6440-L6475: keep (or move) the canonical definition here.crates/engine/src/game/mana_abilities.rs#L3770-L3806: drop the duplicate and import the shared helper instead — e.g. viacrate::game::test_fixtures, the modulemana_abilities.rsalready imports from forbrushland_colored_ability.🤖 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/ability_rw.rs` around lines 6440 - 6475, The five-shape mana fixture matrix is duplicated across two test modules; retain mana_fixture_roles() in crates/engine/src/game/ability_rw.rs:6440-6475 as the canonical definition, remove the duplicate helper from crates/engine/src/game/mana_abilities.rs:3770-3806, and import/reuse the shared helper there through the existing crate::game::test_fixtures path.crates/engine/src/parser/oracle_effect/mana.rs (1)
612-630: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a fixed-count test for the new colorless count-prefix arm.
This new arm is exercised only via the "that much {C}" (
EventContextAmount) path (parse_add_mana_target_player_that_much_colorless). There's no test driving a literal numeral (e.g., "Add three {C}.") through this exact branch to confirmscale_for_each_count(symbol_count, QuantityExpr::Fixed { .. })behaves correctly here — the parameter range of the new grammar arm (Fixed vs. Ref counts) isn't independently covered.🧪 Suggested additional test
#[test] fn parse_add_mana_fixed_count_colorless_no_subject() { let effect = try_parse_add_mana_effect("Add three {C}.") .expect("count-prefixed colorless mana must parse"); match effect { Effect::Mana { produced: ManaProduction::Colorless { count }, target, .. } => { assert_eq!(count, QuantityExpr::Fixed { value: 3 }); assert_eq!(target, None); } other => panic!("expected Effect::Mana, got {other:?}"), } }As per path instructions, "Build reusable card-class building blocks rather than one-card special cases... test the building block and its parameter range rather than a single card case."
🤖 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/parser/oracle_effect/mana.rs` around lines 612 - 630, Add a fixed-count parser test near the existing add-mana tests using try_parse_add_mana_effect with “Add three {C}.”. Assert it produces Effect::Mana with Colorless count QuantityExpr::Fixed value 3 and no target, covering the count-prefix branch and its Fixed parameter path.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.
Inline comments:
In `@crates/engine/src/game/ability_rw.rs`:
- Around line 3109-3123: Update the comment above the Effect::Mana arm to cite
CR 601.2c instead of CR 603.10a, while preserving the existing explanation that
the activated mana ability scans the target role’s declared filters.
In `@scripts/migrate-mana-target-roles.mjs`:
- Around line 151-155: Update FIELD_BY_ROLE and the envelope-writing paths to
handle Both explicitly: make envelopeField(role) reject Both (and any
unsupported role) before constructing an envelope, since a single legacy target
filter cannot reconstruct both filters. Use envelopeField(role) for the computed
keys at both writers so invalid roles fail loudly instead of producing an
"undefined" property.
---
Nitpick comments:
In `@crates/engine/src/game/ability_rw.rs`:
- Around line 6440-6475: The five-shape mana fixture matrix is duplicated across
two test modules; retain mana_fixture_roles() in
crates/engine/src/game/ability_rw.rs:6440-6475 as the canonical definition,
remove the duplicate helper from
crates/engine/src/game/mana_abilities.rs:3770-3806, and import/reuse the shared
helper there through the existing crate::game::test_fixtures path.
In `@crates/engine/src/parser/oracle_effect/mana.rs`:
- Around line 612-630: Add a fixed-count parser test near the existing add-mana
tests using try_parse_add_mana_effect with “Add three {C}.”. Assert it produces
Effect::Mana with Colorless count QuantityExpr::Fixed value 3 and no target,
covering the count-prefix branch and its Fixed parameter path.
🪄 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: 54010d4d-b01c-4738-b792-aa30ca2a78da
⛔ Files ignored due to path filters (4)
crates/engine/tests/fixtures/combo_infinite_pile_4p_offer.json.gzis excluded by!**/*.gzcrates/engine/tests/fixtures/combo_infinite_pile_4p_untapped_precast.json.gzis excluded by!**/*.gzcrates/engine/tests/fixtures/kilo_freed_relic_pentad_4p.json.gzis excluded by!**/*.gzcrates/engine/tests/fixtures/sprout_witherbloom_realistic_lands_4p.json.gzis excluded by!**/*.gz
📒 Files selected for processing (24)
crates/engine/src/ai_support/filter.rscrates/engine/src/game/ability_rw.rscrates/engine/src/game/ability_scan.rscrates/engine/src/game/ability_utils.rscrates/engine/src/game/coverage.rscrates/engine/src/game/effects/change_targets.rscrates/engine/src/game/effects/mana.rscrates/engine/src/game/engine.rscrates/engine/src/game/mana_abilities.rscrates/engine/src/parser/oracle_effect/imperative.rscrates/engine/src/parser/oracle_effect/mana.rscrates/engine/src/parser/oracle_effect/mod.rscrates/engine/src/parser/oracle_effect/sequence.rscrates/engine/src/parser/oracle_effect/tests.rscrates/engine/src/parser/oracle_ir/ast.rscrates/engine/src/parser/oracle_tests.rscrates/engine/src/parser/oracle_trigger_tests.rscrates/engine/src/types/ability.rscrates/engine/tests/fixtures/integration_cards.jsoncrates/engine/tests/integration/main.rscrates/engine/tests/integration/mana_role_fixture_migration.rscrates/engine/tests/integration/mana_target_recipient_and_count_source.rscrates/mtgish-import/src/convert/action.rsscripts/migrate-mana-target-roles.mjs
| // CR 603.10a (D5): `Effect::Mana`'s target is a ROLE (`ManaTargetRole`), | ||
| // not a bare `Option<TargetFilter>`, so it can no longer share the | ||
| // or-pattern below. It must still be scanned: several shipping mana | ||
| // cards carry exactly the frozen event-context tags this visitor exists | ||
| // to find — `TriggeringPlayer` (Bubbling Muck, High Tide, Mana Flare) | ||
| // and `ParentTargetController` (Fertile Ground, Utopia Sprawl, Wild | ||
| // Growth, Shimmerwilds Growth). Dropping Mana from the visitor would | ||
| // silently change `legacy_*` verdicts for all of them. Scan EVERY | ||
| // declared role filter (`declared_filters`, not `surfaced_filters`) — | ||
| // the tags live on context-ref filters, which `surfaced_filters` | ||
| // excludes by definition. | ||
| Effect::Mana { target, .. } => target | ||
| .as_ref() | ||
| .is_some_and(|role| role.declared_filters().any(|(_, f)| legacy_target_filter(f))), | ||
| Effect::LoseTheGame { target } | Effect::WinTheGame { target } => otf(target), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n "^603\.10" docs/MagicCompRules.txt
rg -n "^601\.2c" docs/MagicCompRules.txtRepository: phase-rs/phase
Length of output: 280
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Repo root: '; pwd
printf '\nTop-level files:\n'
git ls-files | sed -n '1,80p'
printf '\nLooking for CR docs and relevant code:\n'
fd -a 'MagicCompRules.txt|CLAUDE.md|ability_rw.rs|ai_support/filter.rs|ability_scan.rs|coverage.rs' .
printf '\nSearch for cited rules and mana-target annotation:\n'
rg -n '^603\.10a|^601\.2c|CR 603\.10a|CR 601\.2c|Effect::Mana' .Repository: phase-rs/phase
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'CR docs locations:\n'
fd -a 'MagicCompRules.txt|MagicCompRules.*' . || true
printf '\nability_rw slice:\n'
sed -n '3095,3135p' crates/engine/src/game/ability_rw.rs
printf '\nSibling CR citations around Effect::Mana:\n'
sed -n '1,120p' crates/phase-ai/src/policies/free_outlet_activation.rs
printf '\n---\n'
sed -n '1,120p' crates/phase-ai/src/search.rs
printf '\n---\n'
sed -n '1,120p' crates/engine/tests/integration/kutzils_flanker_mode_one_counter.rsRepository: phase-rs/phase
Length of output: 17871
Replace the CR 603.10a citation with CR 601.2c here; this Effect::Mana arm is scanning the target role on an activated mana ability.
🤖 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/ability_rw.rs` around lines 3109 - 3123, Update the
comment above the Effect::Mana arm to cite CR 601.2c instead of CR 603.10a,
while preserving the existing explanation that the activated mana ability scans
the target role’s declared filters.
Source: Path instructions
| /** Field name carrying the filter inside each role variant. */ | ||
| const FIELD_BY_ROLE = { | ||
| Recipient: "recipient", | ||
| CountSource: "count_source", | ||
| }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Both roles silently produce a malformed envelope instead of failing loudly. FIELD_BY_ROLE has no Both entry, yet the docs (lines 80, 292) tell maintainers to add Both { recipient, count_source } mappings. If a "Both" value ever reaches the writers at lines 210/271, FIELD_BY_ROLE["Both"] is undefined, so the computed key stringifies to "undefined" and you get { role: "Both", "undefined": <filter> } written into the fixture/state — silent corruption, directly contradicting the "fail loudly rather than guess" property. Additionally, a Both card cannot be reconstructed from a single legacy target filter at all (there's only one filter to wrap), so it must be rejected explicitly.
🛡️ Proposed guard
const FIELD_BY_ROLE = {
Recipient: "recipient",
CountSource: "count_source",
};
+
+// A `Both` role needs two independent filters; the legacy encoding carries only
+// one, so it is not migratable by this script and must never be inferred here.
+function envelopeField(role) {
+ const field = FIELD_BY_ROLE[role];
+ if (field === undefined) {
+ throw new Error(
+ `Cannot migrate role "${role}" from a single legacy filter ` +
+ `(only Recipient/CountSource are single-filter). Decide it by hand.`,
+ );
+ }
+ return field;
+}Then use [envelopeField(role)] at lines 210 and 271.
🤖 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 `@scripts/migrate-mana-target-roles.mjs` around lines 151 - 155, Update
FIELD_BY_ROLE and the envelope-writing paths to handle Both explicitly: make
envelopeField(role) reject Both (and any unsupported role) before constructing
an envelope, since a single legacy target filter cannot reconstruct both
filters. Use envelopeField(role) for the computed keys at both writers so
invalid roles fail loudly instead of producing an "undefined" property.
…ters-3b456b # Conflicts: # crates/engine/src/ai_support/filter.rs
There was a problem hiding this comment.
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/game/coverage.rs (1)
10743-10808: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy liftMove the new coverage tests to the integration test tree.
Lines [10743] and [12389] add engine tests inside the private
mod testsofcrates/engine/src/game/coverage.rs. Move these cases undercrates/engine/tests/integration/, wired throughcrates/engine/tests/integration/main.rs, and exercise the corresponding public coverage entry points there.As per path instructions, new engine tests must go under
crates/engine/tests/integration/as modules wired fromcrates/engine/tests/integration/main.rs(avoid new top-level test binaries).Also applies to: 12389-12440
🤖 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/coverage.rs` around lines 10743 - 10808, Move the new tests, including mana_role_parse_details_names_each_role_key and the corresponding test near the later range, out of the private tests module in coverage.rs into modules under the integration test tree. Wire those modules through integration/main.rs, adapt them to use the public coverage entry points and accessible APIs, and remove the in-file test copies without creating additional top-level test binaries.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.
Outside diff comments:
In `@crates/engine/src/game/coverage.rs`:
- Around line 10743-10808: Move the new tests, including
mana_role_parse_details_names_each_role_key and the corresponding test near the
later range, out of the private tests module in coverage.rs into modules under
the integration test tree. Wire those modules through integration/main.rs, adapt
them to use the public coverage entry points and accessible APIs, and remove the
in-file test copies without creating additional top-level test binaries.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3bc6eefe-b5a5-47e4-b94f-febc3b2997e6
📒 Files selected for processing (15)
crates/engine/src/ai_support/filter.rscrates/engine/src/game/ability_rw.rscrates/engine/src/game/ability_scan.rscrates/engine/src/game/ability_utils.rscrates/engine/src/game/coverage.rscrates/engine/src/game/engine.rscrates/engine/src/game/mana_abilities.rscrates/engine/src/parser/oracle_effect/imperative.rscrates/engine/src/parser/oracle_effect/mod.rscrates/engine/src/parser/oracle_effect/tests.rscrates/engine/src/parser/oracle_ir/ast.rscrates/engine/src/parser/oracle_tests.rscrates/engine/src/parser/oracle_trigger_tests.rscrates/engine/src/types/ability.rscrates/engine/tests/integration/main.rs
🚧 Files skipped from review as they are similar to previous changes (14)
- crates/engine/tests/integration/main.rs
- crates/engine/src/parser/oracle_effect/imperative.rs
- crates/engine/src/parser/oracle_trigger_tests.rs
- crates/engine/src/parser/oracle_effect/mod.rs
- crates/engine/src/parser/oracle_ir/ast.rs
- crates/engine/src/parser/oracle_tests.rs
- crates/engine/src/game/engine.rs
- crates/engine/src/parser/oracle_effect/tests.rs
- crates/engine/src/game/mana_abilities.rs
- crates/engine/src/types/ability.rs
- crates/engine/src/game/ability_scan.rs
- crates/engine/src/ai_support/filter.rs
- crates/engine/src/game/ability_rw.rs
- crates/engine/src/game/ability_utils.rs
matthewevans
left a comment
There was a problem hiding this comment.
Blocked — the current head still drops multi-role targets during forced retargeting and can corrupt a future migration.
🔴 Blocker
crates/engine/src/game/effects/change_targets.rs:71-82 takes the forced-retarget path outside engine.rs::apply_retarget: after checking a flat union, it overwrites stack_ability.targets with vec![new_target]. A ManaTargetRole::Both spell has two independently declared target slots; this drops the other slot and never applies the positional legality check added for interactive retargeting. CR 115.7a says each target may change only to another legal target and otherwise the original target remains unchanged; CR 115.7b permits changing only one target, not deleting the others (verified in docs/MagicCompRules.txt:863-869). The existing forced-retarget test covers a single-target destroy spell, so it cannot exercise this new multi-slot production path.
Please represent which target slot a forced effect changes, preserve all other targets, and validate the replacement against that slot's filter. Add an end-to-end forced-retarget regression for a Both mana ability that proves both the untouched target and the role-specific replacement survive; it should fail with this vec![new_target] assignment.
🟡 Non-blocking
scripts/migrate-mana-target-roles.mjs:151-155,210,271 documents Both as a supported mapping but has field names only for Recipient and CountSource. If a maintainer follows that documented path, the computed key is undefined, producing a malformed { role: "Both", undefined: ... } envelope rather than failing loudly. A legacy single filter cannot reconstruct both roles, so reject Both explicitly (and require a dedicated migration) before either writer constructs an envelope.
✅ Clean
The role model correctly separates recipient and count-source provenance, and the current parse-diff artifact is present for this head: 58 cards / 21 signatures, updated after the merge commit (https://github.com/phase-rs/phase/pull/6056#issuecomment-4997648480).
Recommendation: request changes. Resolve the forced-retarget behavior with a discriminating runtime test; the migration guard can ride with that repair.
matthewevans
left a comment
There was a problem hiding this comment.
Blocked — forced retargeting still destroys one target slot for a multi-role mana ability.
🔴 Blocker
crates/engine/src/game/effects/change_targets.rs:71-82 validates the forced replacement against a flat union and then assigns stack_ability.targets = vec![new_target]. A mana ability with independent recipient and count-source targets therefore loses its other declared target; this path does not call the positional retarget_slot_violation guard added only to interactive apply_retarget in crates/engine/src/game/engine.rs:8456-8483. CR 115.7a requires each target to be changed only to another legal target, not removal or reassignment of a separate target slot.
Use the role-slot authority to identify which slot the forced filter can replace, preserve the full target vector, validate that slot against its own filter, and add an end-to-end forced-retarget regression with distinct legal recipient and count-source players.
Recommendation: request changes; retain the two target roles through the forced-retarget assignment before re-review.
Implements the Transformers DFC Jetfire, Ingenious Scientist // Jetfire, Air Guardian (set BOT).
Summary
End-to-end tracing showed almost every mechanic already existed — More Than Meets the Eye, Living metal, Flying,
convert→transform, adapt, the "can't be spent to cast nonartifact spells" restriction (SpellTypeOrAbilityActivation{Artifact, Any}) and its cross-sentence attachment, and the counter-removal cost →chosen_x→ "that much" wiring. Four surgical changes complete the two activated abilities:oracle_effect/mana.rs):strip_mana_subject_prefixrecognizes "target player adds" →TargetFilter::Player, a genuine chosen recipient distinct from the "active/that player" anaphors (CR 115.1 + CR 106.4).effects/mana.rs):mana_effect_recipientnow deposits mana into the targeted player for aPlayerrecipient, provenance-gated by a newmana_count_reads_targetspredicate — the identicalTargetFilter::Playeris also emitted as a count source ("Add {U} for each card in target player's hand"), where the recipient must stay the controller. No Jeska's Will regression.oracle_effect/mana.rs): "that much {C}" now parses as a count-prefixed colorless production (previously fell toUnimplemented).sequence.rs):"adapt"added to the clause-start verb table so "Convert Jetfire, then adapt 3" chains the adapt sub-ability.No new enum variants.
Card text
{4}{U}Legendary Artifact Creature — Robot 3/4: More Than Meets the Eye {3}{U}; Flying; Remove one or more +1/+1 counters from among artifacts you control: Target player adds that much {C}. This mana can't be spent to cast nonartifact spells. Convert Jetfire.Tests
6 new tests: both faces parse end-to-end (front: chosen-X counter cost + targeted restricted mana + Convert sub-ability; back: convert→adapt chain), the "target player adds that much {C}" clause, targeted-player deposit, the multi-authority recipient-vs-count-source gate (incl. Jeska's Will non-regression), and spend-restriction deposit.
Verification
cargo fmtclean;cargo clippy -p engine --lib --testsclean (-D warnings)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests