diff --git a/mino/src/lib.rs b/mino/src/lib.rs index 441260c..d9a2430 100644 --- a/mino/src/lib.rs +++ b/mino/src/lib.rs @@ -2,6 +2,7 @@ pub mod location; pub mod matrix; +pub mod piece; pub use location::Loc; pub use matrix::Mat; diff --git a/mino/src/piece.rs b/mino/src/piece.rs new file mode 100644 index 0000000..4908af8 --- /dev/null +++ b/mino/src/piece.rs @@ -0,0 +1,237 @@ +//! Data structures for representing pieces and shapes. + +use crate::location::{Loc, Rot, Spin}; +use crate::matrix::Mat; + +use core::ops::Range; + +/// Interface for representations of shapes in the abstract, not tied to any particular +/// position or orientation. +pub trait Shape<'c> { + /// Returns the cell data for this shape when given a specific rotation state. + fn cells(&self, rot: Rot) -> Cells<'c>; +} + +/// Data structure representing a set of cells located somewhere on the board. Logically +/// this is like a set of (x,y) coordinates, but the implementation uses rowwise bit +/// patterns similar to [`Mat`] for more efficient [intersection +/// tests](Cells::intersects). +/// +/// The cells in this representation cannot be rotated; rotations are achieved by +/// modifying the rotation state of a [`Loc`] and then determining the cells of the result +/// using [`Piece::cells`]. +#[derive(Copy, Clone, Eq)] +pub struct Cells<'c> { + data: *const u16, + x: i16, + y: i16, + h: i16, + w: i16, + _slice: core::marker::PhantomData<&'c [u16]>, +} + +impl PartialEq for Cells<'_> { + fn eq(&self, rhs: &Self) -> bool { + // XXX(iitalics): identical 'data' should imply identical width; width is only + // stored to make bounds tests faster but could theoretically be derived from the + // bits in each row + debug_assert!(self.data != rhs.data || self.w == rhs.w); + (self.data, self.x, self.y, self.h) == (rhs.data, rhs.x, rhs.y, rhs.h) + } +} + +impl<'c> Cells<'c> { + /// Constructs cells from the initial extents and rowwise bit representations of the + /// occupied cells. + #[inline] + pub fn new(xs: Range, ys: Range, data: &'c [u16]) -> Self { + assert!(!xs.is_empty()); + assert!(!ys.is_empty()); + assert_eq!(ys.len(), data.len()); + Self { + x: xs.start, + y: ys.start, + w: xs.end - xs.start, + h: ys.end - ys.start, + data: data.as_ptr(), + _slice: core::marker::PhantomData, + } + } + + /// Returns a pair of ranges, `(xs, ys)`, which indicates the furthest extents that + /// may contain cells. For example, a north T piece at the origin would return + /// `(-1..=1, 0..=1)` since it contains leftmost cell (-1,0), rightmost cell (0,1), + /// and uppermost cell (0,1). + #[inline] + pub fn extents(&self) -> (Range, Range) { + (self.x..self.x + self.w, self.y..self.y + self.h) + } + + #[inline] + fn data(&self) -> &'c [u16] { + let ptr = self.data; + let len = self.h as usize; + unsafe { core::slice::from_raw_parts(ptr, len) } + } + + /// Returns true if any of the cells intersect with the given matrix. + pub fn intersects(&self, mat: &Mat) -> bool { + if self.x < 0 || self.x + self.w > mat.cols() { + return true; + } + for (dy, &row) in self.data().iter().enumerate() { + let y = self.y + dy as i16; + let mask = row << self.x; // should never overflow + if !mat.test_row(y, mask, 0) { + return true; + } + } + false + } + + /// Translates the cells by the given amount in both directions. + #[inline] + pub fn translate(&mut self, dx: i16, dy: i16) { + self.x = self.x.checked_add(dx).expect("overflow/underflow"); + self.y = self.y.checked_add(dy).expect("overflow/underflow"); + } + + #[cfg(test)] + pub(crate) fn coords(&self) -> impl Iterator + 'c { + let data = self.data(); + let (xs, ys) = self.extents(); + ys.enumerate() + .flat_map(move |(i, y)| xs.clone().enumerate().map(move |(j, x)| (i, j, (x, y)))) + .filter_map(move |(i, j, pos)| { + if data[i] & (1 << j) != 0 { + Some(pos) + } else { + None + } + }) + } +} + +/// Represents the current state of a piece. +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct Piece { + /// The shape of the piece. + pub shape: S, + /// The location of the piece. + pub loc: Loc, +} + +impl<'c, S: Shape<'c>> Piece { + /// Returns the cells occupied by this piece. + pub fn cells(&self) -> Cells<'c> { + let mut cells = self.shape.cells(self.loc.r); + cells.translate(self.loc.x, self.loc.y); + cells + } + + /// Translates this piece by the given amount in both directions. + pub fn translate(&mut self, dx: i16, dy: i16) { + self.loc.x = self.loc.x.checked_add(dx).expect("overflow/underflow"); + self.loc.y = self.loc.y.checked_add(dy).expect("overflow/underflow"); + } + + /// Rotates this piece in the given direction. + pub fn rotate(&mut self, dr: Spin) { + self.loc.r += dr; + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::mat; + + use core::ops::RangeInclusive; + + extern crate alloc; + use alloc::vec::Vec; + + // .X. + // .XX origin at (1,1) + // ... + #[derive(Copy, Clone, Eq, PartialEq, Debug)] + struct Tri; + + impl Shape<'static> for Tri { + fn cells(&self, rot: Rot) -> Cells<'static> { + match rot { + Rot::N => Cells::new(0..2, 0..2, &[0b11, 0b01]), + Rot::E => Cells::new(0..2, -1..1, &[0b01, 0b11]), + Rot::S => Cells::new(-1..1, -1..1, &[0b10, 0b11]), + Rot::W => Cells::new(-1..1, 0..2, &[0b11, 0b10]), + } + } + } + + fn coords(x: i16, y: i16, r: Rot) -> Vec<(i16, i16)> { + let piece = Piece { + shape: Tri, + loc: Loc { x, y, r }, + }; + let mut coords: Vec<_> = piece.cells().coords().collect(); + coords.sort(); + coords + } + + #[test] + fn test_coords() { + assert_eq!(coords(5, 8, Rot::N), [(5, 8), (5, 9), (6, 8)]); + assert_eq!(coords(5, 8, Rot::E), [(5, 7), (5, 8), (6, 8),]); + assert_eq!(coords(5, 8, Rot::S), [(4, 8), (5, 7), (5, 8),]); + assert_eq!(coords(5, 8, Rot::W), [(4, 8), (5, 8), (5, 9),]); + } + + const MAT: &Mat = mat! { + ".........x"; + "..x.....xx"; + "xxx.xxx.xx"; + }; + + fn isects(xs: RangeInclusive, y: i16, r: Rot) -> Vec { + xs.map(|x| { + let piece = Piece { + shape: Tri, + loc: Loc { x, y, r }, + }; + piece.cells().intersects(MAT) + }) + .collect() + } + + #[test] + #[allow(clippy::just_underscores_and_digits)] + fn test_intersects() { + use Rot::*; + let __ = false; + let xx = true; + // N. + // NN + assert_eq!(isects(0..=8, 0, N), [xx; 9]); + assert_eq!(isects(0..=8, 1, N), [__, xx, xx, __, __, __, __, xx, xx]); + assert_eq!(isects(0..=8, 2, N), [__, __, __, __, __, __, __, __, xx]); + assert_eq!(isects(0..=8, 3, N), [__; 9]); + // EE + // E. + assert_eq!(isects(0..=8, 0, E), [xx; 9]); + assert_eq!(isects(0..=8, 1, E), [xx, xx, xx, __, xx, xx, xx, xx, xx]); + assert_eq!(isects(0..=8, 2, E), [__, __, xx, __, __, __, __, __, xx]); + assert_eq!(isects(0..=8, 3, E), [__; 9]); + // SS + // .S + assert_eq!(isects(1..=9, 0, S), [xx; 9]); + assert_eq!(isects(1..=9, 1, S), [xx, xx, xx, xx, xx, xx, __, xx, xx]); + assert_eq!(isects(1..=9, 2, S), [__, xx, __, __, __, __, __, xx, xx]); + assert_eq!(isects(1..=9, 3, S), [__, __, __, __, __, __, __, __, xx]); + // .W + // WW + assert_eq!(isects(1..=9, 0, W), [xx; 9]); + assert_eq!(isects(1..=9, 1, W), [__, xx, xx, __, __, __, __, xx, xx]); + assert_eq!(isects(1..=9, 2, W), [__, __, __, __, __, __, __, __, xx]); + assert_eq!(isects(1..=9, 3, W), [__; 9]); + } +}