Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
13 changes: 10 additions & 3 deletions crates/engine/src/game/replacement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1003,10 +1003,17 @@ pub fn replacement_choice_waiting_for(player: PlayerId, state: &GameState) -> Wa
}

/// CR 614.12a: Park on the replacement choice for `player`, unless a downstream
/// effect (a Devour as-enters Sacrifice `EffectZoneChoice`) already surfaced its
/// own interactive prompt — then leave it so the pending choice isn't clobbered.
/// as-enters effect already surfaced its own interactive prompt. Leave that prompt
/// in place so the entry choice completes before the surrounding ability resumes.
pub fn park_waiting_for(state: &mut GameState, player: PlayerId) {
if matches!(state.waiting_for, WaitingFor::EffectZoneChoice { .. }) {
if matches!(
state.waiting_for,
WaitingFor::EffectZoneChoice { .. }
| WaitingFor::CopyTargetChoice { .. }
| WaitingFor::ChooseOneOfBranch { .. }
| WaitingFor::NamedChoice { .. }
| WaitingFor::ReturnAsAuraTarget { .. }
) {

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

Add the required rules annotation.

park_waiting_for changes how the engine preserves choices while a permanent enters the battlefield. Add a verified CR 614.12a: ... annotation at this logic. CR 614.12a requires the choice before the permanent enters the battlefield. (media.wizards.com)

As per coding guidelines, “verify the relevant CR section before completion, and annotate rules-related code with a verified CR number and description.” As per path instructions, rules-touching code without that annotation is a finding.

🤖 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/replacement.rs` around lines 1006 - 1016, Annotate the
park_waiting_for function’s waiting-choice preservation logic with a verified
“CR 614.12a” reference and concise description that the required choice occurs
before the permanent enters the battlefield. Place the annotation directly at
the relevant logic without changing its behavior.

Sources: Coding guidelines, Path instructions

return;
}
state.waiting_for = replacement_choice_waiting_for(player, state);
Expand Down
103 changes: 102 additions & 1 deletion crates/engine/tests/integration/issue_1515_emperor_of_bones.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,30 @@ use engine::game::effects::resolve_ability_chain;
use engine::game::scenario::{GameScenario, P0};
use engine::parser::oracle_effect::parse_effect_chain;
use engine::types::ability::{
AbilityKind, ContinuousModification, DelayedTriggerCondition, Effect, TargetFilter,
AbilityKind, ChoiceType, ChosenAttribute, ContinuousModification, DelayedTriggerCondition,
Effect, TargetFilter,
};
use engine::types::actions::GameAction;
use engine::types::counter::CounterType;
use engine::types::game_state::{ExileLink, ExileLinkKind, WaitingFor};
use engine::types::identifiers::ObjectId;
use engine::types::keywords::Keyword;
use engine::types::phase::Phase;
use engine::types::player::PlayerId;
use engine::types::zones::Zone;

const EMPEROR_COUNTER_TRIGGER_EFFECT: &str = "put a creature card exiled with this creature onto \
the battlefield under your control with a finality counter on it. it gains haste. sacrifice it at \
the beginning of the next end step.";

const ANOINTED_PEACEKEEPER: &str = "Vigilance\n\
As this creature enters, look at an opponent's hand, then choose any card name.\n\
Spells your opponents cast with the chosen name cost {2} more to cast.\n\
Activated abilities of sources with the chosen name cost {2} more to activate unless they're mana abilities.";

const P1: PlayerId = PlayerId(1);
const NAMED_CARD: &str = "Llanowar Elves";

fn creature_has_haste_from_transient_effects(
state: &engine::types::game_state::GameState,
creature: ObjectId,
Expand Down Expand Up @@ -153,3 +164,93 @@ fn issue_1515_emperor_of_bones_binds_haste_and_delayed_sacrifice_to_returned_cre
"the delayed sacrifice must not sacrifice Emperor"
);
}

/// CR 614.12a + CR 400.7j: An as-enters choice on the returned permanent must
/// complete without losing later instructions that refer to that permanent.
#[test]
fn emperor_of_bones_resumes_riders_after_anointed_peacekeepers_as_enters_choices() {
let mut scenario = GameScenario::new_n_player(2, 7);
scenario.at_phase(Phase::PreCombatMain);
let emperor = scenario.add_creature(P0, "Emperor of Bones", 2, 2).id();
let _opponent_card = scenario.add_card_to_hand(P1, "Opponent Secret");
let peacekeeper = {
let mut builder = scenario.add_creature_to_exile(P0, "Anointed Peacekeeper", 3, 3);
builder.from_oracle_text(ANOINTED_PEACEKEEPER);
builder.id()
};

let mut runner = scenario.build();
runner.state_mut().all_card_names = std::sync::Arc::from([NAMED_CARD.to_string()]);
runner.state_mut().exile_links.push(ExileLink {
exiled_id: peacekeeper,
source_id: emperor,
kind: ExileLinkKind::TrackedBySource,
});

let definition = parse_effect_chain(EMPEROR_COUNTER_TRIGGER_EFFECT, AbilityKind::Spell);
let ability = build_resolved_from_def(&definition, emperor, P0);
let mut events = Vec::new();
resolve_ability_chain(runner.state_mut(), &ability, &mut events, 0)
.expect("Emperor of Bones return must reach Peacekeeper's as-enters choice");

let WaitingFor::NamedChoice {
choice_type,
options,
..
} = runner.state().waiting_for.clone()
else {
panic!(
"Peacekeeper must ask which opponent to look at, got {}",
runner.waiting_for_kind()
);
};
assert!(matches!(choice_type, ChoiceType::Opponent { .. }));
assert_eq!(options, vec![P1.0.to_string()]);
runner
.act(GameAction::ChooseOption {
choice: P1.0.to_string(),
})
.expect("choose the opponent whose hand Peacekeeper looks at");

let WaitingFor::NamedChoice { choice_type, .. } = runner.state().waiting_for.clone() else {
panic!(
"Peacekeeper must ask for a card name after looking, got {}",
runner.waiting_for_kind()
);
};
assert!(matches!(choice_type, ChoiceType::CardName));
runner
.act(GameAction::ChooseOption {
choice: NAMED_CARD.to_string(),
})
.expect("choose the card name for Peacekeeper");

let state = runner.state();
let returned = &state.objects[&peacekeeper];
assert_eq!(returned.zone, Zone::Battlefield);
assert!(returned.chosen_attributes.iter().any(
|attribute| matches!(attribute, ChosenAttribute::CardName(name) if name == NAMED_CARD)
));
assert_eq!(
returned
.counters
.get(&CounterType::Finality)
.copied()
.unwrap_or(0),
1,
"Peacekeeper must retain Emperor's finality entry modifier"
);
assert!(
creature_has_haste_from_transient_effects(state, peacekeeper),
"Emperor's forwarded haste rider must resume after both as-enters choices"
);
assert_eq!(
state.delayed_triggers.len(),
1,
"Emperor's delayed sacrifice rider must resume after both as-enters choices"
);
assert_eq!(
state.delayed_triggers[0].ability.targets,
vec![engine::types::ability::TargetRef::Object(peacekeeper)]
);
}
Loading