diff --git a/mino/src/lib.rs b/mino/src/lib.rs index 3eac33b..5b9ff66 100644 --- a/mino/src/lib.rs +++ b/mino/src/lib.rs @@ -3,10 +3,12 @@ pub mod input; pub mod matrix; pub mod piece; +pub mod queue; pub use input::Movement; pub use matrix::{Mat, MatBuf}; pub use piece::{Loc, Piece, Rot}; +pub use queue::Queue; #[cfg(feature = "srs")] pub mod srs; diff --git a/mino/src/queue.rs b/mino/src/queue.rs new file mode 100644 index 0000000..65747b0 --- /dev/null +++ b/mino/src/queue.rs @@ -0,0 +1,139 @@ +//! Data structure to represent the "queue", which encompasses the current piece, the next +//! previews, and the hold slot. The data structure attempts to remove ambiguity between +//! queues that have the same two pieces immediately available, by automatically moving +//! the front of the queue into the hold slot if the hold slot would be empty. + +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct Queue<'a, T: Copy> { + pub hold: Option, + pub next: &'a [T], +} + +impl<'a, T: Copy> Queue<'a, T> { + /// Constructs a new queue. + pub fn new(mut hold: Option, mut next: &'a [T]) -> Self { + if hold.is_none() && !next.is_empty() { + hold = Some(next[0]); + next = &next[1..]; + } + Self { hold, next } + } + + /// Returns `true` if the queue is completely empty (i.e., `reachable()` would yield + /// no values). + pub fn is_empty(&self) -> bool { + // HACK: its impossible for `next` to be non-empty and `hold` to be None, because + // if `next` was non-empty then the `new` constructor would automatically move + // some value into the hold slot. + debug_assert!(self.hold.is_some() || self.next.is_empty()); + self.hold.is_none() /* && self.next.is_empty() */ + } + + /// Returns the number of total pieces in the queue, including the hold slot. This + /// equals the number of times `remove` may be called before the queue becomes empty. + pub fn len(&self) -> usize { + self.hold.map_or(0, |_| 1) + self.next.len() + } + + /// Yields the next pieces available to use from the queue. The returned iterator may + /// be empty, and may yield at most 2 elements. + pub fn reachable(&self) -> impl Iterator { + let front = self.next.get(0).copied(); + [front, self.hold].into_iter().flatten() + } +} + +impl<'a, T: Copy + Eq> Queue<'a, T> { + /// Removes the given piece from the front of the queue, returning a new queue with + /// that piece removed. + /// + /// Panics if the piece is not actually reachable from this queue. + pub fn remove(&self, val: T) -> Self { + assert!(!self.is_empty(), "trying to remove from empty queue"); + if self.hold == Some(val) { + Queue::new(None, self.next) + } else { + assert!(self.next[0] == val, "trying to remove unreachable item"); + Queue::new(self.hold, &self.next[1..]) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloc::vec::Vec; + use core::fmt::Debug; + + #[test] + fn test_queue_new_eq() { + assert_eq!( + Queue::new(None, &["A", "B", "C"]), + Queue::new(Some("A"), &["B", "C"]) + ); + assert_eq!(Queue::new(None, &["A"]), Queue::new(Some("A"), &[])); + } + + #[test] + fn test_queue_empty() { + assert!(!Queue::new(None, &["A", "B", "C"]).is_empty()); + assert!(!Queue::new(None, &["A"]).is_empty()); + assert!(!Queue::new(Some("A"), &[]).is_empty()); + assert!(Queue::new(None, &[] as &[&str]).is_empty()); + } + + fn assert_reachable(q: &Queue, ts: impl IntoIterator) { + let mut ts = ts.into_iter().collect::>(); + for t in q.reachable() { + let idx = ts + .iter() + .enumerate() + .find_map(|(i, &u)| (t == u).then_some(i)); + if let Some(idx) = idx { + ts.remove(idx); + } else { + panic!("{t:?} found but not expected"); + } + } + for t in ts { + panic!("{t:?} expected but not found"); + } + } + + #[test] + fn test_queue_reachable() { + assert_reachable(&Queue::new(None, &[] as &[&str]), []); + assert_reachable(&Queue::new(None, &["A", "B", "C"]), ["A", "B"]); + assert_reachable(&Queue::new(Some("D"), &[]), ["D"]); + assert_reachable(&Queue::new(Some("D"), &["A"]), ["A", "D"]); + assert_reachable(&Queue::new(Some("D"), &["A", "B", "C"]), ["A", "D"]); + } + + #[test] + fn test_queue_remove() { + assert_eq!( + Queue::new(None, &["A", "B", "C"]).remove(&"A"), + Queue::new(Some("B"), &["C"]) + ); + assert_eq!( + Queue::new(Some("D"), &["A", "B", "C"]).remove(&"A"), + Queue::new(Some("D"), &["B", "C"]) + ); + assert_eq!( + Queue::new(None, &["A", "B", "C"]).remove(&"B"), + Queue::new(None, &["A", "C"]) + ); + } + + #[test] + #[should_panic] + fn test_queue_bad_remove_1() { + Queue::new(None, &["A", "B", "C"]).remove(&"C"); + } + + #[test] + #[should_panic] + fn test_queue_bad_remove_2() { + Queue::new(Some("D"), &["A", "B", "C"]).remove(&"B"); + } +} diff --git a/mino/src/srs.rs b/mino/src/srs.rs index 9a853a7..4b98c74 100644 --- a/mino/src/srs.rs +++ b/mino/src/srs.rs @@ -116,6 +116,7 @@ impl core::fmt::Display for InvalidPieceName { } pub type Piece = crate::piece::Piece; +pub type Queue<'a> = crate::queue::Queue<'a, PieceType>; #[cfg(test)] mod test {