fix(engine): add active-voice "that dealt damage this turn" creature filter (#5615)#5877
fix(engine): add active-voice "that dealt damage this turn" creature filter (#5615)#5877philluiz2323 wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the FilterProp::DealtDamageThisTurn variant to represent the active-voice counterpart of WasDealtDamageThisTurn (CR 120.1), matching objects that were the source of a damage event this turn. The change integrates this new property across the engine's filter, parser, AI support, coverage, and event snapshot modules, and includes corresponding unit tests to verify both active-voice parsing and correct source-matching behavior. I have no feedback to provide.
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.
Parse changes introduced by this PR · 4 card(s), 5 signature(s) (baseline: main
|
matthewevans
left a comment
There was a problem hiding this comment.
Verdict: changes requested — the active-voice filter is plausible, but the measured parser impact and production behavior are not yet adequately accounted for.
🔴 Blocker
The current parse-diff reports 32 changed cards, while the PR claims only Red Guardian. crates/engine/src/game/coverage.rs:946-947 explains 30 of those as a passive-voice coverage-label correction (dealt damage → was dealt damage), but the same artifact reports two cards losing until end of turn and two active-voice target additions (Red Guardian and Avenging Arrow). Please explicitly reconcile every category in the sticky diff and verify the two duration removals are correct parser behavior rather than an unintended suffix-consumption change.
The tests exercise parse_target and matches_target_filter directly, but neither drives the affected card through target generation/resolution. Add a production-pipeline regression for Red Guardian (or a shared card-level path) showing that a creature that dealt damage this turn is targetable and an otherwise identical creature that did not is not. That test must fail if the new FilterProp or its parser wiring is reverted.
🟡 Non-blocking
The active and passive damage roles belong to the same CR 120.1 axis, and reusing damage_dealt_this_turn is the right direction. The coverage-label change may be valid; the blocker is that its 32-card blast radius is currently unexplained and untested.
✅ Clean
The live and LKI evaluators both use the source ID for the active voice, while the passive filter remains target-based; the direct filter test correctly distinguishes those roles.
Recommendation: reconcile the full parse-diff and add a discriminating card-level runtime test, then request re-review.
…filter (phase-rs#5615) Red Guardian, Super-Soldier reads "When Red Guardian enters, destroy target creature an opponent controls that dealt damage this turn", but it could destroy a creature that had not dealt any damage. The parser only recognized the PASSIVE "that was dealt damage this turn" (`FilterProp::WasDealtDamageThisTurn` — the creature is the damage recipient). Red Guardian's clause is the ACTIVE voice — the creature is the damage SOURCE — which had no filter, so it was dropped and the target collapsed to "creature an opponent controls" (any). Add `FilterProp::DealtDamageThisTurn` (CR 120.1: "an object that deals damage is the source of that damage"). It reads the same per-turn ledger `state.damage_dealt_this_turn` the passive filter reads, matching on `source_id` instead of `target`. Wired through the parser verb-phrase table, the live and LKI filter evaluators, and every exhaustive classification site (read/write profile, projected-resource axis, AI memoization poison set, swallow-check, event-support, coverage emitter). The two form a damage-role pair; a doc-comment records that a third damage-role filter (e.g. "dealt combat damage") should fold the pair into `DamageThisTurn { role, combat_only }` per the sibling-cluster threshold. Verified: cargo test -p engine --lib -> 16675 passed, 0 failed. New tests cover the parser (active vs passive voice) and the filter evaluator (damage source matches, recipient does not, and the mirror for the passive filter).
…l diff (phase-rs#5615) Addresses review on phase-rs#5877: - Add a production-pipeline regression: Red Guardian's REAL parsed target filter is driven through `find_legal_targets`. An opponent creature that dealt damage this turn is a legal target; an otherwise-identical one that did not is not. Fails if the `DealtDamageThisTurn` FilterProp or its parser wiring is reverted (the filter would drop to "any opponent creature" and both would qualify). - Revert the passive `WasDealtDamageThisTurn` coverage label back to "dealt damage this turn" (now shared by both damage-role arms). This drops the 30 cosmetic label-only entries from the parse-diff, leaving only the genuine target-filter additions (Red Guardian, Avenging Arrow, and the bounce/pump cards) plus the two spurious `until end of turn` removals — which are correct: the old parser mis-read the bare "this turn" as a duration, and consuming "dealt damage this turn" as a filter removes it.
07d962b to
944324d
Compare
|
Thanks — both points addressed in the latest push. Parse-diff reconciled (all 32 categories):
Card-level runtime test added:
|
Summary
Fixes #5615. Red Guardian, Super-Soldier — "When Red Guardian enters, destroy target creature an opponent controls that dealt damage this turn" — could destroy a creature that had dealt no damage. The active-voice damage-source clause had no filter and was silently dropped, so the target collapsed to "creature an opponent controls" (any).
Implementation method (required)
How was the engine/parser logic in this PR produced? Check exactly one:
/engine-implementerpipeline (plan → review-plan → implement → review-impl → commit)/engine-implementer— explain why belowRoot cause
The parser recognized only the passive "that was dealt damage this turn" (
FilterProp::WasDealtDamageThisTurn— the creature is the damage recipient). Red Guardian's clause is the active voice — the creature is the damage source — for which noFilterPropexisted. The relative clause was dropped, leaving:so any opponent creature was a legal target.
Fix
Add
FilterProp::DealtDamageThisTurn(CR 120.1: "an object that deals damage is the source of that damage"). It reads the same per-turn ledgerstate.damage_dealt_this_turnthe passive filter reads, matching onsource_idinstead oftarget— so it reuses existing infrastructure rather than adding new bookkeeping. Wired through:"dealt damage this turn", distinct first word from the passive"was dealt damage this turn"— no ordering conflict);ability_rw), projected-resource axis (ability_scan→CONSERVATIVE, same as the passive arm), AI memoization-poison set, swallow-check, event-support, and the coverage doc-emitter.The
/add-engine-variantgate returned EXTEND_OK / WITHIN_SECTION: the pair shares the CR-120 damage-role axis but sits below the 3-sibling refactor threshold; a doc-comment records that a third damage-role filter should fold the pair intoDamageThisTurn { role, combat_only }.CR references
CR 120.1— an object that deals damage is the source of that damage (the filtered creature is that source).Verification
cargo test -p engine --lib→ 16675 passed, 0 failed (full suite; a target-filter change can silently break unrelated cards).oracle_targettest: "that dealt damage this turn" parses toDealtDamageThisTurn(active) and NOTWasDealtDamageThisTurn(passive).filtertest: the damage source matchesDealtDamageThisTurnwhile the recipient does not — and the exact mirror for the passive filter — proving the two are not conflated.cargo fmt --allclean.