Skip to content

Fix Diluvian Primordial - #6794

Open
invalidCards wants to merge 2 commits into
phase-rs:mainfrom
invalidCards:card/diluvian-primordial
Open

Fix Diluvian Primordial#6794
invalidCards wants to merge 2 commits into
phase-rs:mainfrom
invalidCards:card/diluvian-primordial

Conversation

@invalidCards

@invalidCards invalidCards commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #6754.

Diluvian Primordial previously could not lower its per-opponent, opponent-graveyard cast clause: the fanout path only accepted battlefield-object targets, so the paired opponent/card selections were never represented. This PR adds the narrowly scoped graveyard exception, preserves each selected card as an exact free-cast pool, and routes the card's exile rider through the existing per-cast replacement path.

The fixed pool prevents a later offer from substituting a different card from an opponent's graveyard after a chosen target becomes illegal. The rider is installed once after a selected spell is cast, avoiding duplicate replacement choices while retaining established Invoke Calamity behavior.

Files changed

  • crates/engine/src/game/ability_utils.rs
  • crates/engine/src/game/effects/cast_from_zone.rs
  • crates/engine/src/game/effects/free_cast_from_zones.rs
  • crates/engine/src/parser/oracle_effect/assembly.rs
  • crates/engine/src/parser/oracle_effect/lower.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/tests/integration/diluvian_primordial_6754.rs
  • crates/engine/tests/integration/main.rs
  • docs/parser-misparse-backlog.md

Track

Developer

LLM

Model: gpt-5.6
Tier: Frontier
Thinking: high

Implementation method (required)

Method: /engine-implementer

CR references

CR 115.1a, CR 108.3, CR 601.2a–c, CR 608.2g, CR 614.1a.

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.

  • cargo fmt --all — clean (EXIT_STATUS=0)

  • cargo clippy-strict — clean (EXIT_STATUS=0)

  • cargo test -p phase-engine — clean; 4,194 integration tests passed (EXIT_STATUS=0)

  • ./scripts/gen-card-data.sh — clean (EXIT_STATUS=0)

  • cargo coverage — clean (EXIT_STATUS=0)

  • cargo semantic-audit — clean (EXIT_STATUS=0)

  • ./scripts/check-parser-combinators.sh — Gate G PASS; Gate A PASS

Gate A

Gate A PASS head=145698640c8ab5cea95f9307c012fa91e0e3f348 base=d947f97469a5de1c332701f2062e1852d81ca011

Anchored on

  • crates/engine/tests/integration/riptide_gearhulk_5994.rs:1 — established optional per-opponent ETB fanout coverage.
  • crates/engine/src/game/casting_costs.rs:9314 — established FreeCastOfferRemaining per-cast exile-rider authority.

Final review-impl

Final review-impl PASS head=145698640c8ab5cea95f9307c012fa91e0e3f348

Claimed parse impact

Diluvian Primordial.

Scope Expansion

The exact-pool support is generalized within the existing FreeCastFromZones path so it can carry paired target IDs without changing its empty-pool behavior, which preserves Invoke Calamity's established controller-graveyard scan.

Validation Failures

None.

CI Failures

None.

Summary by CodeRabbit

  • New Features

    • Added parsing and resolution for per-opponent graveyard free-casts (without paying mana costs).
    • Preserves each opponent’s exact paired graveyard choices throughout the free-cast window.
    • Supports spell destination replacement into exile for these cast effects.
  • Bug Fixes

    • Prevents invalid selections from being substituted during re-offers.
    • Correctly handles cases where an opponent has no legal graveyard targets.
  • Tests

    • Expanded parser, regression, and integration coverage for paired graveyard/exile behavior.
    • Updated tests to reflect the new graveyard-replacement configuration.

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

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds parsing and execution support for per-opponent graveyard free casts, pairs each opponent with a typed graveyard card, preserves selected cards in a fixed free-cast pool, and carries structured graveyard replacement destinations through resolution.

Changes

Per-opponent graveyard casting

Layer / File(s) Summary
Oracle parsing and rider lowering
crates/engine/src/parser/oracle_effect/*
Parses per-opponent graveyard casts and “cast this way” replacement riders, validates explicit typed graveyard filters, and lowers exile riders into the appropriate effect representation.
Structured replacement contract
crates/engine/src/types/*, crates/engine/src/game/*, crates/engine/src/parser/oracle_tests.rs
Replaces the exile boolean with an optional graveyard_replacement destination and propagates it through serialization, casting, offers, visibility, coverage, and fixtures.
Paired targeting and fixed-pool resolution
crates/engine/src/game/ability_utils.rs, crates/engine/src/game/effects/{cast_from_zone,free_cast_from_zones}.rs
Pairs opponents with correctly owned graveyard cards, opens a fixed member pool, and removes unavailable targets without substituting other cards.
End-to-end validation
crates/engine/tests/integration/*, docs/parser-misparse-backlog.md
Covers selected pairs, removed cards, targetless opponents, replacement behavior, and integration-module registration.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OracleParser
  participant CastFromZone
  participant TargetSlots
  participant FreeCastFromZones
  OracleParser->>CastFromZone: parse per-opponent graveyard cast
  CastFromZone->>TargetSlots: build paired opponent/card targets
  TargetSlots-->>CastFromZone: return revalidated target IDs
  CastFromZone->>FreeCastFromZones: open fixed member_pool
  FreeCastFromZones-->>CastFromZone: cast selected cards with graveyard replacement
Loading

Possibly related PRs

  • phase-rs/phase#6320: Both changes extend FreeCastFromZones member-pool scoping and candidate selection.
  • phase-rs/phase#6565: Both changes modify per-opponent fanout lowering and target-filter handling.

Suggested labels: bug, enhancement

Suggested reviewers: matthewevans, andriypolanski

🚥 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 is concise and clearly points to the main change: fixing Diluvian Primordial.
Linked Issues check ✅ Passed The changes implement the reported ETB fix: per-opponent graveyard targeting, free casting, and exile replacement on cast.
Out of Scope Changes check ✅ Passed The broader parser, engine, and test updates appear to support the Diluvian Primordial fix and the shared free-cast path.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.45.0)
crates/engine/src/parser/oracle_effect/mod.rs

ast-grep timed out on this file


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: 2

🤖 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/effects/cast_from_zone.rs`:
- Around line 610-620: Replace the locally re-derived is_per_opponent_fanout
matches expression with the existing is_per_opponent_target_fanout predicate.
Reuse that canonical predicate for the router while leaving the surrounding
distribution and slot-building logic unchanged.

In `@crates/engine/tests/integration/diluvian_primordial_6754.rs`:
- Around line 165-170: Strengthen the assertions in the relevant Diluvian
Primordial integration tests, including
diluvian_primordial_skips_targetless_opponent_and_casts_other_selected_spell, to
verify the free-cast spell resolved by asserting P0’s hand or library count
changed as expected from its “Draw a card.” effect. Keep the existing zone
assertions, but ensure the positive effect distinguishes resolution from being
exiled without resolving.
🪄 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: 2b7d77bf-af65-4d0d-b84b-12bd414b6a74

📥 Commits

Reviewing files that changed from the base of the PR and between 240cfc8 and 1456986.

📒 Files selected for processing (10)
  • crates/engine/src/game/ability_utils.rs
  • crates/engine/src/game/effects/cast_from_zone.rs
  • crates/engine/src/game/effects/free_cast_from_zones.rs
  • crates/engine/src/parser/oracle_effect/assembly.rs
  • crates/engine/src/parser/oracle_effect/lower.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/tests/integration/diluvian_primordial_6754.rs
  • crates/engine/tests/integration/main.rs
  • docs/parser-misparse-backlog.md
💤 Files with no reviewable changes (1)
  • docs/parser-misparse-backlog.md

Comment thread crates/engine/src/game/effects/cast_from_zone.rs Outdated
Comment thread crates/engine/tests/integration/diluvian_primordial_6754.rs

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

Reviewing current head 145698640c8ab5cea95f9307c012fa91e0e3f348.

[HIGH] Free-cast routing leaves the sequential graveyard-exile rider attached to the cloned ability. Evidence: crates/engine/src/game/effects/cast_from_zone.rs:632-647 clones the rider-bearing ability and replaces only effect before opening the FreeCastWindow; crates/engine/src/game/effects/mod.rs:10165-10176 then stashes the sequential ParentTarget rider with the inherited targets; crates/engine/src/game/effects/change_zone.rs:465-467 resolves those targets. Why it matters: after a selected spell is free-cast, the continuation can resolve ChangeZone against that selected spell while it is on the stack, exiling it before it resolves. The present integration assertion at crates/engine/tests/integration/diluvian_primordial_6754.rs:167 only observes the final zone, so it cannot distinguish this failure from the required stack-to-graveyard exile replacement. Suggested fix: consume/suppress the parsed rider when routing to the free-cast window, and add a production-pipeline assertion that the selected spell's effect resolves before the stack-to-graveyard replacement puts it in exile.

[MED] The router duplicates only the max == PlayerCount(Opponent) portion of the canonical fanout predicate. Evidence: crates/engine/src/game/effects/cast_from_zone.rs:610-620 omits the canonical stack-timing and ControllerRef::TargetPlayer gates in crates/engine/src/game/ability_utils.rs:4763-4785. Why it matters: the routing condition can diverge from the existing target-slot/distribution authority and apply the free-cast route to a superficially similar, but semantically different, target shape. Suggested fix: reuse or move the canonical predicate so the router and target construction share one authority.

[MED] Required parser-surface evidence is absent for this engine/parser PR. Evidence: no current-head sticky comment containing <!-- coverage-parse-diff --> is present; the only current CodeRabbit review explicitly covers 240cfc8e701c7b55f676fe7ad9333a0b142aa1ae..145698640c8ab5cea95f9307c012fa91e0e3f348. Why it matters: without the card-level parse diff, this review cannot verify that the new parser route has no unintended supported/unsupported or AST-shape blast radius. Suggested fix: provide a current-head coverage parse-diff artifact and reconcile every changed card with this PR's intended scope.

The two current CodeRabbit threads are confirmed and folded into the findings above: canonical predicate reuse (thread) and the non-discriminating final-zone-only test (thread).

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 2 card(s), 6 signature(s) (baseline: main daf5c1df7b2f)

🟡 Modified fields (6 signatures)

  • 1 card · 🔄 ability/CastFromZone · changed field target: any targettarget player controls in graveyard target player controls instant or target player controls in graveyard target player…
    • Affected (first 3): Diluvian Primordial
  • 1 card · 🔄 ability/CastFromZone · changed field targeting: optional (up to)
    • Affected (first 3): Diluvian Primordial
  • 1 card · 🔄 ability/CastFromZone · changed field targets: 0-# of each opponent
    • Affected (first 3): Diluvian Primordial
  • 1 card · 🔄 trigger/ChangesZone · changed field valid target: player
    • Affected (first 3): Diluvian Primordial
  • 1 card · 🔄 ability/FreeCastFromZones · changed field exile instead of graveyard: yes
    • Affected (first 3): Invoke Calamity
  • 1 card · 🔄 ability/FreeCastFromZones · changed field graveyard replacement: Exile
    • Affected (first 3): Invoke Calamity

1 card(s) had Oracle-text changes (errata/reprint) — excluded as non-parser.

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

Review addendum for current head 145698640c8ab5cea95f9307c012fa91e0e3f348.

[MED] The newly published current-head parse-diff exposes an unexplained 34-card AST blast radius. The PR claims parse impact is only Diluvian Primordial, but the <!-- coverage-parse-diff --> artifact reports 34 cards with ability/ChangeZone.from changed from to stack (including Bösium Strip and Chandra, Acolyte of Flame), alongside the five Diluvian-specific signatures. This is explained by the shared lowering change in crates/engine/src/parser/oracle_effect/lower.rs:1060-1074: every SpellStackToGraveyardReplacement::Exile rider now lowers to ChangeZone { origin: Some(Zone::Stack), destination: Zone::Exile, target: ParentTarget, ... }. The PR provides no card-class review, behavior trace, or test coverage for those other 33 cards, so the claimed narrow scope is contradicted by the artifact. Please either (1) narrow/revert the unrelated shared AST change from this PR, or (2) explicitly justify and review the entire 34-card class, including representative runtime coverage and reconciled parse-impact documentation.

The existing current-head blockers remain unresolved:

  • [HIGH] The free-cast routing clones the rider-bearing ability, replacing only effect; the sequential ParentTarget ChangeZone rider can therefore resolve against the selected spell while it is on the stack and exile it before it resolves. Suppress/consume that rider for the free-cast window and add a production-pipeline assertion that the selected spell actually resolves before the stack-to-graveyard exile replacement.
  • [MED] cast_from_zone.rs locally re-derives only the opponent-count portion of the fanout condition and omits the canonical predicate's stack-timing and ControllerRef::TargetPlayer gates. Reuse is_per_opponent_target_fanout (or relocate it to a shared authority) so target construction and routing cannot diverge.

CodeRabbit's current-head findings confirm both retained blockers: canonical predicate reuse and the non-discriminating final-zone-only integration assertion.

@invalidCards

Copy link
Copy Markdown
Contributor Author

Follow-up fixes are now in 9d390ef8805dc65459ce51b32d0a53652b6ce42b.

  • The direct CastFromZone router now uses the canonical per-opponent fanout predicate, including its stack-timing and TargetPlayer safeguards.
  • Free-cast graveyard replacement is now a typed destination carried through the window and installed once after a selected spell is cast. The synthetic window consumes its copied sequential rider, so it cannot exile a spell from the stack before resolution.
  • The Diluvian integration cases now seed a P0 library card and assert the selected Draw a card. spell actually resolves before its replacement sends it to exile.
  • The parser keeps generic if that spell would be put into a graveyard, exile it instead lowering on its prior origin: None path. This removes the unintended 34-card ChangeZone.from: stack parse-diff expansion while retaining Diluvian's window metadata.

The full local batch is green: fmt, clippy-strict, cargo test -p phase-engine (4,203 integration tests), card-data generation, coverage, and semantic audit. Final independent review and Gate A both passed on this commit.

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

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/effects/cast_from_zone.rs (1)

612-635: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Unconditionally clearing window.sub_ability can silently drop non-rider chain effects.

window.sub_ability = None fires for every per-opponent-fanout / during-resolution / without-paying CastFromZone, regardless of whether graveyard_destination (i.e. cast_from_zone_graveyard_destination(ability)) actually found a rider to translate. If a future (or currently-unrecognized) producer of this shape carries a sub_ability that isn't the graveyard-exile rider, this silently discards it with no diagnostic — the same class of bug this change fixes for the known rider, reintroduced for the unknown case.

Scope the clear to only when a rider was actually consumed:

🐛 Proposed fix
         window.targets = target_ids.drain(..).map(TargetRef::Object).collect();
-        // The rider has been translated into the window's per-cast metadata;
-        // retaining it would run a second destination move after the window.
-        window.sub_ability = None;
+        // The rider has been translated into the window's per-cast metadata;
+        // retaining it would run a second destination move after the window.
+        // Only clear it when a rider was actually consumed — an unrecognized
+        // sub_ability shape must not be silently dropped.
+        if graveyard_destination.is_some() {
+            window.sub_ability = None;
+        }
#!/bin/bash
# Look for any other per-opponent fanout / free-cast producer whose sub_ability
# might carry something other than the graveyard-destination rider.
rg -n -C5 'graveyard_destination_rider' crates/engine/src
🤖 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/cast_from_zone.rs` around lines 612 - 635,
Only clear window.sub_ability in the per-opponent fanout path when
graveyard_destination confirms that
cast_from_zone_graveyard_destination(ability) found and consumed the
corresponding graveyard-exile rider. Preserve any unrelated or unrecognized
sub_ability when no rider was translated, while keeping the existing rider
translation behavior unchanged.
🤖 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/effects/cast_from_zone.rs`:
- Around line 612-635: Only clear window.sub_ability in the per-opponent fanout
path when graveyard_destination confirms that
cast_from_zone_graveyard_destination(ability) found and consumed the
corresponding graveyard-exile rider. Preserve any unrelated or unrecognized
sub_ability when no rider was translated, while keeping the existing rider
translation behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5bf1f7d3-278b-4365-acb9-5961852d157e

📥 Commits

Reviewing files that changed from the base of the PR and between 1456986 and 9d390ef.

📒 Files selected for processing (18)
  • crates/engine/src/game/ability_scan.rs
  • crates/engine/src/game/ability_utils.rs
  • crates/engine/src/game/casting_costs.rs
  • crates/engine/src/game/coverage.rs
  • crates/engine/src/game/effects/cast_from_zone.rs
  • crates/engine/src/game/effects/exile_from_top_until.rs
  • crates/engine/src/game/effects/free_cast_from_zones.rs
  • crates/engine/src/game/engine_resolution_choices.rs
  • crates/engine/src/game/visibility.rs
  • crates/engine/src/parser/oracle_effect/assembly.rs
  • crates/engine/src/parser/oracle_effect/lower.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/tests.rs
  • crates/engine/src/parser/oracle_tests.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/src/types/game_state.rs
  • crates/engine/tests/integration/diluvian_primordial_6754.rs
  • crates/engine/tests/integration/invoke_calamity_free_cast.rs

@matthewevans matthewevans self-assigned this Jul 30, 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.

Reviewing current head 9d390ef8805dc65459ce51b32d0a53652b6ce42b.

[HIGH] The per-opponent free-cast route unconditionally discards sequential continuations that are not the recognized graveyard-destination rider. crates/engine/src/game/effects/cast_from_zone.rs:610-633 enters the structural per-opponent, during-resolution free-cast branch and translates only cast_from_zone_graveyard_destination(ability) into FreeCastFromZones.graveyard_replacement. That extractor (:1039-1046) recognizes only the graveyard-destination rider, but window.sub_ability = None at :631 clears the entire sequential chain regardless. An unrelated current or future sequential continuation is therefore silently dropped instead of resolving after the free-cast window.

Please consume only the structurally recognized rider while preserving/executing any remaining continuation, and add a focused production-pipeline regression that places a non-rider sequential continuation after this fanout so the test fails if the continuation is cleared.

@matthewevans matthewevans removed their assignment Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Diluvian Primordial — [[Diluvian Primordial]] triggers its ETB but does nothing.

2 participants