Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
96 changes: 87 additions & 9 deletions crates/engine/src/game/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4161,18 +4161,48 @@ fn fmt_trigger_condition(cond: &crate::types::ability::TriggerCondition) -> Stri
}
}

fn fmt_ordinal(n: u32) -> String {
let suffix = match n % 100 {
11..=13 => "th",
_ => match n % 10 {
1 => "st",
2 => "nd",
3 => "rd",
_ => "th",
},
};
format!("{n}{suffix}")
}

/// Format a `TriggerConstraint` as a human-readable string for the parse-details overlay.
fn fmt_trigger_constraint(c: &crate::types::ability::TriggerConstraint) -> String {
use crate::types::ability::TriggerConstraint as TC;
match c {
TC::OncePerTurn => "once per turn".into(),
TC::OncePerGame => "once per game".into(),
TC::OnlyDuringYourTurn => "only during your turn".into(),
TC::NthSpellThisTurn { n, filter } => match filter {
Some(f) => format!("on your {n}th {} spell this turn", fmt_target(f)),
None => format!("on your {n}th spell this turn"),
},
TC::NthDrawThisTurn { n } => format!("on your {n}th draw this turn"),
TC::NthSpellThisTurn {
n,
comparator,
filter,
} => {
let timing = match comparator {
Comparator::EQ => format!("on your {}", fmt_ordinal(*n)),
Comparator::GT if *n == 1 => "after your first".to_string(),
Comparator::GT
| Comparator::LT
| Comparator::GE
| Comparator::LE
| Comparator::NE => {
format!("when your spell count {} {n}", fmt_comparator(comparator))
}
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
match filter {
Some(f) => format!("{timing} {} spell this turn", fmt_target(f)),
None => format!("{timing} spell this turn"),
}
}
TC::NthDrawThisTurn { n } => format!("on your {} draw this turn", fmt_ordinal(*n)),
TC::OnlyDuringOpponentsTurn => "only during opponent's turn".into(),
TC::OnlyDuringYourMainPhase => "only during your main phase".into(),
TC::AtClassLevel { level } => format!("at class level {level}"),
Expand Down Expand Up @@ -11314,10 +11344,10 @@ mod tests {
use crate::database::legality::{legalities_to_export_map, LegalityStatus};
use crate::parser::oracle_ir::diagnostic::{CascadeSlot, OracleDiagnostic};
use crate::types::ability::{
AbilityCondition, AbilityKind, ContinuousModification, ControllerRef, CounterTransferMode,
DieResultBranch, Effect, PileSource, PlayerFilter, PlayerScope, PreventionAmount,
PreventionScope, ReplacementCondition, StaticDefinition, TargetFilter, VoteTally,
VoteVisibility, VoterScope,
AbilityCondition, AbilityKind, Comparator, ContinuousModification, ControllerRef,
CounterTransferMode, DieResultBranch, Effect, PileSource, PlayerFilter, PlayerScope,
PreventionAmount, PreventionScope, ReplacementCondition, StaticDefinition, TargetFilter,
TriggerConstraint, VoteTally, VoteVisibility, VoterScope,
};
use crate::types::card_type::CardType;
use crate::types::identifiers::{CardId, ObjectId};
Expand All @@ -11327,6 +11357,54 @@ mod tests {
use crate::types::statics::{BlockExceptionKind, ProhibitionScope};
use crate::types::zones::{EtbTapState, Zone};

#[test]
fn nonfirst_spell_constraint_has_grammatical_coverage_detail() {
assert_eq!(
fmt_trigger_constraint(&TriggerConstraint::NthSpellThisTurn {
n: 1,
comparator: Comparator::GT,
filter: None,
}),
"after your first spell this turn"
);
assert_eq!(
fmt_trigger_constraint(&TriggerConstraint::NthSpellThisTurn {
n: 2,
comparator: Comparator::EQ,
filter: None,
}),
"on your 2nd spell this turn"
);
assert_eq!(
fmt_trigger_constraint(&TriggerConstraint::NthSpellThisTurn {
n: 13,
comparator: Comparator::EQ,
filter: None,
}),
"on your 13th spell this turn"
);
assert_eq!(
fmt_trigger_constraint(&TriggerConstraint::NthDrawThisTurn { n: 3 }),
"on your 3rd draw this turn"
);
}

#[test]
fn ordinal_formatter_handles_last_digits_and_teens() {
for (n, expected) in [
(1, "1st"),
(2, "2nd"),
(3, "3rd"),
(4, "4th"),
(11, "11th"),
(12, "12th"),
(13, "13th"),
(21, "21st"),
] {
assert_eq!(fmt_ordinal(n), expected);
}
}

#[test]
fn change_zone_signature_exposes_enters_attacking() {
// #5495: a parser change flipping `enters_attacking` (e.g. teaching
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use crate::game::combat::AttackTarget;
use crate::game::zones::create_object;
use crate::parser::oracle::parse_oracle_text;
use crate::types::ability::{
AbilityCondition, AbilityCost, AbilityDefinition, AbilityKind, ControllerRef, Effect,
EffectScope, FilterProp, ObjectScope, PlayerFilter, QuantityExpr, QuantityRef,
AbilityCondition, AbilityCost, AbilityDefinition, AbilityKind, Comparator, ControllerRef,
Effect, EffectScope, FilterProp, ObjectScope, PlayerFilter, QuantityExpr, QuantityRef,
ReplacementDefinition, ReplacementMode, ResolvedAbility, TapStateChange, TargetFilter,
TargetRef, TriggerConstraint, TriggerDefinition, TypeFilter, TypedFilter, UnlessPayModifier,
};
Expand Down Expand Up @@ -1174,6 +1174,7 @@ fn setup_esper_sentinel_unless_payment(pay_mana: bool) -> GameState {
))
.constraint(TriggerConstraint::NthSpellThisTurn {
n: 1,
comparator: Comparator::EQ,
filter: Some(TargetFilter::Typed(
TypedFilter::default()
.with_type(TypeFilter::Non(Box::new(TypeFilter::Creature))),
Expand Down
8 changes: 6 additions & 2 deletions crates/engine/src/game/trigger_matchers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -679,19 +679,23 @@ fn player_matches_filter(
source_context: &TriggerSourceContext,
) -> bool {
let trigger_controller = source_context.source_read(state).controller();
// CR 102.3: In games between teams, teammates are not opponents; use the
// shared team-topology authority for every opponent-scoped player filter.
match filter {
TargetFilter::Player => true,
TargetFilter::AllPlayers => true,
TargetFilter::Controller => trigger_controller == player_id,
TargetFilter::Opponent => trigger_controller != player_id,
// In team games, opponents are players on other teams;
// teammates are not opponents even though their player IDs differ.
TargetFilter::Opponent => crate::game::players::is_opponent(state, trigger_controller, player_id),
TargetFilter::Typed(TypedFilter {
controller: Some(ControllerRef::You),
..
}) => trigger_controller == player_id,
TargetFilter::Typed(TypedFilter {
controller: Some(ControllerRef::Opponent),
..
}) => trigger_controller != player_id,
}) => crate::game::players::is_opponent(state, trigger_controller, player_id),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
TargetFilter::SourceChosenPlayer => source_context
.source_read(state)
.lki()
Expand Down
15 changes: 12 additions & 3 deletions crates/engine/src/game/triggers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9539,7 +9539,11 @@ fn check_trigger_constraint_with_ref(
// CR 603.2: Per-caster spell count. The caster is extracted from the SpellCast
// event; the count comes from the per-player map (not the global counter).
// When `filter` contains `TypeFilter::Non(Creature)`, use the noncreature counter.
TriggerConstraint::NthSpellThisTurn { n, filter } => {
TriggerConstraint::NthSpellThisTurn {
n,
comparator,
filter,
} => {
let caster = match event {
Some(GameEvent::SpellCast { controller: c, .. }) => *c,
_ => return false,
Expand Down Expand Up @@ -9571,7 +9575,7 @@ fn check_trigger_constraint_with_ref(
})
.count() as u32,
});
count == *n
comparator.evaluate(count as i32, *n as i32)
}
// CR 121.2: Use the ordinal stamped onto the individual draw event
// rather than the final per-turn count after a multi-card draw batch.
Expand Down Expand Up @@ -25606,6 +25610,7 @@ pub mod tests {
let mut d = make_trigger(TriggerMode::SpellCast);
d.constraint = Some(TriggerConstraint::NthSpellThisTurn {
n: 1,
comparator: Comparator::EQ,
filter: Some(TargetFilter::Typed(
TypedFilter::default().properties(vec![FilterProp::HasXInManaCost]),
)),
Expand Down Expand Up @@ -25833,7 +25838,11 @@ pub mod tests {
.valid_target(TargetFilter::Typed(
TypedFilter::default().controller(ControllerRef::You),
))
.constraint(TriggerConstraint::NthSpellThisTurn { n: 2, filter: None })
.constraint(TriggerConstraint::NthSpellThisTurn {
n: 2,
comparator: Comparator::EQ,
filter: None,
})
.execute(AbilityDefinition::new(
AbilityKind::Database,
Effect::Draw {
Expand Down
Loading
Loading