From 047c5fc02ee6f8b35956d881843e6fc667b75834 Mon Sep 17 00:00:00 2001 From: Mitchell M Date: Wed, 24 Dec 2025 10:59:43 -0600 Subject: [PATCH] move engine work --- Cargo.lock | 25 ++ tak_lib/Cargo.toml | 3 + tak_lib/src/engine.rs | 310 +++++++------------- tak_lib/src/lib.rs | 29 +- tak_lib/src/test_engine.rs | 566 +++++++++++++++++++++++++++++++++++++ tak_lib/src/tile.rs | 14 +- 6 files changed, 736 insertions(+), 211 deletions(-) create mode 100644 tak_lib/src/test_engine.rs diff --git a/Cargo.lock b/Cargo.lock index 22ae2c5..92993e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "tak_bot" version = "0.1.0" @@ -12,6 +28,9 @@ dependencies = [ [[package]] name = "tak_lib" version = "0.1.0" +dependencies = [ + "pretty_assertions", +] [[package]] name = "tak_server" @@ -20,3 +39,9 @@ version = "0.1.0" [[package]] name = "tak_visualizer" version = "0.1.0" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" diff --git a/tak_lib/Cargo.toml b/tak_lib/Cargo.toml index b6213af..14c98bd 100644 --- a/tak_lib/Cargo.toml +++ b/tak_lib/Cargo.toml @@ -4,3 +4,6 @@ version = "0.1.0" edition = "2024" [dependencies] + +[dev-dependencies] +pretty_assertions = "1.4.1" diff --git a/tak_lib/src/engine.rs b/tak_lib/src/engine.rs index eea4b0a..1a4efa7 100644 --- a/tak_lib/src/engine.rs +++ b/tak_lib/src/engine.rs @@ -1,9 +1,12 @@ use crate::{ - Board, Move, + Board, Direction, Move, piece::{ColoredPiece, Piece}, + tile::Tile, }; impl Board { + /// Gets an iterator for the moves that can be made by + /// the active player on this board. pub fn moves(&self) -> impl Iterator> { let to_move = self.to_move; let tiles = &self.tiles; @@ -28,9 +31,9 @@ impl Board { let top_piece = tiles[x as usize][y as usize].top_piece(); if top_piece == ColoredPiece::None { // possible moves are place moves - insert_placement_moves(x, y, can_place_caps) + get_placement_moves(x, y, can_place_caps) } else if top_piece.color() == to_move { - get_movement_moves(x, y, tiles[x as usize][y as usize].stack_len()) + get_movement_moves(x, y, tiles) } else { Vec::with_capacity(0) } @@ -38,7 +41,7 @@ impl Board { } } -fn insert_placement_moves(x: u8, y: u8, can_place_caps: bool) -> Vec> { +fn get_placement_moves(x: u8, y: u8, can_place_caps: bool) -> Vec> { let mut moves = Vec::with_capacity(2 + can_place_caps as usize); moves.push(Move::Place { piece: Piece::Flat, @@ -57,21 +60,92 @@ fn insert_placement_moves(x: u8, y: u8, can_place_caps: bool) -> moves } -fn get_movement_moves(x: u8, y: u8, stack_len: u8) -> Vec> { - let max_grab = u8::min(N as u8, stack_len + 1); +pub(crate) fn get_movement_moves( + x: u8, + y: u8, + tiles: &[[Tile; N]; N], +) -> Vec> { + let mut moves = Vec::new(); + let tile = tiles[x as usize][y as usize]; + let is_caps = tile.top_piece().piece() == Piece::Caps; + let max_grab = u8::min(N as u8, tile.stack_len() + 1); - // East - let max_dist_east = max_grab.min(N as u8 - x - 1); - // North - let max_dist_north = max_grab.min(N as u8 - y - 1); - // West - let max_dist_west = max_grab.min(x); - // South - let max_dist_south = max_grab.min(y); - todo!() + for dir in Direction::ALL { + get_moves_in_dir(x, y, dir, tiles, &mut moves, is_caps, max_grab); + } + + moves } -fn get_drop_amounts( +/// Gets the possible moves moving from (`x`, `y`) in the given direction `dir`. +/// +/// This function does not check the colors of anything, so the caller must make sure +/// the active player owns the piece on this tile, if you want to get only valid moves +/// for the active player. +fn get_moves_in_dir( + x: u8, + y: u8, + dir: Direction, + tiles: &[[Tile; N]; N], + moves: &mut Vec>, + is_caps: bool, + max_grab: u8, +) { + // first, get the max distance we can go + let mut max_dist = max_grab.min(dir.dist_to_edge::(x, y)); + let mut end_w_capstone = false; + let (dx, dy) = dir.delta(); + + // step through the tiles to see if out movement gets blocked + for i in 1..=max_dist { + let xi = x as isize + (dx * i as isize); + let yi = y as isize + (dy * i as isize); + // we already clamp `max_distance` to the board edge, so indexing should be safe + let next_piece = tiles[xi as usize][yi as usize].top_piece().piece(); + + // check for pieces that will block us + if next_piece == Piece::Caps { + max_dist = i - 1; + break; + } else if next_piece == Piece::Wall { + // if we have a capstone, we can smash the wall + if is_caps { + end_w_capstone = true; + max_dist = i; + } else { + max_dist = i - 1; + } + break; + } + } + if max_dist == 0 { + return; + } + + // now that we have our max distance, get all the possible moves + let mut insert_fn = |drops: [u8; N]| { + moves.push(Move::Move { + from: (x, y), + count: drops.iter().copied().sum::(), + dir, + drop: drops, + }); + }; + for grab in 1..=max_grab { + let mut buf = [0; N]; + get_drop_amounts(&mut buf, 0, &mut insert_fn, grab, max_dist, end_w_capstone); + } +} + +/// Gets the possible amounts of pieces to drop at each tile. +/// +/// `pieces` is the number of pieces that are picked up. +/// `len` is the max length that the stack can be moved. +/// +/// If the top piece of the stack is a capstone, and the end of the line +/// is a wall, `end_w_capstone` should be `true` and `len` should include +/// that tile. +pub(crate) fn get_drop_amounts( // shared state buf: &mut [u8; N], i: usize, @@ -94,9 +168,17 @@ fn get_drop_amounts( return; } if len == 1 { - buf[i] = pieces; - insert_fn(buf.clone()); - buf[i] = 0; + if end_w_capstone { + if pieces == 1 { + buf[i] = 1; + insert_fn(buf.clone()); + buf[i] = 0; + } + } else { + buf[i] = pieces; + insert_fn(buf.clone()); + buf[i] = 0; + } return; } @@ -107,193 +189,3 @@ fn get_drop_amounts( } buf[i] = 0; } - -#[cfg(test)] -fn expect_get_drop_ammounts( - pieces: u8, - len: u8, - end_w_capstone: bool, - expected_drop_list: Vec<[u8; N]>, -) { - use std::collections::HashSet; - - let mut buf = [0; N]; - let mut drop_list = Vec::new(); - let mut insert_fn = |drops: [u8; N]| { - drop_list.push(drops); - }; - get_drop_amounts(&mut buf, 0, &mut insert_fn, pieces, len, end_w_capstone); - - let expected_drop_set: HashSet<_> = expected_drop_list.iter().cloned().collect(); - let drop_set: HashSet<_> = drop_list.iter().cloned().collect(); - if drop_set.len() != drop_list.len() { - panic!("drop list had duplicate values"); - } - if expected_drop_set != drop_set { - let mut err_str = String::new(); - for entry in expected_drop_list { - if drop_set.contains(&entry) { - err_str += &format!("\t{:?} {:?}\n", entry, entry); - } else { - err_str += &format!("\t{:?}\n", entry); - } - } - for entry in drop_list { - if expected_drop_set.contains(&entry) { - continue; - } - let blank = format!("{:?}", entry) - .chars() - .map(|_| ' ') - .collect::(); - err_str += &format!("\t{} {:?}\n", blank, entry); - } - panic!("incorrect drop list!\n{}", err_str); - } -} - -#[test] -fn test_full_len_drop_amounts() { - expect_get_drop_ammounts(1, 1, false, vec![[1]]); - expect_get_drop_ammounts(1, 1, false, vec![[1, 0]]); - expect_get_drop_ammounts(1, 1, false, vec![[1, 0, 0]]); - expect_get_drop_ammounts(2, 2, false, vec![[1, 1, 0, 0, 0], [2, 0, 0, 0, 0]]); - expect_get_drop_ammounts( - 3, - 3, - false, - vec![ - [1, 1, 1, 0, 0], - [1, 2, 0, 0, 0], - [2, 1, 0, 0, 0], - [3, 0, 0, 0, 0], - ], - ); - expect_get_drop_ammounts( - 4, - 4, - false, - vec![ - [1, 1, 1, 1, 0], - [1, 1, 2, 0, 0], - [1, 2, 1, 0, 0], - [1, 3, 0, 0, 0], - [2, 1, 1, 0, 0], - [2, 2, 0, 0, 0], - [3, 1, 0, 0, 0], - [4, 0, 0, 0, 0], - ], - ); - expect_get_drop_ammounts( - 5, - 5, - false, - vec![ - [1, 1, 1, 1, 1], - [1, 1, 1, 2, 0], - [1, 1, 2, 1, 0], - [1, 1, 3, 0, 0], - [1, 2, 1, 1, 0], - [1, 2, 2, 0, 0], - [1, 3, 1, 0, 0], - [1, 4, 0, 0, 0], - [2, 1, 1, 1, 0], - [2, 1, 2, 0, 0], - [2, 2, 1, 0, 0], - [2, 3, 0, 0, 0], - [3, 1, 1, 0, 0], - [3, 2, 0, 0, 0], - [4, 1, 0, 0, 0], - [5, 0, 0, 0, 0], - ], - ); -} - -#[test] -fn test_truncated_drop_amounts() { - expect_get_drop_ammounts( - 3, - 2, - false, - vec![[1, 2, 0, 0, 0], [2, 1, 0, 0, 0], [3, 0, 0, 0, 0]], - ); - expect_get_drop_ammounts(3, 1, false, vec![[3, 0, 0, 0, 0]]); - expect_get_drop_ammounts( - 4, - 3, - false, - vec![ - [1, 1, 2, 0, 0], - [1, 2, 1, 0, 0], - [1, 3, 0, 0, 0], - [2, 1, 1, 0, 0], - [2, 2, 0, 0, 0], - [3, 1, 0, 0, 0], - [4, 0, 0, 0, 0], - ], - ); - expect_get_drop_ammounts( - 4, - 2, - false, - vec![ - [1, 3, 0, 0, 0], - [2, 2, 0, 0, 0], - [3, 1, 0, 0, 0], - [4, 0, 0, 0, 0], - ], - ); - expect_get_drop_ammounts( - 5, - 4, - false, - vec![ - [1, 1, 1, 2, 0], - [1, 1, 2, 1, 0], - [1, 1, 3, 0, 0], - [1, 2, 1, 1, 0], - [1, 2, 2, 0, 0], - [1, 3, 1, 0, 0], - [1, 4, 0, 0, 0], - [2, 1, 1, 1, 0], - [2, 1, 2, 0, 0], - [2, 2, 1, 0, 0], - [2, 3, 0, 0, 0], - [3, 1, 1, 0, 0], - [3, 2, 0, 0, 0], - [4, 1, 0, 0, 0], - [5, 0, 0, 0, 0], - ], - ); - expect_get_drop_ammounts( - 5, - 3, - false, - vec![ - [1, 1, 3, 0, 0], - [1, 2, 2, 0, 0], - [1, 3, 1, 0, 0], - [1, 4, 0, 0, 0], - [2, 1, 2, 0, 0], - [2, 2, 1, 0, 0], - [2, 3, 0, 0, 0], - [3, 1, 1, 0, 0], - [3, 2, 0, 0, 0], - [4, 1, 0, 0, 0], - [5, 0, 0, 0, 0], - ], - ); - expect_get_drop_ammounts( - 5, - 2, - false, - vec![ - [1, 4, 0, 0, 0], - [2, 3, 0, 0, 0], - [3, 2, 0, 0, 0], - [4, 1, 0, 0, 0], - [5, 0, 0, 0, 0], - ], - ); - expect_get_drop_ammounts(5, 1, false, vec![[5, 0, 0, 0, 0]]); -} diff --git a/tak_lib/src/lib.rs b/tak_lib/src/lib.rs index 25fe370..8a5aec2 100644 --- a/tak_lib/src/lib.rs +++ b/tak_lib/src/lib.rs @@ -1,5 +1,7 @@ pub mod engine; pub mod piece; +#[cfg(test)] +mod test_engine; pub mod tile; use crate::{ @@ -26,12 +28,31 @@ impl Display for Direction { } } impl Direction { - const ALL: [Direction; 4] = [ + pub const ALL: [Direction; 4] = [ Direction::East, Direction::North, Direction::West, Direction::South, ]; + + pub fn delta(&self) -> (isize, isize) { + match self { + Direction::East => (1, 0), + Direction::North => (0, 1), + Direction::West => (-1, 0), + Direction::South => (0, -1), + } + } + + #[inline] + pub fn dist_to_edge(&self, x: u8, y: u8) -> u8 { + match self { + Direction::East => N as u8 - x - 1, + Direction::North => N as u8 - y - 1, + Direction::West => x, + Direction::South => y, + } + } } #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] @@ -39,6 +60,11 @@ pub struct Board { to_move: Color, tiles: [[Tile; N]; N], } +pub type Board3x3 = Board<3>; +pub type Board4x4 = Board<4>; +pub type Board5x5 = Board<5>; +pub type Board6x6 = Board<6>; +pub type Board8x8 = Board<8>; impl Board { /// The number of capstones in each players' pouches. @@ -68,6 +94,7 @@ impl Board { } } +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] pub enum Move { Place { piece: Piece, diff --git a/tak_lib/src/test_engine.rs b/tak_lib/src/test_engine.rs new file mode 100644 index 0000000..e3139b4 --- /dev/null +++ b/tak_lib/src/test_engine.rs @@ -0,0 +1,566 @@ +use crate::{Direction, Move, engine::get_movement_moves, piece::ColoredPiece, tile::Tile}; + +#[cfg(test)] +fn expect_get_drop_ammounts( + pieces: u8, + len: u8, + end_w_capstone: bool, + expected_drop_list: Vec<[u8; N]>, +) { + use crate::engine::get_drop_amounts; + + let mut buf = [0; N]; + let mut drop_list = Vec::new(); + let mut insert_fn = |drops: [u8; N]| { + drop_list.push(drops); + }; + get_drop_amounts(&mut buf, 0, &mut insert_fn, pieces, len, end_w_capstone); + + if let Err(err) = assert_uo_list_eq(expected_drop_list, drop_list) { + panic!( + "incorrect drop list! (pieces={}, len={}, end_w_capstone={})\n{}", + pieces, len, end_w_capstone, err + ); + } +} + +#[test] +fn test_full_len_drop_amounts() { + expect_get_drop_ammounts(1, 1, false, vec![[1]]); + expect_get_drop_ammounts(1, 1, false, vec![[1, 0]]); + expect_get_drop_ammounts(1, 1, false, vec![[1, 0, 0]]); + expect_get_drop_ammounts(2, 2, false, vec![[1, 1, 0, 0, 0], [2, 0, 0, 0, 0]]); + expect_get_drop_ammounts( + 3, + 3, + false, + vec![ + [1, 1, 1, 0, 0], + [1, 2, 0, 0, 0], + [2, 1, 0, 0, 0], + [3, 0, 0, 0, 0], + ], + ); + expect_get_drop_ammounts( + 4, + 4, + false, + vec![ + [1, 1, 1, 1, 0], + [1, 1, 2, 0, 0], + [1, 2, 1, 0, 0], + [1, 3, 0, 0, 0], + [2, 1, 1, 0, 0], + [2, 2, 0, 0, 0], + [3, 1, 0, 0, 0], + [4, 0, 0, 0, 0], + ], + ); + expect_get_drop_ammounts( + 5, + 5, + false, + vec![ + [1, 1, 1, 1, 1], + [1, 1, 1, 2, 0], + [1, 1, 2, 1, 0], + [1, 1, 3, 0, 0], + [1, 2, 1, 1, 0], + [1, 2, 2, 0, 0], + [1, 3, 1, 0, 0], + [1, 4, 0, 0, 0], + [2, 1, 1, 1, 0], + [2, 1, 2, 0, 0], + [2, 2, 1, 0, 0], + [2, 3, 0, 0, 0], + [3, 1, 1, 0, 0], + [3, 2, 0, 0, 0], + [4, 1, 0, 0, 0], + [5, 0, 0, 0, 0], + ], + ); +} + +#[test] +fn test_truncated_drop_amounts() { + expect_get_drop_ammounts( + 3, + 2, + false, + vec![[1, 2, 0, 0, 0], [2, 1, 0, 0, 0], [3, 0, 0, 0, 0]], + ); + expect_get_drop_ammounts(3, 1, false, vec![[3, 0, 0, 0, 0]]); + expect_get_drop_ammounts( + 4, + 3, + false, + vec![ + [1, 1, 2, 0, 0], + [1, 2, 1, 0, 0], + [1, 3, 0, 0, 0], + [2, 1, 1, 0, 0], + [2, 2, 0, 0, 0], + [3, 1, 0, 0, 0], + [4, 0, 0, 0, 0], + ], + ); + expect_get_drop_ammounts( + 4, + 2, + false, + vec![ + [1, 3, 0, 0, 0], + [2, 2, 0, 0, 0], + [3, 1, 0, 0, 0], + [4, 0, 0, 0, 0], + ], + ); + expect_get_drop_ammounts( + 5, + 4, + false, + vec![ + [1, 1, 1, 2, 0], + [1, 1, 2, 1, 0], + [1, 1, 3, 0, 0], + [1, 2, 1, 1, 0], + [1, 2, 2, 0, 0], + [1, 3, 1, 0, 0], + [1, 4, 0, 0, 0], + [2, 1, 1, 1, 0], + [2, 1, 2, 0, 0], + [2, 2, 1, 0, 0], + [2, 3, 0, 0, 0], + [3, 1, 1, 0, 0], + [3, 2, 0, 0, 0], + [4, 1, 0, 0, 0], + [5, 0, 0, 0, 0], + ], + ); + expect_get_drop_ammounts( + 5, + 3, + false, + vec![ + [1, 1, 3, 0, 0], + [1, 2, 2, 0, 0], + [1, 3, 1, 0, 0], + [1, 4, 0, 0, 0], + [2, 1, 2, 0, 0], + [2, 2, 1, 0, 0], + [2, 3, 0, 0, 0], + [3, 1, 1, 0, 0], + [3, 2, 0, 0, 0], + [4, 1, 0, 0, 0], + [5, 0, 0, 0, 0], + ], + ); + expect_get_drop_ammounts( + 5, + 2, + false, + vec![ + [1, 4, 0, 0, 0], + [2, 3, 0, 0, 0], + [3, 2, 0, 0, 0], + [4, 1, 0, 0, 0], + [5, 0, 0, 0, 0], + ], + ); + expect_get_drop_ammounts(5, 1, false, vec![[5, 0, 0, 0, 0]]); +} + +#[test] +fn test_capstone_drops() { + expect_get_drop_ammounts(1, 1, true, vec![[1, 0]]); + expect_get_drop_ammounts(2, 2, true, vec![[1, 1, 0, 0, 0], [2, 0, 0, 0, 0]]); + expect_get_drop_ammounts::<5>(2, 1, true, vec![]); + expect_get_drop_ammounts( + 3, + 3, + true, + vec![ + [1, 1, 1, 0, 0], + [1, 2, 0, 0, 0], + [2, 1, 0, 0, 0], + [3, 0, 0, 0, 0], + ], + ); + expect_get_drop_ammounts(3, 2, true, vec![[2, 1, 0, 0, 0], [3, 0, 0, 0, 0]]); + expect_get_drop_ammounts( + 4, + 4, + true, + vec![ + [1, 1, 1, 1, 0], + [1, 1, 2, 0, 0], + [1, 2, 1, 0, 0], + [1, 3, 0, 0, 0], + [2, 1, 1, 0, 0], + [2, 2, 0, 0, 0], + [3, 1, 0, 0, 0], + [4, 0, 0, 0, 0], + ], + ); + expect_get_drop_ammounts( + 4, + 3, + true, + vec![ + [1, 2, 1, 0, 0], + [1, 3, 0, 0, 0], + [2, 1, 1, 0, 0], + [2, 2, 0, 0, 0], + [3, 1, 0, 0, 0], + [4, 0, 0, 0, 0], + ], + ); + expect_get_drop_ammounts(4, 2, true, vec![[3, 1, 0, 0, 0], [4, 0, 0, 0, 0]]); + expect_get_drop_ammounts::<5>(4, 1, true, vec![]); + expect_get_drop_ammounts( + 5, + 5, + true, + vec![ + [1, 1, 1, 1, 1], + [1, 1, 1, 2, 0], + [1, 1, 2, 1, 0], + [1, 1, 3, 0, 0], + [1, 2, 1, 1, 0], + [1, 2, 2, 0, 0], + [1, 3, 1, 0, 0], + [1, 4, 0, 0, 0], + [2, 1, 1, 1, 0], + [2, 1, 2, 0, 0], + [2, 2, 1, 0, 0], + [2, 3, 0, 0, 0], + [3, 1, 1, 0, 0], + [3, 2, 0, 0, 0], + [4, 1, 0, 0, 0], + [5, 0, 0, 0, 0], + ], + ); + expect_get_drop_ammounts( + 5, + 4, + true, + vec![ + [1, 1, 2, 1, 0], + [1, 1, 3, 0, 0], + [1, 2, 1, 1, 0], + [1, 2, 2, 0, 0], + [1, 3, 1, 0, 0], + [1, 4, 0, 0, 0], + [2, 1, 1, 1, 0], + [2, 1, 2, 0, 0], + [2, 2, 1, 0, 0], + [2, 3, 0, 0, 0], + [3, 1, 1, 0, 0], + [3, 2, 0, 0, 0], + [4, 1, 0, 0, 0], + [5, 0, 0, 0, 0], + ], + ); + expect_get_drop_ammounts( + 5, + 3, + true, + vec![ + [1, 3, 1, 0, 0], + [1, 4, 0, 0, 0], + [2, 2, 1, 0, 0], + [2, 3, 0, 0, 0], + [3, 1, 1, 0, 0], + [3, 2, 0, 0, 0], + [4, 1, 0, 0, 0], + [5, 0, 0, 0, 0], + ], + ); + expect_get_drop_ammounts(5, 2, true, vec![[4, 1, 0, 0, 0], [5, 0, 0, 0, 0]]); + expect_get_drop_ammounts::<5>(5, 1, true, vec![]); +} + +#[test] +fn test_get_moves() { + use crate::{Board5x5, Color}; + + let from = (1, 2); + let mut tiles = Board5x5::new().tiles; + tiles[from.0 as usize][from.1 as usize] = Tile::new( + ColoredPiece::WhiteFlat, + &[Color::White, Color::Black, Color::White, Color::Black], + ); + let actual = get_movement_moves(from.0, from.1, &tiles); + + const LEN1_DROPS: [(u8, [u8; 5]); 5] = [ + (1, [1, 0, 0, 0, 0]), + (2, [2, 0, 0, 0, 0]), + (3, [3, 0, 0, 0, 0]), + (4, [4, 0, 0, 0, 0]), + (5, [5, 0, 0, 0, 0]), + ]; + const LEN2_DROPS: [(u8, [u8; 5]); 15] = [ + // 1 + (1, [1, 0, 0, 0, 0]), + // 2 + (2, [1, 1, 0, 0, 0]), + (2, [2, 0, 0, 0, 0]), + // 3 + (3, [1, 2, 0, 0, 0]), + (3, [2, 1, 0, 0, 0]), + (3, [3, 0, 0, 0, 0]), + // 4 + (4, [1, 3, 0, 0, 0]), + (4, [2, 2, 0, 0, 0]), + (4, [3, 1, 0, 0, 0]), + (4, [4, 0, 0, 0, 0]), + // 5 + (5, [1, 4, 0, 0, 0]), + (5, [2, 3, 0, 0, 0]), + (5, [3, 2, 0, 0, 0]), + (5, [4, 1, 0, 0, 0]), + (5, [5, 0, 0, 0, 0]), + ]; + const LEN3_DROPS: [(u8, [u8; 5]); 25] = [ + // 1 + (1, [1, 0, 0, 0, 0]), + // 2 + (2, [1, 1, 0, 0, 0]), + (2, [2, 0, 0, 0, 0]), + // 3 + (3, [1, 1, 1, 0, 0]), + (3, [1, 2, 0, 0, 0]), + (3, [2, 1, 0, 0, 0]), + (3, [3, 0, 0, 0, 0]), + // 4 + (4, [1, 1, 2, 0, 0]), + (4, [1, 2, 1, 0, 0]), + (4, [1, 3, 0, 0, 0]), + (4, [2, 1, 1, 0, 0]), + (4, [2, 2, 0, 0, 0]), + (4, [3, 1, 0, 0, 0]), + (4, [4, 0, 0, 0, 0]), + // 5 + (5, [1, 1, 3, 0, 0]), + (5, [1, 2, 2, 0, 0]), + (5, [1, 3, 1, 0, 0]), + (5, [1, 4, 0, 0, 0]), + (5, [2, 1, 2, 0, 0]), + (5, [2, 2, 1, 0, 0]), + (5, [2, 3, 0, 0, 0]), + (5, [3, 1, 1, 0, 0]), + (5, [3, 2, 0, 0, 0]), + (5, [4, 1, 0, 0, 0]), + (5, [5, 0, 0, 0, 0]), + ]; + + let east_list: Vec<_> = LEN3_DROPS + .into_iter() + .map(|(count, drop)| Move::Move { + from, + count, + dir: Direction::East, + drop, + }) + .collect(); + let north_list: Vec<_> = LEN2_DROPS + .into_iter() + .map(|(count, drop)| Move::Move { + from, + count, + dir: Direction::North, + drop, + }) + .collect(); + let west_list: Vec<_> = LEN1_DROPS + .into_iter() + .map(|(count, drop)| Move::Move { + from, + count, + dir: Direction::West, + drop, + }) + .collect(); + let south_list: Vec<_> = LEN2_DROPS + .into_iter() + .map(|(count, drop)| Move::Move { + from, + count, + dir: Direction::South, + drop, + }) + .collect(); + let expected: Vec<_> = east_list + .into_iter() + .chain(north_list.into_iter()) + .chain(west_list.into_iter()) + .chain(south_list.into_iter()) + .collect(); + + let result = assert_uo_list_eq(expected, actual); + + if let Err(err) = result { + panic!("incorrect move list!\n{}", err); + } +} + +#[test] +fn test_get_caps_moves() { + use crate::{Board5x5, Color}; + + let from = (1, 2); + let mut tiles = Board5x5::new().tiles; + tiles[from.0 as usize][from.1 as usize] = Tile::new( + ColoredPiece::WhiteCaps, + &[Color::White, Color::Black, Color::White, Color::Black], + ); + tiles[1][0] = Tile::new(ColoredPiece::WhiteWall, &[]); + tiles[1][3] = Tile::new(ColoredPiece::BlackWall, &[]); + tiles[4][2] = Tile::new(ColoredPiece::BlackWall, &[]); + let actual = get_movement_moves(from.0, from.1, &tiles); + + const LEN1_DROPS: [(u8, [u8; 5]); 5] = [ + (1, [1, 0, 0, 0, 0]), + (2, [2, 0, 0, 0, 0]), + (3, [3, 0, 0, 0, 0]), + (4, [4, 0, 0, 0, 0]), + (5, [5, 0, 0, 0, 0]), + ]; + const LEN1_CAPS_DROPS: [(u8, [u8; 5]); 1] = [(1, [1, 0, 0, 0, 0])]; + const LEN2_CAPS_DROPS: [(u8, [u8; 5]); 9] = [ + // 1 + (1, [1, 0, 0, 0, 0]), + // 2 + (2, [1, 1, 0, 0, 0]), + (2, [2, 0, 0, 0, 0]), + // 3 + (3, [2, 1, 0, 0, 0]), + (3, [3, 0, 0, 0, 0]), + // 4 + (4, [3, 1, 0, 0, 0]), + (4, [4, 0, 0, 0, 0]), + // 5 + (5, [4, 1, 0, 0, 0]), + (5, [5, 0, 0, 0, 0]), + ]; + const LEN3_CAPS_DROPS: [(u8, [u8; 5]); 21] = [ + // 1 + (1, [1, 0, 0, 0, 0]), + // 2 + (2, [1, 1, 0, 0, 0]), + (2, [2, 0, 0, 0, 0]), + // 3 + (3, [1, 1, 1, 0, 0]), + (3, [1, 2, 0, 0, 0]), + (3, [2, 1, 0, 0, 0]), + (3, [3, 0, 0, 0, 0]), + // 4 + (4, [1, 2, 1, 0, 0]), + (4, [1, 3, 0, 0, 0]), + (4, [2, 1, 1, 0, 0]), + (4, [2, 2, 0, 0, 0]), + (4, [3, 1, 0, 0, 0]), + (4, [4, 0, 0, 0, 0]), + // 5 + (5, [1, 3, 1, 0, 0]), + (5, [1, 4, 0, 0, 0]), + (5, [2, 2, 1, 0, 0]), + (5, [2, 3, 0, 0, 0]), + (5, [3, 1, 1, 0, 0]), + (5, [3, 2, 0, 0, 0]), + (5, [4, 1, 0, 0, 0]), + (5, [5, 0, 0, 0, 0]), + ]; + + let east_list: Vec<_> = LEN3_CAPS_DROPS + .into_iter() + .map(|(count, drop)| Move::Move { + from, + count, + dir: Direction::East, + drop, + }) + .collect(); + let north_list: Vec<_> = LEN1_CAPS_DROPS + .into_iter() + .map(|(count, drop)| Move::Move { + from, + count, + dir: Direction::North, + drop, + }) + .collect(); + let west_list: Vec<_> = LEN1_DROPS + .into_iter() + .map(|(count, drop)| Move::Move { + from, + count, + dir: Direction::West, + drop, + }) + .collect(); + let south_list: Vec<_> = LEN2_CAPS_DROPS + .into_iter() + .map(|(count, drop)| Move::Move { + from, + count, + dir: Direction::South, + drop, + }) + .collect(); + let expected: Vec<_> = east_list + .into_iter() + .chain(north_list.into_iter()) + .chain(west_list.into_iter()) + .chain(south_list.into_iter()) + .collect(); + + let result = assert_uo_list_eq(expected, actual); + + if let Err(err) = result { + panic!("incorrect move list!\n{}", err); + } +} + +fn assert_uo_list_eq( + expected: Vec, + actual: Vec, +) -> Result<(), String> { + use std::collections::HashSet; + + let actual_set: HashSet = actual.iter().cloned().collect(); + let expected_set: HashSet = expected.iter().cloned().collect(); + + if actual_set != expected_set { + Err(list_diff(expected, actual, expected_set, actual_set)) + } else { + Ok(()) + } +} + +fn list_diff( + expected: Vec, + actual: Vec, + expected_set: std::collections::HashSet, + actual_set: std::collections::HashSet, +) -> String { + let mut diff = String::new(); + for entry in expected { + if actual_set.contains(&entry) { + diff += &format!("\t{:?}\t{:?}\n", entry, entry); + } else { + diff += &format!("\t{:?}\n", entry); + } + } + for entry in actual { + if expected_set.contains(&entry) { + continue; + } + let blank = format!("{:?}", entry) + .chars() + .map(|_| ' ') + .collect::(); + diff += &format!("\t{}\t{:?}\n", blank, entry); + } + diff +} diff --git a/tak_lib/src/tile.rs b/tak_lib/src/tile.rs index 74085d6..f3642e1 100644 --- a/tak_lib/src/tile.rs +++ b/tak_lib/src/tile.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; -use crate::piece::ColoredPiece; +use crate::piece::{Color, ColoredPiece}; /// A tile on the board. /// @@ -41,6 +41,18 @@ impl Tile { const STACK_OFFSET: u8 = 16; const STACK_MASK: u128 = !0xFFFFu128; + pub fn new(top_piece: ColoredPiece, stack: &[Color]) -> Self { + let mut tile = Tile::EMPTY; + tile.set_top_piece(top_piece); + tile.set_stack_len(stack.len() as u8); + let mut stack_bits = 0u128; + for c in stack.iter().rev() { + stack_bits = (stack_bits << 1) | (*c) as u128 + } + tile.set_stack(stack_bits); + tile + } + /// Gets the top piece. pub fn top_piece(&self) -> ColoredPiece { ColoredPiece::from_u8((self.0 & 0b111) as u8)