Skip to main content

manabrew_engine/combat/
attacking_band.rs

1use crate::ids::CardId;
2
3/// A band of attacking creatures.
4/// Mirrors Java's `AttackingBand.java`.
5///
6/// In standard play, each band contains exactly one creature. The banding
7/// keyword (from Alpha/early sets) allows multiple creatures to form a single
8/// band. We store the full band structure for parity, but `isValidBand` only
9/// handles single-creature bands and explicit banding — "bands with" is
10/// stubbed.
11#[derive(Debug, Clone)]
12pub struct AttackingBand {
13    pub attackers: Vec<CardId>,
14    /// `None` = not yet determined, `Some(true)` = blocked, `Some(false)` = unblocked.
15    pub blocked: Option<bool>,
16}
17
18impl AttackingBand {
19    pub fn new(card: CardId) -> Self {
20        Self {
21            attackers: vec![card],
22            blocked: None,
23        }
24    }
25
26    pub fn from_list(cards: Vec<CardId>) -> Self {
27        Self {
28            attackers: cards,
29            blocked: None,
30        }
31    }
32
33    pub fn get_attackers(&self) -> &[CardId] {
34        &self.attackers
35    }
36
37    pub fn add_attacker(&mut self, card: CardId) {
38        self.attackers.push(card);
39    }
40
41    pub fn remove_attacker(&mut self, card: CardId) {
42        self.attackers.retain(|&c| c != card);
43    }
44
45    /// Check if `card` is part of this band.
46    pub fn contains(&self, card: CardId) -> bool {
47        self.attackers.contains(&card)
48    }
49
50    pub fn is_blocked(&self) -> Option<bool> {
51        self.blocked
52    }
53
54    pub fn set_blocked(&mut self, value: bool) {
55        self.blocked = Some(value);
56    }
57
58    pub fn is_empty(&self) -> bool {
59        self.attackers.is_empty()
60    }
61
62    /// Validate that a band is legal. For starting a band (`share_damage` =
63    /// false), all but one creature must have Banding. For sharing damage
64    /// (`share_damage` = true), at least one must have Banding.
65    ///
66    /// Full "bands with" keyword support is stubbed — returns `true` for
67    /// single-creature bands, which covers 99.9% of actual play.
68    pub fn is_valid_band(band: &[CardId], cards: &[crate::card::Card], share_damage: bool) -> bool {
69        if band.is_empty() {
70            return false;
71        }
72        if band.len() == 1 {
73            return true;
74        }
75        // Count creatures with the Banding keyword
76        let banding_count = band
77            .iter()
78            .filter(|&&cid| {
79                let card = &cards[cid.index()];
80                card.has_keyword("Banding")
81            })
82            .count();
83        let needed = if share_damage { 1 } else { band.len() - 1 };
84        banding_count >= needed
85    }
86
87    /// Check if `card` can join this existing band.
88    pub fn can_join_band(&self, card: CardId, cards: &[crate::card::Card]) -> bool {
89        let mut new_band: Vec<CardId> = self.attackers.clone();
90        new_band.push(card);
91        Self::is_valid_band(&new_band, cards, false)
92    }
93}
94
95impl std::fmt::Display for AttackingBand {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        let blocked_str = match self.blocked {
98            None => " ? ",
99            Some(true) => ">||",
100            Some(false) => ">>>",
101        };
102        write!(f, "{:?} {}", self.attackers, blocked_str)
103    }
104}