Create DFS find_locations algorithm
This commit is contained in:
parent
0190bdce66
commit
aade234881
|
@ -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"
|
||||
|
|
|
@ -4,5 +4,11 @@ description = "Bot?"
|
|||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
mino = { path = "../mino" }
|
||||
|
||||
ahash = "0.8"
|
||||
hashbrown = "0.13"
|
||||
|
|
|
@ -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<Item = Loc> + '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<Item = Movement>;
|
||||
|
||||
/// 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<Loc>,
|
||||
pub visited: HashSet<Loc, BuildHasher>,
|
||||
// TODO: useful for profiling?
|
||||
// pub max_height: usize,
|
||||
}
|
||||
|
||||
type BuildHasher = BuildHasherDefault<ahash::AHasher>;
|
||||
|
||||
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<Piece<T>> {
|
||||
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<Self::Item> {
|
||||
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<Loc> {
|
||||
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<E>(matrix: &Mat, ty: PieceType, expected: E)
|
||||
where
|
||||
E: IntoIterator,
|
||||
Loc: From<E::Item>,
|
||||
{
|
||||
let expected = expected.into_iter().map(Loc::from).collect::<Vec<_>>();
|
||||
let found = find_locations(matrix, ty, cap::All).collect::<Vec<_>>();
|
||||
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::<Loc>());
|
||||
}
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
#![no_std]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
pub mod find;
|
||||
|
||||
pub use find::find_locations;
|
||||
|
|
Loading…
Reference in New Issue