manabrew-engine 0.2.4

Magic: The Gathering rules engine — a Rust port of Forge
Documentation
use forge_foundation::ZoneType;

use crate::card::{valid_filter, Card};
use crate::ids::PlayerId;
use crate::parsing::CompiledSelector;
use crate::spellability::SpellAbility;
use crate::staticability::StaticMode;

pub fn cant_target(
    cards: &[Card],
    target: &Card,
    activator: PlayerId,
    source_card: Option<&Card>,
    source_sa: Option<&SpellAbility>,
) -> bool {
    for source in cards.iter().filter(|c| c.zone == ZoneType::Battlefield) {
        for st_ab in source
            .static_abilities
            .iter()
            .filter(|sa| sa.check_mode(&StaticMode::CantTarget))
        {
            if !st_ab.ir.affected_zones.is_empty() {
                if !st_ab.ir.affected_zones.contains(&target.zone) {
                    continue;
                }
            } else if target.zone != ZoneType::Battlefield {
                continue;
            }

            if !matches_valid_target(st_ab.ir.valid_target.as_ref(), target, source) {
                continue;
            }
            if !matches_valid_activator(st_ab.ir.activator.as_ref(), activator, source.controller) {
                continue;
            }
            if let Some(valid_sa) = st_ab.ir.valid_sa.as_deref() {
                let Some(sa) = source_sa else {
                    continue;
                };
                if !spell_ability_matches(valid_sa, sa) {
                    continue;
                }
            }
            if let (Some(valid_source), Some(src)) = (st_ab.ir.valid_source.as_ref(), source_card) {
                if !matches_valid_target(Some(valid_source), src, source) {
                    continue;
                }
            }
            return true;
        }
    }
    false
}

pub fn apply_cant_target_ability(
    st_ab: &crate::staticability::StaticAbility,
    target: &Card,
    source: &Card,
    activator: PlayerId,
    source_card: Option<&Card>,
    source_sa: Option<&SpellAbility>,
) -> bool {
    if !st_ab.ir.affected_zones.is_empty() {
        if !st_ab.ir.affected_zones.contains(&target.zone) {
            return false;
        }
    } else if target.zone != ZoneType::Battlefield {
        return false;
    }

    if !matches_valid_target(st_ab.ir.valid_target.as_ref(), target, source) {
        return false;
    }
    if !matches_valid_activator(st_ab.ir.activator.as_ref(), activator, source.controller) {
        return false;
    }
    if let Some(valid_sa) = st_ab.ir.valid_sa.as_deref() {
        let Some(sa) = source_sa else {
            return false;
        };
        if !spell_ability_matches(valid_sa, sa) {
            return false;
        }
    }
    if let (Some(valid_source), Some(src)) = (st_ab.ir.valid_source.as_ref(), source_card) {
        if !matches_valid_target(Some(valid_source), src, source) {
            return false;
        }
    }
    true
}

fn spell_ability_matches(valid_sa: &str, sa: &SpellAbility) -> bool {
    let tokens: Vec<&str> = valid_sa
        .split(',')
        .map(|s| s.trim())
        .filter(|s| !s.is_empty())
        .collect();
    if tokens.is_empty() {
        return true;
    }
    tokens
        .iter()
        .all(|tok| match tok.to_ascii_lowercase().as_str() {
            "spell" => sa.is_spell,
            "istargeting" => sa.target_restrictions.is_some(),
            "xcost" => sa.cost_has_x(),
            _ => false,
        })
}

fn matches_valid_activator(
    valid: Option<&CompiledSelector>,
    player: PlayerId,
    source_controller: PlayerId,
) -> bool {
    valid_filter::matches_valid_player_selector_opt(valid, player, source_controller)
}

fn matches_valid_target(valid: Option<&CompiledSelector>, target: &Card, source: &Card) -> bool {
    valid_filter::matches_valid_card_selector_opt(valid, target, source)
}