diff --git a/Cargo.lock b/Cargo.lock index 1d5651e..afc300e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,25 +2,71 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf6ccdb167abbf410dcb915cabd428929d7f6a04980b54a11f26a39f1c7f7107" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "anyhow" version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "fish" version = "0.1.0" dependencies = [ + "ahash", + "hashbrown", "mino", ] +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038" +dependencies = [ + "ahash", +] + [[package]] name = "itoa" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +[[package]] +name = "libc" +version = "0.2.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" + [[package]] name = "mino" version = "0.1.0" @@ -37,6 +83,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + [[package]] name = "proc-macro2" version = "1.0.47" @@ -108,3 +160,15 @@ name = "unicode-ident" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/fish/Cargo.toml b/fish/Cargo.toml index eb856ca..75bcbb1 100644 --- a/fish/Cargo.toml +++ b/fish/Cargo.toml @@ -4,5 +4,11 @@ description = "Bot?" version = "0.1.0" edition = "2021" +[features] +default = [] + [dependencies] mino = { path = "../mino" } + +ahash = "0.8" +hashbrown = "0.13" diff --git a/fish/src/find.rs b/fish/src/find.rs new file mode 100644 index 0000000..5525da6 --- /dev/null +++ b/fish/src/find.rs @@ -0,0 +1,388 @@ +//! Find locations. + +use alloc::vec::Vec; +use core::hash::BuildHasherDefault; +use hashbrown::hash_set::HashSet; +use mino::input::Kicks; +use mino::piece::{Shape, Spawn}; +use mino::{input, Loc, Mat, Movement, Piece}; + +// Generic arguments legend +// ======================== +// - T: piece type (e.g. srs::PieceType) +// - C: capabilities (e.g. cap::All) +// - 'm: matrix lifetime +// - 'c: T::cells() lifetime +// - 'k: T::kicks() lifetime +// - 'a: general purpose lifetime + +/// Helper function to just yield all of the locations reachable on `matrix` for the shape +/// `shape`, given input capabilities `capabilities`. +pub fn find_locations<'a, 'c, 'k, T, C>( + matrix: &'a Mat, + piece_ty: T, + capabilities: C, +) -> impl Iterator + 'a +where + T: Shape<'c> + Kicks<'k> + Spawn + Clone + 'a, + C: Capabilities + 'a, +{ + FindLocations::new(matrix, piece_ty, capabilities) +} + +/// Interface to describe what inputs the location finder is capable of performing in +/// order to reach target locations. This can be used, for instance, to prevent soft-drops +/// or 180 spins. +pub trait Capabilities { + /// Iterator type returned by `all_inputs`. + type InputsIter: IntoIterator; + + /// Returns a list of all of the inputs that can be performed. + fn all_inputs(&self) -> Self::InputsIter; + + // TODO: flags for soft drop or kicks +} + +/// Different implementations of the `Capabilities` trait. +pub mod cap { + use super::Capabilities; + use mino::Movement; + + /// Find all possible reachable locations; no restrictions. + pub struct All; + + impl Capabilities for All { + type InputsIter = [Movement; 4]; + fn all_inputs(&self) -> Self::InputsIter { + [Movement::LEFT, Movement::RIGHT, Movement::CW, Movement::CCW] + } + } +} + +/// Algorithm used to search for reachable locations on a given board state. The current +/// algorithm is just depth-first search but may be subject to change after benchmarking +/// experiments. +/// +/// [`FindLocations`] is an [`Iterator`], so you can use that interface to obtain the next +/// available location or loop over all of them. +pub struct FindLocations<'m, T, C> { + matrix: &'m Mat, + piece_ty: T, + capabilities: C, + buffers: FindLocationsBuffers, +} + +/// Contains the underlying buffers used by `FindLocations`. This structure may be used to +/// preallocate space or reuse buffers for multiple runs of the search algorithm. +/// +/// See [`FindLocations::with_buffers`] and [`FindLocations::into_buffers`]. +#[derive(Clone, Debug)] +pub struct FindLocationsBuffers { + pub stack: Vec, + pub visited: HashSet, + // TODO: useful for profiling? + // pub max_height: usize, +} + +type BuildHasher = BuildHasherDefault; + +impl Default for FindLocationsBuffers { + fn default() -> Self { + Self { + stack: Vec::with_capacity(64), + visited: HashSet::with_capacity(400), + // max_height: 0, + } + } +} + +impl<'m, 'c, 'k, T, C> FindLocations<'m, T, C> +where + T: Shape<'c> + Kicks<'k> + Spawn + Clone, + C: Capabilities, +{ + /// Constructs a new instance of the location finding algorithm. + pub fn new(matrix: &'m Mat, piece_ty: T, capabilities: C) -> Self { + let bufs = FindLocationsBuffers::default(); + Self::with_buffers(matrix, piece_ty, capabilities, bufs) + } + + /// Constructs a new instance of the location finding algorithm. Uses `buffers` for + /// storage needed by the algorithm. The buffers will be cleared on initialization so + /// it does not matter if they previously contained data. + pub fn with_buffers( + matrix: &'m Mat, + piece_ty: T, + capabilities: C, + buffers: FindLocationsBuffers, + ) -> Self { + let mut this = Self { + matrix, + piece_ty, + capabilities, + buffers, + }; + this.init(); + this + } + + /// Aborts the search algorithm and relinquishes ownership of the underlying + /// buffers. This is useful for reusing the buffers by passing them to + /// [`Self::with_buffers`] the next time the algorithm is run. + pub fn into_buffers(self) -> FindLocationsBuffers { + self.buffers + } + + fn init(&mut self) { + self.buffers.stack.clear(); + self.buffers.visited.clear(); + // self.buffers.max_height = 0; + + let init_pc = Piece::spawn(self.piece_ty.clone()); + if init_pc.cells().intersects(self.matrix) { + // game over + return; + } + self.push(init_pc.loc); + } + + fn push(&mut self, loc: Loc) { + // 'visited' set prevents cycles + if self.buffers.visited.insert(loc) { + self.buffers.stack.push(loc); + // self.buffers.max_height = + // core::cmp::max(self.buffers.max_height, self.buffers.stack.len()); + } + } + + fn pop(&mut self) -> Option> { + let ty = self.piece_ty.clone(); + self.buffers.stack.pop().map(|loc| Piece { ty, loc }) + } +} + +impl<'m, 'c, 'k, T, C> Iterator for FindLocations<'m, T, C> +where + T: Shape<'c> + Kicks<'k> + Spawn + Clone, + C: Capabilities, +{ + type Item = Loc; + fn next(&mut self) -> Option { + while let Some(pc0) = self.pop() { + // push all locations reachable by performing an input + for inp in self.capabilities.all_inputs() { + let mut pc = pc0.clone(); + if inp.perform(&mut pc, self.matrix) { + self.push(pc.loc); + } + } + let mut pc = pc0; + if input::drop(&mut pc, self.matrix) { + // piece was floating, so drop it and analyze that later + self.push(pc.loc); + // TODO: don't push if soft drops are denied by self.capabilities + } else { + // piece was not floating so it's a valid final piece location. + return Some(pc.loc); + } + } + None + } +} + +impl<'m, 'c, 'k, T: Shape<'c> + Kicks<'k> + Spawn + Clone, C: Capabilities> + core::iter::FusedIterator for FindLocations<'m, T, C> +{ + // When `buffers.stack` is empty, the iterator will constanty return `None`. +} + +#[cfg(test)] +mod test { + use super::*; + use mino::srs::PieceType; + use mino::{mat, Rot}; + + use alloc::vec::Vec; + + fn find_missing(ty: PieceType, lhs: &[Loc], rhs: &[Loc]) -> Option { + lhs.iter().find_map(|&loc_l| { + let in_rhs = rhs.iter().any(|&loc_r| { + let pc_l = Piece { ty, loc: loc_l }; + let pc_r = Piece { ty, loc: loc_r }; + pc_l.cells() == pc_r.cells() + }); + if in_rhs { + None + } else { + Some(loc_l) + } + }) + } + + fn test_find_locs(matrix: &Mat, ty: PieceType, expected: E) + where + E: IntoIterator, + Loc: From, + { + let expected = expected.into_iter().map(Loc::from).collect::>(); + let found = find_locations(matrix, ty, cap::All).collect::>(); + if let Some(exp) = find_missing(ty, &expected, &found) { + panic!("{exp:?} expected but not found"); + } else if let Some(fnd) = find_missing(ty, &found, &expected) { + panic!("{fnd:?} returned but not expected"); + } + + // XXX(iitalics): currently its OK for find_locations() to yield locations with + // identical cells, since a transposition table will deduplicate them later on in + // the engine logic. however it may turn out to be a performance win to do some + // deduplication early to reduce pressure on the transposition table. + assert!(expected.len() <= found.len()); + //assert_eq!(expected.len(), found.len()); + } + + #[test] + fn test_o_empty() { + test_find_locs(Mat::EMPTY, PieceType::O, (0..=8).map(|x| (x, 0, Rot::N))); + } + + #[test] + fn test_i_empty() { + test_find_locs( + Mat::EMPTY, + PieceType::I, + [ + // N/S + (1..=7, 0, Rot::N), + // E/W + (0..=9, 1, Rot::W), + ] + .into_iter() + .flat_map(|(xs, y, r)| xs.map(move |x| (x, y, r))), + ); + } + + #[test] + fn test_o_bumpy() { + const MAT: &Mat = mat! { + ".........x"; + ".........x"; + ".x...xx..x"; + }; + test_find_locs( + MAT, + PieceType::O, + [ + (0, 1, Rot::N), + (1, 1, Rot::N), + (2, 0, Rot::N), + (3, 0, Rot::N), + (4, 1, Rot::N), + (5, 1, Rot::N), + (6, 1, Rot::N), + (7, 0, Rot::N), + (8, 3, Rot::N), + ], + ); + } + + #[test] + fn test_t_spin() { + const MAT: &Mat = mat! { + ".......xx."; + "xxxxx...xx"; + "xxxxxx.xxx"; + }; + test_find_locs( + MAT, + PieceType::T, + [ + // top: N + (1..=5, 2, Rot::N), + (6..=8, 3, Rot::N), + // top: E + (0..=4, 3, Rot::E), + (5..=5, 2, Rot::E), + (6..=6, 3, Rot::E), + (7..=8, 4, Rot::E), + // top: S + (1..=4, 3, Rot::S), + (5..=5, 2, Rot::S), + (6..=6, 3, Rot::S), + (7..=8, 4, Rot::S), + // top: W + (1..=4, 3, Rot::W), + (5..=5, 2, Rot::W), + (6..=6, 1, Rot::W), + (7..=8, 4, Rot::W), + (9..=9, 3, Rot::W), + // TSS + (6..=6, 1, Rot::E), + (6..=6, 1, Rot::N), + // TSD + (6..=6, 1, Rot::S), + ] + .into_iter() + .flat_map(|(xs, y, r)| xs.map(move |x| (x, y, r))), + ); + } + + #[test] + fn test_s_spin() { + const MAT: &Mat = mat! { + "xxxxxx..xx"; + "xxxxx..xxx"; + }; + test_find_locs( + MAT, + PieceType::S, + [ + // N/S + (1..=6, 2, Rot::N), + (7..=7, 1, Rot::N), + (8..=8, 2, Rot::N), + // E/W + (0..=4, 3, Rot::E), + (5..=6, 2, Rot::E), + (7..=8, 3, Rot::E), + // spin + (6..=6, 1, Rot::S), // equiv (6..=6, 0, Rot::N), + ] + .into_iter() + .flat_map(|(xs, y, r)| xs.map(move |x| (x, y, r))), + ); + } + + #[test] + fn test_z_spin() { + const MAT: &Mat = mat! { + "xxxxx..xxx"; + "xxxxxx..xx"; + }; + test_find_locs( + MAT, + PieceType::Z, + [ + // N/S + (1..=4, 2, Rot::N), + (5..=5, 1, Rot::N), + (6..=8, 2, Rot::N), + // E/W + (0..=4, 3, Rot::E), + (5..=6, 2, Rot::E), + (7..=8, 3, Rot::E), + // spin + (6..=6, 1, Rot::S), // equiv (6..=6, 0, Rot::N), + ] + .into_iter() + .flat_map(|(xs, y, r)| xs.map(move |x| (x, y, r))), + ); + } + + #[test] + fn test_top_out() { + // giant pillar in the center should cause top-out + let rows = [0b0000100000u16; 22]; + let mat = Mat::new(&rows); + test_find_locs(mat, PieceType::I, core::iter::empty::()); + } +} diff --git a/fish/src/lib.rs b/fish/src/lib.rs index 6ec9ef6..f6deba4 100644 --- a/fish/src/lib.rs +++ b/fish/src/lib.rs @@ -1,3 +1,7 @@ #![no_std] extern crate alloc; + +pub mod find; + +pub use find::find_locations;