shark/mino/src/matrix.rs

441 lines
12 KiB
Rust

//! 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.
/// 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 core::ops::Index<i16> 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)
}
}
}
impl core::ops::Index<core::ops::RangeTo<i16>> for Mat {
type Output = [u16];
fn index(&self, r: core::ops::RangeTo<i16>) -> &[u16] {
let y = core::cmp::max(r.end, 0);
let y = core::cmp::min(y as usize, self.0.len());
&self.0[..y]
}
}
impl core::ops::Index<core::ops::RangeFull> for Mat {
type Output = [u16];
fn index(&self, _: core::ops::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<const N: usize>(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
}
}
/// Wrapper struct for using an underlying buffer (such as an array or vec) as a
/// "writable" matrix. This allows operations such as changing if a cell is occupied or
/// not.
///
/// [`MatBuf`] implements [`Deref`], so it automatically inherits the methods of [`Mat`].
#[derive(Clone, Default)]
pub struct MatBuf<T: AsRef<[u16]> = [u16; 40]> {
buffer: T,
rows: usize,
}
impl<const N: usize> MatBuf<[u16; N]> {
/// Returns a new empty [`MatBuf`] backed by a fixed-size array.
pub fn new() -> Self {
Self {
buffer: [0u16; N],
rows: 0,
}
}
}
impl<T> MatBuf<T>
where
T: AsRef<[u16]>,
{
/// Returns the underyling buffer.
pub fn into_inner(self) -> T {
self.buffer
}
/// Returns a read-only view of this matrix.
#[inline]
pub fn as_mat(&self) -> &Mat {
Mat::new(&self.buffer.as_ref()[..self.rows])
}
/// Resets the matrix so it is empty.
pub fn clear(&mut self) {
self.rows = 0;
}
}
impl<T> MatBuf<T>
where
T: AsRef<[u16]> + AsMut<[u16]>,
{
/// Modifies the cells in this matrix to be identical to those in `mat`.
///
/// Panics if the buffer space cannot fit the rows of `mat`.
pub fn copy_from(&mut self, mat: &Mat) {
let mat_data = &mat[..];
let buf_data = self.buffer.as_mut();
if mat_data.len() > buf_data.len() {
panic!("matrix cannot fit in available buffer space");
}
self.rows = mat_data.len();
buf_data[..self.rows].copy_from_slice(mat_data);
}
pub fn fill_row(&mut self, y: i16, mask: u16) {
if y < 0 {
// OOB coordinates are considered already set
return;
}
let y = y as usize;
let buf_data = self.buffer.as_mut();
while y >= self.rows {
*buf_data
.get_mut(self.rows)
.expect("y should be within available buffer space") = EMPTY_ROW;
self.rows += 1;
}
buf_data[y] |= mask;
}
/// Fills in the cell at the given (x,y) coordinate. Adds new rows to the top of the
/// matrix if necessary.
///
/// Panics if the buffer space cannot fit the new rows.
#[inline]
pub fn set(&mut self, x: i16, y: i16) {
if (0..COLUMNS).contains(&x) {
self.fill_row(y, 1 << x);
}
}
/// Removes any rows that are completely filled, shifting rows above down. Returns a
/// new view of the buffer that only includes the remaining rows.
pub fn clear_lines(&mut self) {
let data = self.buffer.as_mut();
let mut dst_y = 0;
for y in 0..self.rows {
if data[y] != FULL_ROW && data[y] != EMPTY_ROW {
data[dst_y] = data[y];
dst_y += 1;
}
}
self.rows = dst_y;
}
}
// All boilerplate below
impl<T: AsRef<[u16]>> core::ops::Deref for MatBuf<T> {
type Target = Mat;
fn deref(&self) -> &Mat {
self.as_mat()
}
}
impl<T: AsRef<[u16]>> core::fmt::Debug for MatBuf<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.as_mat().fmt(f)
}
}
impl<T: AsRef<[u16]>> core::cmp::Eq for MatBuf<T> {}
impl<T: AsRef<[u16]>> core::cmp::PartialEq<Mat> for MatBuf<T> {
fn eq(&self, other: &Mat) -> bool {
self.as_mat() == other
}
}
impl<T: AsRef<[u16]>> core::cmp::PartialEq<&Mat> for MatBuf<T> {
fn eq(&self, other: &&Mat) -> bool {
self.as_mat() == *other
}
}
impl<T: AsRef<[u16]>, U: AsRef<[u16]>> core::cmp::PartialEq<MatBuf<U>> for MatBuf<T> {
fn eq(&self, other: &MatBuf<U>) -> bool {
self.as_mat() == other.as_mat()
}
}
#[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);
}
fn occ(m: &Mat, y: i16, xs: RangeInclusive<i16>) -> Vec<bool> {
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_copy_from() {
let mut buf: MatBuf = 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() {
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<[u16; 7]> = MatBuf::new();
assert_eq!(buf.rows(), 0);
buf.copy_from(mat0);
assert_eq!(buf.rows(), 7);
buf.clear_lines();
assert_eq!(buf, mat1);
assert_eq!(buf.rows(), 3);
}
#[test]
fn test_set() {
let mut buf: MatBuf<[u16; 4]> = MatBuf::new();
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<[u16; 5]> = MatBuf::new();
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<[u16; 4]> = MatBuf::new();
buf.set(0, 4);
}
#[test]
#[should_panic]
fn test_fill_oob() {
let mut buf: MatBuf<[u16; 4]> = MatBuf::new();
buf.fill_row(4, 0b1001);
}
}