Skip to content

Partial: Avenge - #6994

Open
JacobWoodson wants to merge 1 commit into
phase-rs:mainfrom
JacobWoodson:card/avenge
Open

Partial: Avenge#6994
JacobWoodson wants to merge 1 commit into
phase-rs:mainfrom
JacobWoodson:card/avenge

Conversation

@JacobWoodson

@JacobWoodson JacobWoodson commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes a parse-fidelity defect on Avenge.

Issue: Cost-reduction condition "if a player attacked you during their last turn" is dropped (ModifyCost condition=null), so the {2} reduction applies unconditionally instead of only when the attack condition holds.

Files changed

  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\types\ability.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\types\game_state.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\game\turns.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\game\layers.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\game\quantity.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\game\ability_rw.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\game\ability_scan.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\game\coverage.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\parser\oracle_nom\condition.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\parser\oracle_trigger.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\parser\oracle_effect\conditions.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\parser\oracle_condition.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\parser\oracle_static\tests.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\game\casting_tests.rs

CR references

  • CR 508.6
  • CR 514.2
  • CR 514.3
  • CR 109.5

Track

Developer

LLM

Model: claude-opus-4-8
Thinking: high

Tier: Frontier

Verification

  • cargo fmt --all — pass (clean)
  • ./scripts/check-parser-combinators.sh — pass (Gate G PASS + Gate A PASS; Family D NOT skipped — ran via working msys64 python3 3.9.7, detector self-test suite 10/10 ok; WindowsApps python3 stub bypassed by removing it from PATH)
  • cargo clippy-strict — pass (exit 0, no warnings)
  • cargo test -p phase-engine — fail (18475 passed, 1 failed: stage2_injector_tests::the_cr_603_5_prompt_census... — Windows-only path-separator artifact, unrelated to Avenge; also cleared a 97GB target/debug/incremental dir that had filled the disk before this run)
  • cargo export-cards data --stats --sidecar-dir client/public && cp client/public/card-data.json data/card-data.json — pass (card-data regenerated fresh against this branch; sidecar copied to data/, 98176031 bytes)
  • cargo coverage — fail-result (command exit 0, but Avenge supported:false gap_count:1 — Swallow:Condition_If on the cost-reduction clause)
  • cargo semantic-audit — pass (command exit 0; 32700 supported audited, 295 flagged; Avenge has 0 findings — it is not in the supported-audit set)

Scope Expansion

Scope grew by 3 files beyond the plan's list: the plan missed three exhaustive StaticCondition mappers (oracle_trigger.rs, oracle_effect/conditions.rs, oracle_condition.rs) that require a compile-mandatory "-> None" arm.

Validation Failures

See review/cross-check notes.

CI Failures

  • Avenge is NOT supported: cargo coverage reports supported=false, gap_count=1. Gap = Swallow:Condition_If on 'This spell costs {2} less to cast if a player attacked you during their last turn.' The DestroyAll + GainLife(filtered tracked set: creatures destroyed) body parses correctly, but the conditional cost reduction is swallowed: the ModifyCost static ability captures the {2} reduction with condition=null — the 'if a player attacked you during their last turn' condition is dropped. This is a genuine parse gap surfaced by swallow_check.rs (a file this branch modifies), confirmed against freshly regenerated card-data, not stale data. To reach gap:0 the swallowed StaticCondition must be parsed and attached to the ModifyCost static ability (a feature implementation, out of scope for command-level retry).
  • cargo test -p phase-engine: 1 unrelated test failed — game::engine::stage2_injector_tests::the_cr_603_5_prompt_census_is_pinned_so_a_sixth_producer_is_a_counted_event (engine.rs:15185). Windows-only path-separator artifact: file!()/rel.display() yields backslash paths (game\effects\mod.rs:5999) while the test hardcodes forward-slash paths (game/effects/mod.rs:5999). Census content is identical (same files, same lines 5999/6076/9051/452/11427), the count assertions (5 producers, 7 readers, 25 test-lines) all passed, and none of this branch's modified files (triggers.rs, oracle_trigger.rs, swallow_check.rs, integration tests) touch the census sites. Deterministic (retry futile, not a flake); the test's own comment notes it passes on Linux CI. Not caused by this branch and unrelated to Avenge; left unedited (sensitive unrelated census pin, multi-agent-safety).

Summary by CodeRabbit

  • New Features

    • Added support for “a player” or “an opponent attacked you during their last turn” conditions.
    • Avenge-style cost reductions now correctly activate when the condition is met.
    • Multiplayer games now evaluate attacks across eligible opponents, with accurate turn-by-turn tracking.
    • Added clear condition text and support for recognized card rules.
  • Bug Fixes

    • Corrected attack-history updates when turns end, including clearing records when no attacks occur.
  • Tests

    • Added coverage for parsing, cost reductions, multiplayer behavior, and attack-history persistence.

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

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds AnyPlayerAttackedYouLastTurn, stores attack targets at turn cleanup, parses supported Avenge phrases, evaluates the condition across non-eliminated opponents, and validates conditional cost reduction and multiplayer behavior.

Changes

Avenge attack condition

Layer / File(s) Summary
Attack history state
crates/engine/src/types/ability.rs, crates/engine/src/types/game_state.rs, crates/engine/src/game/turns.rs
Adds the condition variant, stores each player's last-turn defenders, exposes directional queries, and updates records during cleanup.
Condition parsing and lowering
crates/engine/src/parser/oracle_nom/condition.rs, crates/engine/src/parser/oracle_static/tests.rs, crates/engine/src/parser/oracle_condition.rs, crates/engine/src/parser/oracle_effect/conditions.rs, crates/engine/src/parser/oracle_trigger.rs
Parses “a player” and “an opponent” last-turn attack phrases. Unsupported condition conversions return None.
Condition evaluation and classification
crates/engine/src/game/layers.rs, crates/engine/src/game/coverage.rs, crates/engine/src/game/quantity.rs, crates/engine/src/game/ability_rw.rs
Evaluates whether any eligible opponent attacked the controller and registers formatting, profiling, and resolver classifications.
Cost-reduction and multiplayer validation
crates/engine/src/game/casting_tests.rs
Tests Avenge cost gating, attack direction, and existential evaluation in a three-player game.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Player
  participant execute_cleanup
  participant GameState
  participant ModifyCost
  Player->>execute_cleanup: complete turn with attack targets
  execute_cleanup->>GameState: snapshot attacked defenders
  ModifyCost->>GameState: query last-turn attack condition
  GameState-->>ModifyCost: return whether an opponent attacked controller
  ModifyCost-->>Player: apply conditional cost reduction
Loading

Possibly related PRs

  • phase-rs/phase#5868: Extends StaticCondition handling across shared classifier, parser, resolver, and formatting match points.
  • phase-rs/phase#6743: Shares cost-reduction mechanics in the ability and casting code.

Suggested labels: bug

Suggested reviewers: matthewevans

🚥 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 identifies the Avenge-related change and accurately signals that the implementation is partial.
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

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/game/casting_tests.rs

ast-grep timed out on this file


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

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Generated for head 7972b8d793ba5f85611d3a8fbd33cdc2c55b59b1.

Parse changes introduced by this PR · 1 card(s), 1 signature(s) (baseline: main 4f524c6014f3)

🟡 Modified fields (1 signature)

  • 1 card · 🔄 static/ReduceCost · changed field conditional: a player attacked you during their last turn
    • Affected (first 3): Avenge

@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/casting_tests.rs`:
- Around line 2886-2901: Add a test in the existing AnyPlayerAttackedYouLastTurn
cases near evaluate_condition_for_test where P2 records attacking you, then
eliminate or remove P2 from the game using the established GameState API, and
assert the condition evaluates false. Ensure the stale
attacked_defenders_last_turn entry remains present so the test verifies
eliminated attackers are ignored.

In `@crates/engine/src/game/layers.rs`:
- Around line 1617-1621: Update the
StaticCondition::AnyPlayerAttackedYouLastTurn evaluation to stop filtering
attackers with !p.is_eliminated, allowing departed opponents’ recorded attacks
to remain valid. Enforce expiry when the departed player’s skipped next-turn
boundary is reached in the existing last-turn attack record lifecycle,
preserving attacks until that boundary.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 399d8fd8-31f7-4349-82dc-b5ed15b5f341

📥 Commits

Reviewing files that changed from the base of the PR and between 4f524c6 and 7972b8d.

📒 Files selected for processing (14)
  • crates/engine/src/game/ability_rw.rs
  • crates/engine/src/game/ability_scan.rs
  • crates/engine/src/game/casting_tests.rs
  • crates/engine/src/game/coverage.rs
  • crates/engine/src/game/layers.rs
  • crates/engine/src/game/quantity.rs
  • crates/engine/src/game/turns.rs
  • crates/engine/src/parser/oracle_condition.rs
  • crates/engine/src/parser/oracle_effect/conditions.rs
  • crates/engine/src/parser/oracle_nom/condition.rs
  • crates/engine/src/parser/oracle_static/tests.rs
  • crates/engine/src/parser/oracle_trigger.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/src/types/game_state.rs

Comment on lines +2886 to +2901
// Only P2 attacked you (P1 attacked no one) ⇒ true (existential over players).
let mut state = GameState::new(FormatConfig::standard(), 3, 7);
state
.attacked_defenders_last_turn
.insert(PlayerId(2), [you].into_iter().collect());
assert!(evaluate_condition_for_test(&state, &cond, you, src));

// Opponents attacked each other but not you ⇒ false (the defender must be you).
let mut state = GameState::new(FormatConfig::standard(), 3, 7);
state
.attacked_defenders_last_turn
.insert(PlayerId(1), [PlayerId(2)].into_iter().collect());
state
.attacked_defenders_last_turn
.insert(PlayerId(2), [PlayerId(1)].into_iter().collect());
assert!(!evaluate_condition_for_test(&state, &cond, you, src));

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Cover an eliminated attacker.

Add a case where P2 attacked P0, then P2 leaves the game, and assert that AnyPlayerAttackedYouLastTurn is false. These cases only cover active opponents. A stale attack-history entry must not grant Avenge's reduction after its attacker leaves the multiplayer game. (media.wizards.com)

As per path instructions, check edge cases where eliminated players remain referenced.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/game/casting_tests.rs` around lines 2886 - 2901, Add a test
in the existing AnyPlayerAttackedYouLastTurn cases near
evaluate_condition_for_test where P2 records attacking you, then eliminate or
remove P2 from the game using the established GameState API, and assert the
condition evaluates false. Ensure the stale attacked_defenders_last_turn entry
remains present so the test verifies eliminated attackers are ignored.

Source: Path instructions

Comment on lines +1617 to +1621
StaticCondition::AnyPlayerAttackedYouLastTurn => state.players.iter().any(|p| {
!p.is_eliminated
&& p.id != controller
&& state.player_attacked_player_last_turn(p.id, controller)
}),

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve a departed opponent’s last-turn attack until the required expiry boundary.

Line 1618 excludes an attacker as soon as that player leaves the game. CR 800.4i requires effects that inspect actions during a player’s last turn to retain that information until that player’s next turn would have begun. An opponent can attack the controller, leave the game, and still satisfy Avenge before that boundary. Expire the stored record at the departed player’s skipped turn boundary instead of filtering the player here. (media.wizards.com)

As per path instructions, “Implement MTG behavior according to the Comprehensive Rules.”

🤖 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/layers.rs` around lines 1617 - 1621, Update the
StaticCondition::AnyPlayerAttackedYouLastTurn evaluation to stop filtering
attackers with !p.is_eliminated, allowing departed opponents’ recorded attacks
to remain valid. Enforce expiry when the departed player’s skipped next-turn
boundary is reached in the existing last-turn attack record lifecycle,
preserving attacks until that boundary.

Source: Path instructions

@matthewevans matthewevans self-assigned this Aug 4, 2026
@matthewevans matthewevans added the bug Bug fix label Aug 4, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes — the last-turn defender ledger is preserved but not given the CR-required departed-player expiry.

🔴 Blocker

[HIGH] Eliminated players are excluded too early from the layer predicate. Evidence: crates/engine/src/game/layers.rs:1617-1621 filters eliminated players, while docs/MagicCompRules.txt:6429-6433 (CR 800.4i) says actions during a player's last turn remain findable until that player's next turn would have begun. types/game_state.rs:14142-14152 and the cleanup in turns.rs:2148-2161 preserve the record, but introduce no departed-player expiry. Why it matters: the engine loses a required last-turn attack relationship immediately on elimination instead of retaining it through the specified skipped-turn boundary. Suggested fix: carry the relationship through that CR boundary, then expire it when the eliminated player's next turn would begin; add a production-pipeline test for both sides of that boundary.

[HIGH] Required CI is failing the CR733 authority census. Evidence: run 30930044012, job 92062093858, reports attacked_defenders_last_turn as missing. Why it matters: the lifecycle field lacks the required authoritative classification/fixture evidence. Suggested fix: classify it at the authority that owns its lifecycle and update the corresponding fixture consistently with the retention/expiry behavior above.

✅ Evidence checked

The current head-bound parse-diff is scoped to Avenge only: one changed static/ReduceCost signature, matching the claimed scope. I also checked CodeRabbit's immediate-elimination suggestion against CR 800.4i; it conflicts with the verified rule and is not the requested fix.

Recommendation: request changes for the CR 800.4i retention boundary and the CR733 census failure, then re-request review on a new head.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants