use mino::matrix::{MatBuf, COLUMNS}; use rand::Rng; use std::ops::RangeInclusive; /// Manages the current level of garbage and the remaining garbage lines. pub struct Garbage { cheese: std::iter::Take>, // current level of garbage on the matrix level: i16, // min/max garbage to insert on the matrix range: RangeInclusive, } impl Garbage { /// Constructs a new garbage generator. /// /// - `rng`: random number source /// - `count`: total number of garbage rows to insert /// - `range`: min/max amount of garbage on the matrix at a given time pub fn new(rng: R, count: usize, range: RangeInclusive) -> Self { Self { cheese: Cheese::new(rng).take(count), level: 0, range, } } /// Signals that a line clear happened at row `min_y`. This is necessary to determine /// how much garbage needs to be inserted. Returns the number of garbage lines that /// were removed from this line clear. pub fn clear(&mut self, min_y: i16) -> usize { if min_y < self.level { let removed = (self.level - min_y) as usize; self.level = min_y; removed } else { 0 } } /// Inserts new garbage lines to the bottom of `mat`. If `comboing` is `true` then /// less garbage lines are inserted (like jstris's cheese race mechanics). Returns the /// number of new garbage lines inserted. pub fn insert(&mut self, mat: &mut MatBuf, comboing: bool) -> usize { let target = if comboing { *self.range.start() } else { *self.range.end() }; let difference = target.saturating_sub(self.level as usize); (&mut self.cheese) .take(difference) .map(|col| { mat.shift_up(); mat.fill_row(0, garbage_row(col)); self.level += 1; }) .count() } } fn garbage_row(col: i16) -> u16 { !(1 << col) } struct Cheese { rng: R, col: i16, } impl Cheese { fn new(mut rng: R) -> Self { let col = rng.gen_range(0..COLUMNS); Self { rng, col } } } impl Iterator for Cheese { type Item = i16; #[inline] fn next(&mut self) -> Option { self.col += self.rng.gen_range(1..COLUMNS); self.col %= COLUMNS; Some(self.col) } } #[cfg(test)] mod tests { use super::*; use mino::mat; #[test] fn test_garbage_row() { let mat = mat! { "xxxxxxxxx."; "xxxxx.xxxx"; "xx.xxxxxxx"; ".xxxxxxxxx"; }; assert_eq!(mat[0], garbage_row(0)); assert_eq!(mat[1], garbage_row(2)); assert_eq!(mat[2], garbage_row(5)); assert_eq!(mat[3], garbage_row(9)); } #[test] fn test_cheese_no_duplicates() { for _ in 0..50 { let cheese = Cheese::new(rand::thread_rng()); let mut prev = None; let mut history = std::collections::HashSet::new(); for x1 in cheese.take(500) { if let Some(x0) = prev { assert_ne!(x0, x1); } prev = Some(x1); history.insert(x1); } assert_eq!(history.len(), COLUMNS as usize); for x in 0..COLUMNS { assert!(history.contains(&x)); } } } #[test] fn test_garbage_insert() { let mut mat = MatBuf::new(); let mut garbage_left = 15; let mut downstacked = 0; let mut garbage = Garbage::new(rand::thread_rng(), garbage_left, 3..=9); garbage_left -= garbage.insert(&mut mat, true); assert_eq!(garbage_left, 12); assert_eq!(mat.rows(), 3); let m0 = mat[0]; let m1 = mat[1]; let m2 = mat[2]; garbage_left -= garbage.insert(&mut mat, false); assert_eq!(garbage_left, 6); assert_eq!(mat.rows(), 9); assert_ne!(mat[5], m0); assert_eq!(mat[6], m0); assert_eq!(mat[7], m1); assert_eq!(mat[8], m2); mat.fill_row(9, mino::matrix::EMPTY_ROW | 0b1); assert_eq!(garbage.insert(&mut mat, false), 0); assert_eq!(mat.rows(), 10); mat.fill_row(7, mino::matrix::FULL_ROW); mat.fill_row(8, mino::matrix::FULL_ROW); assert_eq!(mat.clear_lines(), 7..9); assert_eq!(mat.rows(), 8); downstacked += garbage.clear(7); assert_eq!(downstacked, 2); garbage_left -= garbage.insert(&mut mat, false); assert_eq!(garbage_left, 4); assert_eq!(mat.rows(), 10); mat.clear(); downstacked += garbage.clear(0); assert_eq!(downstacked, 11); garbage_left -= garbage.insert(&mut mat, false); assert_eq!(garbage_left, 0); assert_eq!(mat.rows(), 4); mat.clear(); downstacked += garbage.clear(0); assert_eq!(downstacked, 15); assert_eq!(garbage.insert(&mut mat, false), 0); assert_eq!(mat.rows(), 0); } }