Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
23 changes: 18 additions & 5 deletions crates/engine/src/game/ability_rw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2202,8 +2202,11 @@ fn legacy_controller_ref(x: &ControllerRef) -> bool {
| ControllerRef::ChosenPlayer { .. }
| ControllerRef::SourceChosenPlayer
// CR 102.1: the active player is a game-defined role read live, not a
// frozen event-context tag.
| ControllerRef::ActivePlayer
// frozen event-context tag. `relation` (CR 102.2 / CR 102.3) narrows
// candidacy against the ability's own controller — also live, also not
// one of the legacy-12 event-context tags — so it does not change this
// answer.
| ControllerRef::ActivePlayer { .. }
| ControllerRef::EnchantedPlayer => false,
}
}
Expand Down Expand Up @@ -2551,7 +2554,10 @@ fn member_bound_controller_ref(x: &ControllerRef) -> bool {
| ControllerRef::TargetOpponent
// CR 102.1: the active player is a game-defined role read live from
// `state.active_player`, not per-source member-bound storage.
| ControllerRef::ActivePlayer
// `relation` (CR 102.2 / CR 102.3) additionally reads the ability's own
// controller and the format topology — both uniformity-invariant across
// batch members, so the reference stays member-INVARIANT.
| ControllerRef::ActivePlayer { .. }
| ControllerRef::DefendingPlayer => false,
}
}
Expand Down Expand Up @@ -6529,8 +6535,15 @@ fn rw_controller_ref(x: &ControllerRef) -> RwProfile {
| ControllerRef::TargetOpponent
| ControllerRef::DefendingPlayer
// CR 102.1: a live read of `state.active_player` — no sibling-mutable
// state, empty RW profile (mirrors `DefendingPlayer`).
| ControllerRef::ActivePlayer
// state, empty RW profile (mirrors `DefendingPlayer`). AUDITED for the
// `relation` parameterization: `state.active_player` is defined by whose
// TURN it is (CR 102.1) and a turn is a bounded five-phase sequence
// (CR 500.1), so it changes only at a turn boundary — never as a side
// effect of a sibling batch member. `relation` (CR 102.2 / CR 102.3) additionally
// reads only the ability's controller and the fixed format topology —
// neither is sibling-mutable — so the profile stays genuinely empty
// rather than fail-open.
| ControllerRef::ActivePlayer { .. }
// resolution-local (ResolvedAbility.chosen_players)
| ControllerRef::ChosenPlayer { .. } => RwProfile::empty(),
}
Expand Down
5 changes: 4 additions & 1 deletion crates/engine/src/game/ability_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4142,7 +4142,10 @@ fn scan_controller_ref(x: &ControllerRef) -> Axes {
},
ControllerRef::EnchantedPlayer => Axes::NONE,
// CR 102.1: a live read of `state.active_player` — no event/sibling axis.
ControllerRef::ActivePlayer => Axes::NONE,
// `relation` (CR 102.2 / CR 102.3) adds only a controller-identity read,
// which is likewise off every scanned axis. Destructured explicitly so a
// future field forces a re-audit rather than eliding into `{ .. }`.
ControllerRef::ActivePlayer { relation: _ } => Axes::NONE,
}
}

Expand Down
36 changes: 26 additions & 10 deletions crates/engine/src/game/ability_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3543,9 +3543,19 @@ pub(crate) fn collect_player_targets(
.and_then(|host| host.as_player())
== Some(p.id)
}
// CR 102.1 + CR 109.4: the active player, resolvable directly
// (unlike the fail-closed DefendingPlayer arm above).
Some(ControllerRef::ActivePlayer) => p.id == state.active_player,
// CR 102.1 + CR 102.2 / CR 102.3 + CR 109.4: the active player,
// resolvable directly (unlike the fail-closed DefendingPlayer arm
// above). Read LIVE; never latched at announce (CR 608.2b
// re-checks). `relation` narrows candidacy relative to the
// ability's controller through the team-aware authority.
Some(ControllerRef::ActivePlayer { relation }) => {
p.id == state.active_player
&& crate::game::players::active_player_satisfies_relation(
state,
Some(ability.controller),
*relation,
)
}
None => true,
})
.map(|p| p.id)
Expand Down Expand Up @@ -7040,10 +7050,11 @@ mod tests {
CastManaObjectScope, CastManaSpentMetric, Comparator, ContinuousModification,
ControllerRef, CountScope, CounterTransferMode, DamageChannel, DamageKindFilter, Duration,
Effect, FilterProp, GameRestriction, LibraryPosition, ModalChoice,
ModalSelectionConstraint, MultiTargetSpec, ObjectProperty, ObjectScope, ProhibitedActivity,
PtStat, PtValue, PtValueScope, QuantityExpr, QuantityRef, RestrictionExpiry,
RestrictionPlayerScope, SearchSelectionConstraint, SharedQuality, SharedQualityRelation,
StaticDefinition, TargetFilter, TargetRef, TypeFilter, TypedFilter, UnlessPayModifier,
ModalSelectionConstraint, MultiTargetSpec, ObjectProperty, ObjectScope, PlayerRelation,
ProhibitedActivity, PtStat, PtValue, PtValueScope, QuantityExpr, QuantityRef,
RestrictionExpiry, RestrictionPlayerScope, SearchSelectionConstraint, SharedQuality,
SharedQualityRelation, StaticDefinition, TargetFilter, TargetRef, TypeFilter, TypedFilter,
UnlessPayModifier,
};
use crate::types::card_type::CoreType;
use crate::types::game_state::{
Expand Down Expand Up @@ -7457,8 +7468,11 @@ mod tests {
ObjectId(1),
PlayerId(0),
);
let active_filter =
TargetFilter::Typed(TypedFilter::default().controller(ControllerRef::ActivePlayer));
let active_filter = TargetFilter::Typed(TypedFilter::default().controller(
ControllerRef::ActivePlayer {
relation: PlayerRelation::All,
},
));
assert_eq!(
collect_player_targets(&state, &ability, &active_filter),
vec![PlayerId(2)]
Expand Down Expand Up @@ -9876,7 +9890,9 @@ mod tests {
Effect::TargetOnly {
target: TargetFilter::Typed(
TypedFilter::creature()
.controller(ControllerRef::ActivePlayer)
.controller(ControllerRef::ActivePlayer {
relation: PlayerRelation::All,
})
.properties(vec![FilterProp::ControlledContinuouslySinceTurnBegan]),
),
},
Expand Down
60 changes: 48 additions & 12 deletions crates/engine/src/game/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ use crate::types::ability::{
CounterSourceRider, DelayedTriggerCondition, DieRollModifier, DoublePTMode, Duration,
EachDamageRecipient, Effect, EffectOutcomeSignal, EffectScope, FilterProp,
ForEachCategoryAction, GameRestriction, LibraryPosition, ManaProduction, ObjectProperty,
ObjectScope, PerpetualModification, PlayerFilter, PlayerScope, PtStat, PtValue, PtValueScope,
QuantityExpr, QuantityRef, ReplacementCondition, ReplacementDefinition, ReplacementMode,
SeatDirection, SharedQuality, SharedQualityRelation, SpeedDelta, SpellCastingOption,
SpellCastingOptionKind, SpellStackToGraveyardReplacement, StaticCondition, StaticDefinition,
TapStateChange, TargetFilter, TriggerDefinition, TypeFilter, TypedFilter, ZoneRef,
ObjectScope, PerpetualModification, PlayerFilter, PlayerRelation, PlayerScope, PtStat, PtValue,
PtValueScope, QuantityExpr, QuantityRef, ReplacementCondition, ReplacementDefinition,
ReplacementMode, SeatDirection, SharedQuality, SharedQualityRelation, SpeedDelta,
SpellCastingOption, SpellCastingOptionKind, SpellStackToGraveyardReplacement, StaticCondition,
StaticDefinition, TapStateChange, TargetFilter, TriggerDefinition, TypeFilter, TypedFilter,
ZoneRef,
};
use crate::types::card::CardFace;
use crate::types::card_type::CoreType;
Expand Down Expand Up @@ -887,12 +888,34 @@ fn fmt_typed_filter(tf: &TypedFilter) -> String {
ControllerRef::TriggeringPlayer => "triggering player's",
// CR 303.4b: Display label for enchanted-player controller scope.
ControllerRef::EnchantedPlayer => "enchanted player's",
// CR 102.1: Display label for active-player controller scope.
ControllerRef::ActivePlayer => "the active player's",
// CR 102.1 + CR 102.2 / CR 102.3: Display label for the
// active-player controller scope, rendered per legality
// relation so a narrowed scope is never described with the
// unnarrowed label. This slot is a possessive DETERMINER, so
// the `Controller` relation contributes only "your own" here
// — its turn narrowing trails the zone noun below.
ControllerRef::ActivePlayer { relation } => match relation {
PlayerRelation::All => "the active player's",
PlayerRelation::Opponent => "the active opponent's",
PlayerRelation::Controller => "your own",
},
};
let zone_str = format!("{zone:?}").to_lowercase();
// CR 102.1: whose turn it is, is a legality condition rather
// than a possession, so it reads as a trailing qualifier instead
// of interrupting the possessive determiner.
let turn_qualifier = if matches!(
scope,
ControllerRef::ActivePlayer {
relation: PlayerRelation::Controller
}
) {
" on your turn"
} else {
""
};
parts.push(format!(
"most prevalent creature type in {scope_str} {zone_str}"
"most prevalent creature type in {scope_str} {zone_str}{turn_qualifier}"
));
}
FilterProp::IsChosenCardType => parts.push("chosen card type".into()),
Expand Down Expand Up @@ -1059,8 +1082,15 @@ fn fmt_typed_filter(tf: &TypedFilter) -> String {
ControllerRef::TriggeringPlayer => "triggering player",
// CR 303.4b: Display label for enchanted-player controller scope.
ControllerRef::EnchantedPlayer => "enchanted player",
// CR 102.1: Display label for active-player controller scope.
ControllerRef::ActivePlayer => "the active player",
// CR 102.1 + CR 102.2 / CR 102.3: Display label for the
// active-player controller scope, rendered per legality relation
// so a narrowed scope is never described with the unnarrowed
// label.
ControllerRef::ActivePlayer { relation } => match relation {
PlayerRelation::All => "the active player",
PlayerRelation::Opponent => "the active opponent",
PlayerRelation::Controller => "you on your turn",
},
};
parts.push(label.into());
} else {
Expand Down Expand Up @@ -1134,8 +1164,14 @@ fn fmt_controller(ctrl: &ControllerRef) -> String {
ControllerRef::TriggeringPlayer => "triggering player controls",
// CR 303.4b: Display label for enchanted-player controller scope.
ControllerRef::EnchantedPlayer => "enchanted player controls",
// CR 102.1: Display label for active-player controller scope.
ControllerRef::ActivePlayer => "the active player controls",
// CR 102.1 + CR 102.2 / CR 102.3: Display label for the active-player
// controller scope, rendered per legality relation so a narrowed scope is
// never described with the unnarrowed label.
ControllerRef::ActivePlayer { relation } => match relation {
PlayerRelation::All => "the active player controls",
PlayerRelation::Opponent => "the active opponent controls",
PlayerRelation::Controller => "you control on your turn",
},
}
.into()
}
Expand Down
2 changes: 1 addition & 1 deletion crates/engine/src/game/effects/copy_spell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ fn resolve_copier_player(
| ControllerRef::EnchantedPlayer
// CR 102.1: no card scopes "the active player copies this spell";
// fail closed (mirrors DefendingPlayer / EnchantedPlayer).
| ControllerRef::ActivePlayer => None,
| ControllerRef::ActivePlayer { .. } => None,
}
}

Expand Down
17 changes: 15 additions & 2 deletions crates/engine/src/game/effects/sacrifice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,21 @@ fn resolve_sacrifice_scope(
)
.map(|pid| vec![pid])
.unwrap_or_default(),
// CR 102.1: the active player, read live.
Some(ControllerRef::ActivePlayer) => vec![state.active_player],
// CR 102.1 + CR 102.2 / CR 102.3: the active player, read LIVE (never
// latched — CR 608.2b), narrowed by `relation` relative to the ability's
// controller. Fails CLOSED to an empty sacrifice scope when the active
// player does not satisfy the relation.
Some(ControllerRef::ActivePlayer { relation }) => {
if crate::game::players::active_player_satisfies_relation(
state,
Some(ability.controller),
relation,
) {
vec![state.active_player]
} else {
Vec::new()
}
}
}
}

Expand Down
Loading
Loading