Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/engine/src/parser/oracle_effect/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2399,6 +2399,8 @@ pub(super) fn rewrite_counter_instead_target_from_antecedent(
if !matches!(current_target, TargetFilter::SelfRef) {
return false;
}
// CR 608.2c + CR 115.1: an instead clause later in the same instruction
// reuses the original chosen target rather than announcing a new target.
// Existing attachment-host case — only when the antecedent is itself a `PutCounter`.
// Preserved verbatim (clone the host filter) so attachment-host cards stay byte-identical.
if let Effect::PutCounter {
Expand All @@ -2410,6 +2412,10 @@ pub(super) fn rewrite_counter_instead_target_from_antecedent(
*current_target = antecedent_target.clone();
return true;
}
if matches!(antecedent_target, TargetFilter::Typed(_)) {
*current_target = TargetFilter::ParentTarget;
return true;
}
return false;
}
// FIX A′ — CR 608.2c: an instead-override "Put a +1/+1 counter on it" whose antecedent
Expand Down
57 changes: 57 additions & 0 deletions crates/engine/src/parser/oracle_effect/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19144,6 +19144,63 @@ fn instead_condition_recognizes_that_permanent_is_color() {
);
}

/// CR 608.2c + CR 115.1: a conditional counter override whose base clause
/// targeted a creature reuses that chosen object through `ParentTarget`. The
/// override must not turn its bare "it" into the resolving source or request a
/// second target. Wakandan Royal Guard and Elder Cathar cover the unrestricted
/// and controller-qualified target forms of this grammar.
#[test]
fn counter_instead_override_reuses_typed_antecedent_target() {
for effect_text in [
"Put a +1/+1 counter on target creature. If that creature is another Hero, put two +1/+1 counters on it instead.",
"Put a +1/+1 counter on target creature you control. If that creature is a Human, put two +1/+1 counters on it instead.",
] {
let ability = parse_effect_chain(effect_text, AbilityKind::Spell);
assert!(
matches!(
ability.effect.as_ref(),
Effect::PutCounter {
count: QuantityExpr::Fixed { value: 1 },
target: TargetFilter::Typed(_),
..
}
),
"the base counter instruction must retain its typed target for {effect_text:?}; got {:?}",
ability.effect
);
let override_branch = ability
.sub_ability
.as_deref()
.unwrap_or_else(|| panic!("expected counter override for {effect_text:?}"));
assert!(
matches!(
override_branch.condition.as_ref(),
Some(AbilityCondition::ConditionInstead { inner })
if matches!(inner.as_ref(), AbilityCondition::TargetMatchesFilter { .. })
),
"the override must retain a typed target-match condition for {effect_text:?}; got {:?}",
override_branch.condition
);
assert!(
matches!(
override_branch.effect.as_ref(),
Effect::PutCounter {
count: QuantityExpr::Fixed { value: 2 },
target: TargetFilter::ParentTarget,
..
}
),
"the override must put two counters on the original target for {effect_text:?}; got {:?}",
override_branch.effect
);
assert!(
!matches!(ability.effect.as_ref(), Effect::Unimplemented { .. })
&& !matches!(override_branch.effect.as_ref(), Effect::Unimplemented { .. }),
"both clauses must lower without Effect::Unimplemented for {effect_text:?}"
);
}
}

/// CR 117.1 + CR 400.7j + CR 608.2k + CR 614.1a: Stormscale Anarch class —
/// the "discard a card at random" cost paid object is checked against the
/// "multicolored" property to gate the override damage. The condition is
Expand Down
104 changes: 104 additions & 0 deletions crates/engine/tests/integration/issue_6677_wakandan_royal_guard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! Regression for issue #6677: Wakandan Royal Guard's conditional counter
//! override must keep using the creature chosen for its ETB trigger.
//!
//! The real Oracle text first targets a creature, then says "put two +1/+1
//! counters on it instead" when that creature is another Hero. The override's
//! bare pronoun must resolve to the original target, never Wakandan Royal Guard.

use engine::game::scenario::{GameScenario, P0};
use engine::types::counter::CounterType;
use engine::types::identifiers::ObjectId;
use engine::types::mana::ManaCost;
use engine::types::phase::Phase;

const WAKANDAN_ROYAL_GUARD_ORACLE: &str = "Vigilance\n\
When this creature enters, put a +1/+1 counter on target creature. If that creature is another Hero, put two +1/+1 counters on it instead.";

fn p1p1_counters(state: &engine::types::game_state::GameState, object: ObjectId) -> u32 {
state
.objects
.get(&object)
.and_then(|card| card.counters.get(&CounterType::Plus1Plus1).copied())
.unwrap_or(0)
}

fn resolve_guard_targeting_creature(target_is_hero: bool) -> (u32, u32, u32) {
let mut scenario = GameScenario::new();
scenario.at_phase(Phase::PreCombatMain);

let selected = if target_is_hero {
scenario
.add_creature(P0, "Selected Hero", 2, 2)
.with_subtypes(vec!["Hero"])
.id()
} else {
scenario.add_creature(P0, "Selected Soldier", 2, 2).id()
};
let unrelated_hero = scenario
.add_creature(P0, "Unselected Hero", 2, 2)
.with_subtypes(vec!["Hero"])
.id();
let guard = scenario
.add_creature_to_hand_from_oracle(
P0,
"Wakandan Royal Guard",
2,
2,
WAKANDAN_ROYAL_GUARD_ORACLE,
)
.with_subtypes(vec!["Human", "Soldier", "Hero"])
.with_mana_cost(ManaCost::generic(0))
.id();

let mut runner = scenario.build();
let outcome = runner.cast(guard).target_object(selected).resolve();

(
p1p1_counters(outcome.state(), selected),
p1p1_counters(outcome.state(), guard),
p1p1_counters(outcome.state(), unrelated_hero),
)
}

/// CR 603.2 + CR 115.1d + CR 608.2c + CR 122.1: the ETB trigger targets the
/// selected Hero, and its matching instead-override puts two counters on that
/// same object. The zero-counter siblings prove the pronoun did not rebound to
/// either the resolving guard or another legal Hero.
#[test]
fn wakandan_royal_guard_doubles_counters_on_the_selected_hero() {
let (selected, guard, unrelated_hero) = resolve_guard_targeting_creature(true);

assert_eq!(
selected, 2,
"the selected Hero must receive two +1/+1 counters"
);
assert_eq!(
guard, 0,
"Wakandan Royal Guard must not receive the counters"
);
assert_eq!(
unrelated_hero, 0,
"an unselected Hero must not receive the counters"
);
}

/// CR 603.2 + CR 115.1d + CR 608.2c + CR 122.1: when the chosen creature is
/// not a Hero, the override does not apply and the printed base instruction
/// still places exactly one counter on that chosen object.
#[test]
fn wakandan_royal_guard_keeps_one_counter_on_a_nonhero_target() {
let (selected, guard, unrelated_hero) = resolve_guard_targeting_creature(false);

assert_eq!(
selected, 1,
"the non-Hero target must receive the base instruction's one counter"
);
assert_eq!(
guard, 0,
"Wakandan Royal Guard must not receive the counter"
);
assert_eq!(
unrelated_hero, 0,
"an unrelated Hero must not satisfy the selected-target condition"
);
}
1 change: 1 addition & 0 deletions crates/engine/tests/integration/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,7 @@ mod issue_654_stridehangar_automaton;
mod issue_6566_granted_leave_exile;
mod issue_6634_aven_courier;
mod issue_6643_party_dude_opponents_attacked;
mod issue_6677_wakandan_royal_guard;
mod issue_6678_captain_america_shield_tap_defender;
mod issue_680_shalai_and_hallar_forgotten_ancient;
mod issue_680_shalai_upkeep_move;
Expand Down
Loading