manabrew-engine 0.4.1

Magic: The Gathering rules engine — a Rust port of Forge
Documentation
use crate::game::GameState;
use crate::ids::PlayerId;

pub fn is_active_player(game: &GameState, player: PlayerId) -> bool {
    game.active_player() == player
}

pub fn is_opponent_of(game: &GameState, player: PlayerId, other: PlayerId) -> bool {
    player != other && !same_team(game, player, other)
}

pub fn same_team(game: &GameState, player: PlayerId, other: PlayerId) -> bool {
    let a = game.player(player).team_number;
    let b = game.player(other).team_number;
    a >= 0 && a == b
}

pub fn is_alive(game: &GameState, player: PlayerId) -> bool {
    game.player(player).is_alive()
}

pub fn can_discard_by(game: &GameState, player: PlayerId, other: PlayerId) -> bool {
    is_opponent_of(game, player, other) || same_team(game, player, other)
}

pub fn has_counters(game: &GameState, player: PlayerId) -> bool {
    let p = game.player(player);
    p.poison_counters > 0 || p.energy_counters > 0 || p.radiation_counters > 0
}

pub fn life_less_or_equal_to(game: &GameState, player: PlayerId, life: i32) -> bool {
    game.player(player).life <= life
}

pub fn life_greater_or_equal_to(game: &GameState, player: PlayerId, life: i32) -> bool {
    game.player(player).life >= life
}

pub fn has_counter(game: &GameState, player: PlayerId, counter: &str) -> bool {
    match counter {
        "Poison" => game.player(player).poison_counters > 0,
        "Energy" => game.player(player).energy_counters > 0,
        "Rad" | "Radiation" => game.player(player).radiation_counters > 0,
        _ => false,
    }
}

pub fn has_keyword(game: &GameState, player: PlayerId, keyword: &str) -> bool {
    game.player(player)
        .changed_keywords
        .iter()
        .any(|candidate| candidate.eq_ignore_ascii_case(keyword))
}

pub fn can_be_attached(game: &GameState, player: PlayerId) -> bool {
    is_alive(game, player)
}

pub fn has_urza_lands(game: &GameState, player: PlayerId) -> bool {
    let controls = |subtype: &str| {
        game.cards_in_zone(forge_foundation::ZoneType::Battlefield, player)
            .iter()
            .any(|&cid| {
                let card = game.card(cid);
                card.type_line.has_subtype("Urza's") && card.type_line.has_subtype(subtype)
            })
    };
    controls("Mine") && controls("Power-Plant") && controls("Tower")
}

pub fn restriction(game: &GameState, player: PlayerId, restriction: &str) -> bool {
    if restriction.trim().is_empty() {
        return true;
    }
    restriction
        .split(',')
        .map(str::trim)
        .filter(|part| !part.is_empty())
        .all(|part| match part {
            "You" | "Player.You" => true,
            "Opponent" | "Player.Opponent" => game
                .player_order
                .iter()
                .copied()
                .any(|other| is_opponent_of(game, player, other)),
            "ActivePlayer" => is_active_player(game, player),
            "Monarch" => game.monarch == Some(player),
            _ => false,
        })
}

pub fn compare_by_zone_size(
    game: &GameState,
    zone: forge_foundation::ZoneType,
) -> impl Fn(&PlayerId, &PlayerId) -> std::cmp::Ordering + '_ {
    move |a, b| {
        game.cards_in_zone(zone, *a)
            .len()
            .cmp(&game.cards_in_zone(zone, *b).len())
    }
}

pub fn compare_by_life(
    game: &GameState,
) -> impl Fn(&PlayerId, &PlayerId) -> std::cmp::Ordering + '_ {
    move |a, b| game.player(*a).life.cmp(&game.player(*b).life)
}

pub fn compare_by_poison(
    game: &GameState,
) -> impl Fn(&PlayerId, &PlayerId) -> std::cmp::Ordering + '_ {
    move |a, b| {
        game.player(*a)
            .poison_counters
            .cmp(&game.player(*b).poison_counters)
    }
}