manabrew-engine 0.4.1

Magic: The Gathering rules engine — a Rust port of Forge
Documentation
use serde::{Deserialize, Serialize};

use crate::agent::types::{MainPhaseAction, PlayOption, TargetChoice};
use crate::agent::PlayerAgent;
use crate::ids::{CardId, PlayerId};
use crate::player::PlayerController;

pub const STATIC_ALTERNATIVE_ABILITY_INDEX: usize = usize::MAX;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PlayerAction {
    PassPriority,
    Concede,
    FinishTargeting,
    CastSpell(PlayOption),
    ActivateMana(CardId, Option<usize>, Option<u16>),
    UndoMana(CardId),
    ActivateAbility(AbilityRef),
    PayCost(CardId),
    PayManaFromPool(ManaChoice),
    SelectCard(CardId),
    SelectPlayer(PlayerId),
    TargetEntity(TargetEntity),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct AbilityRef {
    pub card_id: CardId,
    pub ability_index: usize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ManaChoice {
    pub color_code: u8,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TargetEntity {
    Card(CardId),
    Player(PlayerId),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlayerActionOutcome {
    Priority(MainPhaseAction),
    Target(TargetChoice),
    Pending,
}

impl PlayerAction {
    pub fn to_priority_action(
        self,
        playable: &[PlayOption],
        tappable_lands: &[CardId],
        untappable_lands: &[CardId],
        activatable: &[(CardId, usize)],
    ) -> Option<MainPhaseAction> {
        match self {
            PlayerAction::PassPriority => Some(MainPhaseAction::Pass),
            PlayerAction::CastSpell(play) => playable
                .contains(&play)
                .then_some(MainPhaseAction::Play(play)),
            PlayerAction::ActivateMana(card_id, ability_index, express_choice) => tappable_lands
                .contains(&card_id)
                .then_some(MainPhaseAction::ActivateMana(
                    card_id,
                    ability_index,
                    express_choice,
                )),
            PlayerAction::UndoMana(card_id) => untappable_lands
                .contains(&card_id)
                .then_some(MainPhaseAction::UntapMana(card_id)),
            PlayerAction::ActivateAbility(ability) => activatable
                .iter()
                .copied()
                .find(|(card_id, index)| {
                    *card_id == ability.card_id && *index == ability.ability_index
                })
                .map(|(card_id, index)| MainPhaseAction::ActivateAbility(card_id, index)),
            _ => None,
        }
    }

    pub fn to_target_choice(self) -> Option<TargetChoice> {
        match self {
            PlayerAction::SelectCard(card_id) => Some(TargetChoice::Card(card_id)),
            PlayerAction::SelectPlayer(player_id) => Some(TargetChoice::Player(player_id)),
            PlayerAction::TargetEntity(TargetEntity::Card(card_id)) => {
                Some(TargetChoice::Card(card_id))
            }
            PlayerAction::TargetEntity(TargetEntity::Player(player_id)) => {
                Some(TargetChoice::Player(player_id))
            }
            PlayerAction::FinishTargeting => Some(TargetChoice::None),
            _ => None,
        }
    }

    pub fn run<A: PlayerAgent + ?Sized>(
        self,
        _controller: &mut PlayerController<'_, A>,
        playable: &[PlayOption],
        tappable_lands: &[CardId],
        untappable_lands: &[CardId],
        activatable: &[(CardId, usize)],
    ) -> PlayerActionOutcome {
        if let Some(priority) =
            self.to_priority_action(playable, tappable_lands, untappable_lands, activatable)
        {
            return PlayerActionOutcome::Priority(priority);
        }
        if let Some(target) = self.to_target_choice() {
            return PlayerActionOutcome::Target(target);
        }
        PlayerActionOutcome::Pending
    }
}