//! Data structures and operations on tetrix matrices. The matrix types in this module are //! uncolored, since color information is generally not used by bots and many operations //! can be made much more efficient by not tracking it. use core::cmp::{max, min}; use core::ops::{Range, RangeFull}; /// Number of columns in a matrix. pub const COLUMNS: i16 = 10; /// Bitboard representation of a full row. pub const FULL_ROW: u16 = u16::MAX; /// Bitboard representation of an empty row. pub const EMPTY_ROW: u16 = !((1u16 << COLUMNS) - 1); /// Represents a "slice" to a tetris matrix bitboard. /// /// Bitboards are represented rowwise, with each row represented by a u16 holding bits /// describing if a cell is occupied (1) or not (0). The least significant bit describes /// the leftmost column, and the most significant bits are all set, even past the logical /// end of the matrix (columns 11+). /// /// Logically, a matrix is considered infinite in every direction. Cells below the bottom /// or beyond the left/right sides are all considered occupied. Cells above the physical /// top of the matrix are considered unoccupied. #[derive(Eq, PartialEq, Hash)] #[repr(transparent)] pub struct Mat([u16]); impl Mat { /// Constructs a new matrix from a slice of rowwise bitboard data. pub const fn new(data: &[u16]) -> &Self { if data.len() >= i16::MAX as usize { panic!("matrix height overflows i16"); } unsafe { core::mem::transmute(data) } } pub const EMPTY: &'static Self = Self::new(&[]); /// Returns the number of columns in the matrix. This always returns `COLUMNS`. #[inline] pub const fn cols(&self) -> i16 { COLUMNS } /// Returns the number of rows in the matrix. #[inline] pub const fn rows(&self) -> i16 { // XXX(iitalics): this is guarunteed not to wrap, since we check len in `new`. self.0.len() as i16 } /// Returns true if the cells in row `y` selected by `mask` match the pattern `test`. #[inline] pub fn test_row(&self, y: i16, mask: u16, test: u16) -> bool { (self[y] & mask) == test } /// Returns true if the cell at `(x,y)` is occupied. #[inline] pub fn get(&self, x: i16, y: i16) -> bool { if (0..COLUMNS).contains(&x) { self.test_row(y, 1 << x, 1 << x) } else { true } } } impl AsRef for Mat { fn as_ref(&self) -> &Mat { self } } // `mat[y]` = bit pattern of row `y` impl core::ops::Index for Mat { type Output = u16; fn index(&self, y: i16) -> &u16 { if y < 0 { &FULL_ROW } else { self.0.get(y as usize).unwrap_or(&EMPTY_ROW) } } } // `mat[y0..y1]` = bit patterns of rows in range impl core::ops::Index> for Mat { type Output = [u16]; fn index(&self, r: Range) -> &[u16] { let y1 = min(max(r.end, 0), self.rows()); let y0 = min(max(r.start, 0), y1); &self.0[y0 as usize..y1 as usize] } } // `mat[..]` = bit patterns of all rows impl core::ops::Index for Mat { type Output = [u16]; fn index(&self, _: RangeFull) -> &[u16] { &self.0 } } impl core::fmt::Debug for Mat { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "mat!{{")?; let mut sep = ""; for y in (0..self.rows()).rev() { write!(f, "{sep}\"")?; for x in 0..self.cols() { let occ = self.get(x, y); f.write_str(if occ { "x" } else { "." })?; } write!(f, "\"")?; sep = ";"; } write!(f, "}}") } } #[cfg(any(feature = "ascii", test))] #[doc(hidden)] pub mod __ascii { use super::*; pub const fn parse(strs: [&str; N]) -> [u16; N] { let mut data = [EMPTY_ROW; N]; let mut i = 0; while i < N { let row = strs[i].as_bytes(); if row.len() != COLUMNS as usize { panic!("wrong number of columns in ascii row"); } let y = N - i - 1; let mut x = 0i16; while x < COLUMNS { match row[x as usize] { b'.' | b'_' | b' ' => {} _ => data[y] |= 1 << x, } x += 1; } i += 1; } data } } /// Matrix bitboard backed by a static u16 array of a given length. This type allows /// writable operations such as changing if a cell is occupied or not, and growing or /// shrinking the number of rows as long as it stays within the underlying capacity. /// /// [`MatBuf`] implements [`Deref`], so it automatically inherits the methods of [`Mat`]. #[derive(Clone)] pub struct MatBuf { rows: i16, buf: [u16; LEN], } impl MatBuf { /// Returns a new empty [`MatBuf`] that is able to grow up to 40 rows. pub fn new() -> Self { Self::default() } } impl MatBuf { /// Returns a read-only view of this matrix. #[inline] pub fn as_mat(&self) -> &Mat { Mat::new(&self.buf[0..self.rows as usize]) } /// Modifies the cells in this matrix to be identical to those in `mat`. /// /// Panics if the buffer space cannot fit the rows of `mat`. #[inline] pub fn copy_from(&mut self, mat: &Mat) { let data = &mat[..]; if data.len() > LEN { panic!("matrix cannot fit in available buffer space"); } self.rows = mat.rows(); self.buf[..data.len()].copy_from_slice(data); } /// Resets the matrix so it is empty. #[inline] pub fn clear(&mut self) { self.copy_from(Mat::EMPTY) } /// Modifies the cells in row `y` to have additional cells occupied according to the /// bit pattern in `mask`. /// /// Panics if the buffer space cannot fit the desired row. #[inline] pub fn fill_row(&mut self, y: i16, mask: u16) { if y >= 0 { self[y] |= mask; } } /// Fills in the cell at the given (x,y) coordinate. /// /// Panics if the buffer space cannot fit the desired cell. pub fn set(&mut self, x: i16, y: i16) { self.fill_row(y, if (0..COLUMNS).contains(&x) { 1 << x } else { 0 }) } /// Removes any rows that are completely filled, shifting rows above down. Returns the /// range of rows that were cleared. pub fn clear_lines(&mut self) -> Range { // TODO: this could be made faster if given the following assumptions: // - provide lowest y that may contain filled rows // - assume that filled rows must all be adjacent let mut dst = 0usize; let mut rng = self.rows..self.rows; for y in 0..self.rows { let i = y as usize; if self.buf[i] == FULL_ROW || self.buf[i] == EMPTY_ROW { rng.start = min(rng.start, y); rng.end = y + 1; } else { self.buf[dst] = self.buf[i]; dst += 1; } } self.rows = dst as i16; rng } pub fn shift_up(&mut self) { if self.rows as usize == LEN { panic!("not enough available buffer space to shift up"); } self.buf.copy_within(0..self.rows as usize, 1); self.buf[0] = EMPTY_ROW; self.rows += 1; } } impl Default for MatBuf { fn default() -> Self { Self { buf: [0; LEN], rows: 0, } } } impl core::ops::IndexMut for MatBuf { fn index_mut(&mut self, y: i16) -> &mut u16 { if y < 0 || y as usize >= LEN { panic!("row does not fit in available buffer space"); } if y >= self.rows { self.buf[self.rows as usize..(y + 1) as usize].fill(EMPTY_ROW); self.rows = y + 1; } &mut self.buf[y as usize] } } impl core::ops::IndexMut> for MatBuf { fn index_mut(&mut self, r: Range) -> &mut [u16] { // FIXME: should this expand the buffer like `IndexMut` does? let y1 = min(max(r.end, 0), self.rows); let y0 = min(max(r.start, 0), y1); &mut self.buf[y0 as usize..y1 as usize] } } impl core::ops::IndexMut for MatBuf { fn index_mut(&mut self, _: RangeFull) -> &mut [u16] { &mut self.buf[..self.rows as usize] } } // boilerplate impl's that all defer to `self.as_mat()` impl AsRef for MatBuf { #[inline] fn as_ref(&self) -> &Mat { self.as_mat() } } impl core::ops::Deref for MatBuf { type Target = Mat; fn deref(&self) -> &Mat { self.as_mat() } } impl> core::cmp::PartialEq for MatBuf { fn eq(&self, other: &Rhs) -> bool { *self.as_mat() == *other.as_ref() } } impl core::cmp::Eq for MatBuf {} impl core::fmt::Debug for MatBuf { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { self.as_mat().fmt(f) } } impl core::hash::Hash for MatBuf { fn hash(&self, state: &mut H) { self.as_mat().hash(state); } } impl core::ops::Index for MatBuf { type Output = u16; fn index(&self, y: i16) -> &u16 { &self.as_mat()[y] } } impl core::ops::Index> for MatBuf { type Output = [u16]; fn index(&self, r: Range) -> &[u16] { &self.as_mat()[r] } } impl core::ops::Index for MatBuf { type Output = [u16]; fn index(&self, _: RangeFull) -> &[u16] { &self.as_mat()[..] } } // TODO(?): MatVec, which is resizable // TODO(?): test_row(), fill_row(), clear_lines() made into traits #[cfg(test)] mod test { use super::*; use crate::mat; use alloc::vec::Vec; use core::ops::RangeInclusive; #[test] fn test_bit_constants() { for i in 0..16 { let m = 1u16 << i; let e = EMPTY_ROW & m != 0; let f = FULL_ROW & m != 0; assert!(f, "full, i={i}"); assert_eq!(e, i >= 10, "empty, i={i}"); } } const M1: &Mat = mat! { "...x....x."; "xx..xxxx.x"; }; const M2: &Mat = mat! { ".........x"; ".........x"; "x.......xx"; "xxxx.xxxxx"; }; #[test] fn test_dims() { assert_eq!(Mat::EMPTY.rows(), 0); assert_eq!(Mat::EMPTY.cols(), 10); assert_eq!(M1.rows(), 2); assert_eq!(M1.cols(), 10); assert_eq!(M2.rows(), 4); assert_eq!(M2.cols(), 10); } #[test] fn test_index() { assert_eq!(Mat::EMPTY[0], EMPTY_ROW); assert_eq!(Mat::EMPTY[1], EMPTY_ROW); assert_eq!(Mat::EMPTY[-1], FULL_ROW); // let m1_0 = 0b1011110011 | EMPTY_ROW; let m1_1 = 0b0100001000 | EMPTY_ROW; assert_eq!(M1[0], m1_0, "{:b}", M1[0]); assert_eq!(M1[1], m1_1, "{:b}", M1[1]); assert_eq!(M1[2], EMPTY_ROW); assert_eq!(M1[-1], FULL_ROW); assert_eq!(M1[0..2], [m1_0, m1_1]); // let m2_0 = 0b1111101111 | EMPTY_ROW; let m2_1 = 0b1100000001 | EMPTY_ROW; let m2_2 = 0b1000000000 | EMPTY_ROW; let m2_3 = 0b1000000000 | EMPTY_ROW; assert_eq!(M2[0..4], [m2_0, m2_1, m2_2, m2_3]); assert_eq!(M2[0..1], [m2_0]); assert_eq!(M2[2..4], [m2_2, m2_3]); assert_eq!(M2[2..5], M2[2..4]); assert_eq!(M2[-1..3], M2[0..3]); } fn occ(m: &Mat, y: i16, xs: RangeInclusive) -> Vec { xs.map(|x| m.get(x, y)).collect() } #[test] #[allow(clippy::just_underscores_and_digits)] fn test_occupied() { // get row data as bools let __ = false; let xx = true; assert_eq!( occ(M1, 0, -1..=10), [xx, xx, xx, __, __, xx, xx, xx, xx, __, xx, xx] ); assert_eq!(occ(M1, 1, 0..=9), [__, __, __, xx, __, __, __, __, xx, __]); assert_eq!(occ(M2, 1, 0..=9), [xx, __, __, __, __, __, __, __, xx, xx],); // test oob circumstances for x in -16..=16 { let oob = !(0..COLUMNS).contains(&x); assert_eq!(M1.get(x, 2), oob, "M1,x={x},y=2"); assert_eq!(M1.get(x, 3), oob, "M1,x={x},y=3"); assert_eq!(M2.get(x, 4), oob, "M2,x={x},y=4"); assert_eq!(M2.get(x, 5), oob, "M2,x={x},y=5"); assert_eq!(M1.get(x, 16), oob, "M1,x={x},y=16"); assert_eq!(M2.get(x, 17), oob, "M2,x={x},y=17"); for y in -4..0 { assert!(M1.get(x, y), "M1,x={x},y={y}"); assert!(M2.get(x, y), "M2,x={x},y={y}"); } } } #[test] fn test_mat_buf_size() { macro_rules! mat_buf_size { ($n:literal) => { core::mem::size_of::>() }; } assert_eq!(mat_buf_size!(0), core::mem::size_of::<[u16; 1]>()); assert_eq!(mat_buf_size!(1), core::mem::size_of::<[u16; 2]>()); assert_eq!(mat_buf_size!(10), core::mem::size_of::<[u16; 11]>()); assert_eq!(mat_buf_size!(40), core::mem::size_of::<[u16; 41]>()); assert_eq!( core::mem::size_of::(), core::mem::size_of::<[u16; 41]>() ); } #[test] fn test_mat_buf_copy_from() { let mut buf = MatBuf::new(); assert_eq!(buf, Mat::EMPTY); assert_eq!(buf.rows(), 0); let mat = mat! { "xxx......."; "xx........"; "x........."; }; buf.copy_from(mat); assert_eq!(buf, mat); assert_eq!(buf.rows(), 3); } #[test] fn test_clear_lines_1() { let mat0 = mat! { ".........."; // clear ".........."; // clear "x........."; ".........."; // clear ".x.xxxxxxx"; "xxxxxxxxxx"; // clear "x.xxxxxxxx"; }; let mat1 = mat! { "x........."; ".x.xxxxxxx"; "x.xxxxxxxx"; }; let mut buf: MatBuf<7> = MatBuf::default(); assert_eq!(buf.rows(), 0); buf.copy_from(mat0); assert_eq!(buf.rows(), 7); assert_eq!(buf.clear_lines(), 1..7); assert_eq!(buf, mat1); assert_eq!(buf.rows(), 3); } #[test] fn test_clear_lines_2() { let mut buf = MatBuf::new(); buf.copy_from(mat! { ".x.xxxxxxx"; "xxxxxxxxxx"; // clear "xxxxxxxxxx"; // clear "x.xxxxxxxx"; }); let tgt = mat! { ".x.xxxxxxx"; "x.xxxxxxxx"; }; assert_eq!(buf.clear_lines(), 1..3); assert_eq!(buf, tgt); assert_eq!(buf.clear_lines(), 2..2); assert_eq!(buf, tgt); } #[test] fn test_clear_lines_3() { let mut buf = MatBuf::new(); buf.copy_from(mat! { "xxxxxxxxxx"; // clear "xxxxxxxxxx"; // clear "xxxxxxxxxx"; // clear }); let tgt = Mat::EMPTY; assert_eq!(buf.clear_lines(), 0..3); assert_eq!(buf, tgt); assert_eq!(buf.clear_lines(), 0..0); } #[test] fn test_set() { let mut buf: MatBuf<4> = MatBuf::default(); buf.set(0, 0); // a buf.set(9, 3); // b buf.set(1, 1); // c buf.set(2, 1); // d buf.set(3, 1); // e assert!(buf.get(0, 0)); assert!(buf.get(9, 3)); assert!(buf.get(1, 1)); assert!(buf.get(2, 1)); assert!(buf.get(3, 1)); let mat = mat! { ".........b"; ".........."; ".cde......"; "a........."; }; assert_eq!(buf, mat); } #[test] fn test_fill() { let mut buf: MatBuf<5> = MatBuf::default(); buf.fill_row(1, 0b110111); // a let mat = mat! { "aaa.aa...."; ".........."; }; assert_eq!(buf, mat); buf.fill_row(3, 0b1000000000); // b buf.fill_row(0, u16::MAX); // c let mat = mat! { ".........b"; ".........."; "aaa.aa...."; "cccccccccc"; }; assert_eq!(buf, mat); } #[test] #[should_panic] fn test_set_oob() { let mut buf: MatBuf<4> = MatBuf::default(); buf.set(0, 4); } #[test] #[should_panic] fn test_fill_oob() { let mut buf: MatBuf<4> = MatBuf::default(); buf.fill_row(4, 0b1001); } #[test] fn test_shift_up() { let mat0 = mat! { ".x........"; "xxxxx.xxxx"; "x.xxxxxxxx"; ".xxxxxxxxx"; }; let mat1 = mat! { ".x........"; "xxxxx.xxxx"; "x.xxxxxxxx"; ".xxxxxxxxx"; ".........."; }; let mat2 = mat! { ".x........"; "xxxxx.xxxx"; "x.xxxxxxxx"; ".xxxxxxxxx"; ".........."; ".........."; }; let mut buf = MatBuf::new(); buf.copy_from(mat0); buf.shift_up(); assert_eq!(buf, mat1); buf.shift_up(); assert_eq!(buf, mat2); assert_eq!(buf.clear_lines(), 0..2); assert_eq!(buf, mat0); buf.shift_up(); assert_eq!(buf, mat1); assert_eq!(buf.clear_lines(), 0..1); } }