use super::*;
use crate::cost::CostPart;
impl GameLoop {
pub(crate) fn mana_source_available_for_payment(
game: &GameState,
player: PlayerId,
card_id: CardId,
) -> bool {
Self::mana_source_available_for_payment_with_reserved(game, player, card_id, &[])
}
pub(crate) fn mana_source_available_for_payment_with_reserved(
game: &GameState,
player: PlayerId,
card_id: CardId,
reserved_sacrifices: &[CardId],
) -> bool {
Self::mana_source_available_for_payment_with_reserved_and_reuse(
game,
player,
card_id,
reserved_sacrifices,
false,
)
}
pub(crate) fn mana_source_available_for_payment_with_reserved_and_reuse(
game: &GameState,
player: PlayerId,
card_id: CardId,
reserved_sacrifices: &[CardId],
allow_reserved_source_reuse: bool,
) -> bool {
let card = game.card(card_id);
let summoning_sick = card.is_creature() && card.summoning_sick && !card.has_haste();
let has_usable_mana_ability = card.activated_abilities.iter().any(|ab| {
let needs_tap = ab
.cost
.parts
.iter()
.any(|part| matches!(part, CostPart::Tap));
ab.is_mana_ability
&& (!card.tapped || !needs_tap)
&& (!summoning_sick || !needs_tap)
&& Self::mana_ability_available_for_payment_with_reserved_and_reuse(
game,
player,
card_id,
ab,
reserved_sacrifices,
allow_reserved_source_reuse,
)
});
has_usable_mana_ability
}
pub(crate) fn mana_ability_available_for_payment_with_reserved(
game: &GameState,
player: PlayerId,
card_id: CardId,
ab: &crate::ability::activated::ActivatedAbility,
reserved_sacrifices: &[CardId],
) -> bool {
Self::mana_ability_available_for_payment_with_reserved_and_reuse(
game,
player,
card_id,
ab,
reserved_sacrifices,
false,
)
}
pub(crate) fn mana_ability_available_for_payment_with_reserved_and_reuse(
game: &GameState,
player: PlayerId,
card_id: CardId,
ab: &crate::ability::activated::ActivatedAbility,
reserved_sacrifices: &[CardId],
allow_reserved_source_reuse: bool,
) -> bool {
if !crate::cost::can_pay_ignoring_mana(&ab.cost, game, card_id, player) {
return false;
}
if !crate::mana::mana_ability_meets_script_requirements(game, card_id, ab) {
return false;
}
for part in &ab.cost.parts {
if let CostPart::Sacrifice {
type_filter,
amount,
} = part
{
if type_filter == "CARDNAME" {
if amount.resolve(game, card_id, player) > 1
|| (reserved_sacrifices.contains(&card_id) && !allow_reserved_source_reuse)
{
return false;
}
} else {
let mut targets = crate::cost::get_sacrifice_targets_for_cost(
game,
player,
type_filter,
None,
);
if !allow_reserved_source_reuse {
targets.retain(|cid| !reserved_sacrifices.contains(cid));
}
if (targets.len() as i32) < amount.resolve(game, card_id, player) {
return false;
}
}
}
}
true
}
pub(crate) fn action_space(
&self,
game: &GameState,
player: PlayerId,
is_main_phase: bool,
) -> crate::agent::PriorityActionSpace {
let _params_lookup_scope =
crate::perf::ParamsLookupScopeGuard::enter(crate::perf::ParamsLookupScope::ActionSpace);
let can_play_sorcery =
is_main_phase && player == game.active_player() && game.stack.is_empty();
let must_be_instant = !can_play_sorcery;
let playable = {
let _params_lookup_scope = crate::perf::ParamsLookupScopeGuard::enter(
crate::perf::ParamsLookupScope::ActionSpacePlayable,
);
self.get_playable_cards(game, player, must_be_instant)
};
let activatable: Vec<crate::agent::ActivatableAction> = {
let _params_lookup_scope = crate::perf::ParamsLookupScopeGuard::enter(
crate::perf::ParamsLookupScope::ActionSpaceActivatable,
);
self.get_activatable_abilities(game, player, can_play_sorcery)
.into_iter()
.map(|(card_id, ability_index)| {
let card = game.card(card_id);
let ability = card
.activated_abilities
.iter()
.find(|a| a.ability_index == ability_index);
let (produced_mana, produced_mana_amount) = ability
.map(|a| {
crate::mana::mana_ability_prompt_metadata(game, card_id, player, a)
})
.unwrap_or((None, None));
crate::agent::ActivatableAction {
card_id,
ability_index,
description: ability
.map(|a| a.display_description(&card.card_name))
.unwrap_or_default(),
cost: ability.and_then(|a| a.cost_string()),
is_mana_ability: ability.map(|a| a.is_mana_ability).unwrap_or(false),
is_class_level_up: ability.is_some_and(|a| {
a.ability_api == Some(crate::ability::api_type::ApiType::ClassLevelUp)
}),
produced_mana,
produced_mana_amount,
}
})
.collect()
};
let tappable_lands: Vec<CardId> = {
let _params_lookup_scope = crate::perf::ParamsLookupScopeGuard::enter(
crate::perf::ParamsLookupScope::ActionSpaceManaSource,
);
game.cards_in_zone(ZoneType::Battlefield, player)
.iter()
.copied()
.filter(|&cid| Self::mana_source_available_for_payment(game, player, cid))
.collect()
};
let untappable_lands: Vec<CardId> = {
let _params_lookup_scope = crate::perf::ParamsLookupScopeGuard::enter(
crate::perf::ParamsLookupScope::ActionSpaceUntap,
);
self.undoable_mana_sources(player)
};
let mut mana_abilities = Vec::new();
for &card_id in &tappable_lands {
let card = game.card(card_id);
for ability in card
.activated_abilities
.iter()
.filter(|a| a.is_mana_ability)
{
let (produced_mana, produced_mana_amount) =
crate::mana::mana_ability_prompt_metadata(game, card_id, player, ability);
mana_abilities.push(crate::agent::ActivatableAction {
card_id,
ability_index: ability.ability_index,
description: ability.display_description(&card.card_name),
cost: ability.cost_string(),
is_mana_ability: true,
is_class_level_up: false,
produced_mana,
produced_mana_amount,
});
}
}
crate::agent::PriorityActionSpace {
playable,
tappable_lands,
untappable_lands,
activatable,
mana_abilities,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ability::activated::parse_activated_ability;
use crate::card::Card;
use forge_foundation::{CardTypeLine, ColorSet, ManaCost, PhaseType};
fn make_card(
id: u32,
owner: PlayerId,
name: &str,
type_line: &str,
abilities: Vec<&str>,
) -> Card {
let mut card = Card::new(
CardId(id),
name.to_string(),
owner,
CardTypeLine::parse(type_line),
ManaCost::zero(),
ColorSet::COLORLESS,
None,
None,
vec![],
vec![],
);
card.abilities = abilities.iter().map(|s| s.to_string()).collect();
card.activated_abilities = abilities
.iter()
.enumerate()
.filter_map(|(i, raw)| parse_activated_ability(raw, i))
.collect();
card
}
#[test]
fn tapped_non_tap_mana_source_is_available_for_payment() {
let player = PlayerId(0);
let mut game = GameState::new(&["P1", "P2"], 20);
let spawn = game.create_card(make_card(
1,
player,
"Eldrazi Spawn Token",
"Creature Eldrazi Spawn",
vec!["AB$ Mana | Cost$ Sac<1/CARDNAME> | Produced$ C | Amount$ 1"],
));
game.add_card_to_zone(ZoneType::Battlefield, player, spawn);
game.card_mut(spawn).zone = ZoneType::Battlefield;
game.card_mut(spawn).tapped = true;
assert!(GameLoop::mana_source_available_for_payment(
&game, player, spawn
));
}
#[test]
fn reserved_cardname_mana_source_can_be_reused_for_activated_cost_payment() {
let player = PlayerId(0);
let mut game = GameState::new(&["P1", "P2"], 20);
let spawn = game.create_card(make_card(
1,
player,
"Eldrazi Spawn Token",
"Creature Eldrazi Spawn",
vec!["AB$ Mana | Cost$ Sac<1/CARDNAME> | Produced$ C | Amount$ 1"],
));
game.add_card_to_zone(ZoneType::Battlefield, player, spawn);
game.card_mut(spawn).zone = ZoneType::Battlefield;
let mana_ability = game.card(spawn).activated_abilities[0].clone();
assert!(
!GameLoop::mana_ability_available_for_payment_with_reserved_and_reuse(
&game,
player,
spawn,
&mana_ability,
&[spawn],
false,
)
);
assert!(
GameLoop::mana_ability_available_for_payment_with_reserved_and_reuse(
&game,
player,
spawn,
&mana_ability,
&[spawn],
true,
)
);
}
#[test]
fn untapped_non_mana_land_is_not_available_for_payment() {
let player = PlayerId(0);
let mut game = GameState::new(&["P1", "P2"], 20);
let vista = game.create_card(make_card(
1,
player,
"Prismatic Vista",
"Land",
vec!["AB$ ChangeZone | Cost$ T PayLife<1> Sac<1/CARDNAME> | Origin$ Library | Destination$ Battlefield | ChangeType$ Land.Basic"],
));
game.add_card_to_zone(ZoneType::Battlefield, player, vista);
game.card_mut(vista).zone = ZoneType::Battlefield;
assert!(!GameLoop::mana_source_available_for_payment(
&game, player, vista
));
}
#[test]
fn action_space_marks_only_class_level_up_abilities() {
let player = PlayerId(0);
let mut game = GameState::new(&["P1", "P2"], 20);
game.turn.phase = PhaseType::Main1;
let class = game.create_card(make_card(
1,
player,
"Test Class",
"Enchantment Class",
vec![
"AB$ ClassLevelUp | Cost$ 0 | ClassLevel$ EQ1 | SorcerySpeed$ True | SpellDescription$ Level 2",
"AB$ Draw | Cost$ 0 | Defined$ You | NumCards$ 1 | SpellDescription$ Draw a card.",
],
));
game.card_mut(class).set_class_level(1);
game.add_card_to_zone(ZoneType::Battlefield, player, class);
game.card_mut(class).zone = ZoneType::Battlefield;
let game_loop = GameLoop::new(2);
let action_space = game_loop.action_space(&game, player, true);
let class_level_up = action_space
.activatable
.iter()
.find(|action| action.description == "Level 2")
.expect("class level action");
let draw = action_space
.activatable
.iter()
.find(|action| action.description == "Draw a card.")
.expect("ordinary activated ability");
assert!(class_level_up.is_class_level_up);
assert!(!draw.is_class_level_up);
}
}