Skip to main content

ygopro_data/data/
deck.rs

1//! A deck of cards.
2//!
3//! Provides the [`Deck`] container and the deck-parsing logic.
4
5use std::collections::HashMap;
6use std::convert::Infallible;
7use std::fmt::Display;
8use std::str::FromStr;
9
10use binrw::BinRead;
11use binrw::BinWrite;
12use binrw::binrw;
13use modular_bitfield::Specifier;
14use modular_bitfield::bitfield;
15use num_enum::IntoPrimitive;
16use num_enum::TryFromPrimitive;
17
18use crate::constants::OT;
19use crate::constants::Rule;
20use crate::constants::Type;
21use crate::data::Card;
22use crate::data::LFList;
23
24const DECK_MIN: usize = 40;
25const DECK_MAX: usize = 60;
26const EXTRA_MAX: usize = 15;
27const SIDE_MAX: usize = 15;
28
29/// Player Deck.
30#[binrw]
31#[derive(Debug, Clone, Default)]
32pub struct Deck {
33    #[bw(calc = main.len() as u32 + extra.len() as u32)]
34    main_size: u32,
35    #[bw(calc = side.len() as u32)]
36    side_size: u32,
37    #[br(count = main_size)]
38    pub main: Vec<u32>,
39    #[br(count = side_size)]
40    pub side: Vec<u32>,
41    #[br(ignore)]
42    pub extra: Vec<u32>,
43}
44
45impl Deck {
46    /// Create an empty deck.
47    pub fn new() -> Self { Self::default() }
48
49    /// Build a deck from raw card codes, splitting into main and side.
50    pub fn load_from_codes(codes: &[u32], mainc: usize, sidec: usize) -> Self {
51        let mut d = Self::new();
52        let mc = mainc.min(codes.len());
53        d.main.extend_from_slice(&codes[..mc]);
54        let sc = sidec.min(codes.len().saturating_sub(mc));
55        d.side.extend_from_slice(&codes[mc..mc + sc]);
56        d
57    }
58
59    /// Count each card code across main, extra, and side.
60    pub fn get_hash(&self) -> HashMap<u32, usize> {
61        let mut counts: HashMap<u32, usize> = HashMap::new();
62        for &code in self.main.iter().chain(self.extra.iter()).chain(self.side.iter()) {
63            *counts.entry(code).or_insert(0) += 1;
64        }
65        counts 
66    }
67
68    /// Drop unknown/token cards and move extra-deck cards into the extra.
69    pub fn load<'a>(&mut self, resolve_card: impl Fn(u32) -> Option<&'a Card>) -> Option<DeckError> {
70        let response = remove_unknown_cards(&mut self.main, |c| resolve_card(c).map(|c| c.card_type))
71            .or(remove_unknown_cards(&mut self.side, |c| resolve_card(c).map(|c| c.card_type)));
72        self.separate(|c| resolve_card(c).map(|c| c.card_type).unwrap_or(Type::empty()));
73        response
74    }
75
76    /// Check the deck against the limit list and the rule.
77    pub fn prepare<'a>(&mut self, lflist: &LFList, rule: Rule, resolve_card: impl Fn(u32) -> Option<&'a Card>) -> Result<(), DeckError> {
78        self.check(lflist, rule, 
79            |c| resolve_card(c).map(|c| c.ot).unwrap_or(OT::empty()), 
80            |c| resolve_card(c).map(|c| c.card_type).unwrap_or(Type::empty()),
81            |c| resolve_card(c).map(|c| c.duel_code()).unwrap_or(0))
82    }
83
84    /// Check a deck after replacing side, comparing against this deck.
85    pub fn check_after_replacing_side<'a>(&self, deck: &mut Deck, resolve_card: impl Fn(u32) -> Option<&'a Card>) -> Result<(), DeckError> {
86        deck.separate(|c| resolve_card(c).map(|c| c.card_type).unwrap_or(Type::empty()));
87        if self == deck {
88            Ok(())
89        } else {
90            Err(DeckError::new().with_error_type(DeckErrorType::SideCount))
91        }
92    }
93
94    /// Move extra-deck cards from main into the extra.
95    pub fn separate(&mut self, resolve_type: impl Fn(u32) -> Type) {
96        separate_main_and_extra(&mut self.main, &mut self.extra, resolve_type);
97    }
98
99    /// Run all the deck checks (length, illegal cards, rule, limit list).
100    pub fn check(&self, lflist: &LFList, rule: Rule, get_rule: impl Fn(u32) -> OT, get_type: impl Fn(u32) -> Type, resolve_code: impl Fn(u32) -> u32) -> Result<(), DeckError> {
101        check_deck_length(&self.main, &self.extra, &self.side)?;
102        check_illegal_cards(&self.main, &self.side, &self.extra, get_type)?;
103        let iter = self.main.iter().chain(self.extra.iter()).chain(self.side.iter());
104        check_rule(iter.clone(), rule, get_rule)?;
105        check_deck_lflists(iter, lflist, resolve_code)
106    }
107}
108
109impl ToString for Deck {
110    fn to_string(&self) -> String {
111        let mut text = String::from("#ygopro-rs deck generated\n#main\n");
112        for code in &self.main {
113            text.push_str(&code.to_string());
114            text.push('\n');
115        }
116        if self.extra.len() > 0 {
117            text.push_str("#extra\n");
118            for code in &self.extra {
119                text.push_str(&code.to_string());
120                text.push('\n');
121            }
122        }
123        text.push_str("!side\n");
124        for code in &self.side {
125            text.push_str(&code.to_string());
126            text.push('\n');
127        }
128        text
129    }
130}
131
132impl FromStr for Deck {
133    type Err = Infallible;
134
135    fn from_str(s: &str) -> Result<Self, Self::Err> {
136        let mut deck = Self::new();
137        let mut section = &mut deck.main;
138        for line in s.lines() {
139            let line = line.trim();
140            if line.is_empty() { continue; }
141            match line.as_bytes()[0] {
142                b'!' => section = &mut deck.side,
143                b'#' => match line {
144                    "#main" => section = &mut deck.main,
145                    "#extra" => section = &mut deck.extra,
146                    _ => {}
147                },
148                b'0'..=b'9' => {
149                    let code_end = line.find(|c: char| !c.is_ascii_digit()).unwrap_or(line.len());
150                    if let Ok(code) = line[..code_end].parse::<u32>() {
151                        section.push(code);
152                    }
153                }
154                _ => {}
155            }
156        }
157        Ok(deck)
158    }
159}
160
161impl PartialEq for Deck {
162    fn eq(&self, other: &Self) -> bool {
163        if self.main.len() != other.main.len() 
164            || self.side.len() != other.side.len()
165            || self.extra.len() != other.extra.len() {
166            return false;
167        }
168        self.get_hash() == other.get_hash()
169    }
170}
171
172impl Eq for Deck {}
173
174/// The kind of deck error.
175#[derive(Specifier, Clone, Copy, Debug, IntoPrimitive, TryFromPrimitive, PartialEq, Eq)]
176#[bits = 4]
177#[repr(u8)]
178pub enum DeckErrorType {
179    /// Violates the limit list.
180    Lflist = 0x1,
181    /// Contains card only available in OCG.
182    OcgOnly = 0x2,
183    /// Contains card only available in TCG.
184    TcgOnly = 0x3,
185    /// Contains unknown card.
186    UnknownCard = 0x4,
187    /// Too many copies of a card.
188    CardCount = 0x5,
189    /// The main deck size is wrong.
190    MainCount = 0x6,
191    /// The extra deck size is wrong.
192    ExtraCount = 0x7,
193    /// The side deck size is wrong.
194    SideCount = 0x8,
195    /// A card is not available.
196    NotAvailable = 0x9,
197}
198
199/// A deck error, packing an error type and the offending card code.
200#[bitfield]
201#[derive(BinRead, BinWrite, Debug, Clone, Copy, PartialEq, Eq)]
202#[br(map = Self::from_bytes)]
203#[bw(map = |&x| Self::into_bytes(x))]
204#[repr(u32)]
205pub struct DeckError {
206    /// The offending card code.
207    pub code: modular_bitfield::specifiers::B28,
208    /// The error type.
209    pub error_type: DeckErrorType,
210}
211
212impl Display for DeckError {
213    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214        write!(f, "DeckError({:?}, code: {})", self.error_type(), self.code())
215    }
216}
217
218impl std::error::Error for DeckError {}
219
220const EXTRA_TYPE: Type = Type::from_bits_retain(0x4802040);
221
222/// Move extra-deck cards from `main` into `ex`.
223pub fn separate_main_and_extra(main: &mut Vec<u32>, ex: &mut Vec<u32>, resolve_type: impl Fn(u32) -> Type) {
224    main.retain(|&code| {
225        if resolve_type(code).intersects(EXTRA_TYPE) {
226            if ex.len() < EXTRA_MAX { ex.push(code); }
227            false
228        } else {
229            true
230        }
231    });
232}
233
234/// Check the main, extra, and side deck sizes.
235pub fn check_deck_length(main: &[u32], extra: &[u32], side: &[u32]) -> Result<(),DeckError> {
236    if main.len() < DECK_MIN || main.len() > DECK_MAX { return Err(DeckError::new().with_error_type(DeckErrorType::MainCount).with_code(main.len() as u32)); }
237    if extra.len() > EXTRA_MAX { return Err(DeckError::new().with_error_type(DeckErrorType::ExtraCount).with_code(extra.len() as u32)); }
238    if side.len() > SIDE_MAX { return Err(DeckError::new().with_error_type(DeckErrorType::SideCount).with_code(side.len() as u32)); }
239    Ok(())
240}
241
242/// Remove unknown or token cards, returning an error for the last removed card.
243pub fn remove_unknown_cards(main: &mut Vec<u32>, get_type: impl Fn(u32) -> Option<Type>) -> Option<DeckError> {
244    let mut last_removed_code = None;
245    main.retain(|code| {
246        let _type = get_type(*code);
247        if match _type {
248            Some(_type) => _type.contains(Type::Token),
249            None => true
250        } {
251            last_removed_code = Some(*code);
252            false
253        } else { true }
254    });
255    last_removed_code.map(|code| DeckError::new().with_error_type(DeckErrorType::UnknownCard).with_code(code))
256}
257
258/// Check that no illegal card (token, or a card in the wrong section) is present.
259pub fn check_illegal_cards(main: &Vec<u32>, side: &Vec<u32>, ex: &Vec<u32>, get_type: impl Fn(u32) -> Type) -> Result<(), DeckError> {
260    for code in main {
261        let card_type = get_type(*code);
262        if card_type.contains(Type::Token) || card_type.intersects(EXTRA_TYPE) {
263            return Err(DeckError::new().with_error_type(DeckErrorType::MainCount).with_code(0));
264        }
265    }
266    for code in side {
267        if get_type(*code).contains(Type::Token) {
268            return Err(DeckError::new().with_error_type(DeckErrorType::SideCount).with_code(0));
269        }
270    }
271    for code in ex {
272        let card_type = get_type(*code);
273        if card_type.contains(Type::Token) || !card_type.intersects(EXTRA_TYPE) {
274            return Err(DeckError::new().with_error_type(DeckErrorType::ExtraCount).with_code(0));
275        }
276    }
277    Ok(())
278}
279
280/// Check that every card is allowed by the rule's availability.
281pub fn check_rule<'a>(codes: impl Iterator<Item = &'a u32>, rule: Rule, get_rule: impl Fn(u32) -> OT) -> Result<(), DeckError> {
282    for &code in codes {
283        let ot = get_rule(code);
284        if let Some(error_type) = rule.check_ot(ot) {
285            return Err(DeckError::new().with_error_type(error_type).with_code(code));
286        }
287    }
288    Ok(())
289}
290
291/// Check the card counts against the limit list.
292pub fn check_deck_lflists<'a>(codes: impl Iterator<Item = &'a u32>, lflist: &LFList, resolve_code: impl Fn(u32) -> u32) -> Result<(), DeckError> {
293    let mut counts: HashMap<u32, u32> = HashMap::new();
294    for &code in codes {
295        let resolved = resolve_code(code);
296        *counts.entry(resolved).or_insert(0) += 1;
297    }
298
299    let mut current = 0;
300    for (&code, &count) in &counts {
301        if count > 3 {
302            return Err(DeckError::new().with_error_type(DeckErrorType::CardCount).with_code(code));
303        }
304        if lflist.genesys > 0 && let Some(&limit) = lflist.glist.get(&code) {
305            current += limit * count;
306            if current > lflist.genesys {
307                return Err(DeckError::new().with_error_type(DeckErrorType::Lflist).with_code(code));
308            }
309        }
310        if let Some(&limit) = lflist.content.get(&code)
311            && count as u8 > limit {
312                return Err(DeckError::new().with_error_type(DeckErrorType::Lflist).with_code(code));
313        }
314    }
315    Ok(())
316}
317
318#[cfg(test)]
319mod tests {
320    use crate::data::Deck;
321
322    #[test]
323    fn splits_main_and_side_at_bang_marker() {
324        let deck: Deck = "#created by test\n#main\n123\n456\n#extra\n789\n!side\n111\n222\n"
325            .parse()
326            .unwrap();
327        assert_eq!(deck.main, vec![123, 456, 789]);
328        assert!(deck.extra.is_empty());
329        assert_eq!(deck.side, vec![111, 222]);
330    }
331
332    #[test]
333    fn drops_comment_blank_and_invalid_lines() {
334        let deck: Deck = "   \n#comment\n123abc\nnot-a-number\n!side\n\nxyz\n".parse().unwrap();
335        assert_eq!(deck.main, vec![123]);
336        assert!(deck.side.is_empty());
337    }
338
339    #[test]
340    fn round_trips_through_to_string() {
341        let deck: Deck = "#main\n1\n2\n3\n!side\n4\n".parse().unwrap();
342        let text = deck.to_string();
343        let reparsed: Deck = text.parse().unwrap();
344        assert_eq!(reparsed.main, deck.main);
345        assert_eq!(reparsed.extra, deck.extra);
346        assert_eq!(reparsed.side, deck.side);
347    }
348}