forge-foundation 0.3.0

Core Magic types for the manabrew engine: colors, mana, card types, zones, phases
Documentation
use rand::seq::SliceRandom;
use rand::Rng;

use super::paper_card::PaperCard;
use super::sealed_template::SealedTemplate;
use super::unopened_product::IUnOpenedProduct;
use crate::edition::EditionsRegistry;

pub struct ChaosBoosterSupplier {
    sets: Vec<(String, SealedTemplate, Vec<PaperCard>)>,
    bag: Vec<usize>,
}

impl ChaosBoosterSupplier {
    pub fn new(sets: Vec<(String, SealedTemplate, Vec<PaperCard>)>) -> Self {
        assert!(
            !sets.is_empty(),
            "ChaosBoosterSupplier needs at least one set to draw from"
        );
        Self {
            sets,
            bag: Vec::new(),
        }
    }

    pub fn from_codes<F>(editions: &EditionsRegistry, codes: &[&str], pool_for: F) -> Self
    where
        F: Fn(&str) -> Vec<PaperCard>,
    {
        let mut sets = Vec::new();
        for code in codes {
            let edition = match editions.get(code) {
                Some(e) => e,
                None => {
                    eprintln!("[chaos_booster] edition `{code}` not registered");
                    continue;
                }
            };
            let template = match edition.to_sealed_template() {
                Some(t) => t,
                None => {
                    eprintln!("[chaos_booster] edition `{code}` has no Booster recipe");
                    continue;
                }
            };
            let pool = pool_for(code);
            sets.push((code.to_string(), template, pool));
        }
        Self::new(sets)
    }

    pub fn set_count(&self) -> usize {
        self.sets.len()
    }

    pub fn bag_empty(&self) -> bool {
        self.bag.is_empty()
    }

    fn refill_bag<R: Rng + ?Sized>(&mut self, rng: &mut R) {
        self.bag = (0..self.sets.len()).collect();
        self.bag.shuffle(rng);
    }
}

impl IUnOpenedProduct for ChaosBoosterSupplier {
    fn open<R: Rng + ?Sized>(&mut self, rng: &mut R) -> Vec<PaperCard> {
        if self.bag.is_empty() {
            self.refill_bag(rng);
        }
        let idx = self.bag.pop().expect("bag refilled but still empty");
        let (_code, template, pool) = &self.sets[idx];
        super::booster_generator::BoosterGenerator::get_booster_pack(template, pool, rng)
    }
}

#[cfg(test)]
mod tests {
    use super::super::rarity::Rarity;
    use super::*;
    use rand::rngs::StdRng;
    use rand::SeedableRng;

    fn pool() -> Vec<PaperCard> {
        let mut v = Vec::new();
        for i in 0..30 {
            v.push(PaperCard::new(
                format!("Common {i}"),
                "TST",
                format!("c{i}"),
                Rarity::Common,
            ));
        }
        for i in 0..6 {
            v.push(PaperCard::new(
                format!("Rare {i}"),
                "TST",
                format!("r{i}"),
                Rarity::Rare,
            ));
        }
        v
    }

    #[test]
    fn bag_visits_each_set_before_repeating() {
        let mut tpl = SealedTemplate::generic_no_slot_booster();
        tpl.foil_chance = 0.0;
        tpl.foil_type = super::super::foil_type::FoilType::NotSupported;
        let sets = vec![
            ("AAA".to_string(), tpl.clone(), pool()),
            ("BBB".to_string(), tpl.clone(), pool()),
            ("CCC".to_string(), tpl, pool()),
        ];
        let mut supp = ChaosBoosterSupplier::new(sets);
        let mut rng = StdRng::seed_from_u64(0);
        for _ in 0..3 {
            assert_eq!(supp.open(&mut rng).len(), 15);
        }
        assert!(
            supp.bag_empty(),
            "bag should be drained after one set per pack"
        );
    }
}