diff --git a/src/board/enums.rs b/src/board/enums.rs index 704ecbe..bf16ac2 100644 --- a/src/board/enums.rs +++ b/src/board/enums.rs @@ -52,6 +52,10 @@ impl Display for SquareState { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Moveable { Allowed = 0, - Occupied = 1, - OutOfBounds = 2, + UnoccupiedSrc = 1, + OccupiedDest = 2, + OutOfBounds = 3, + Unplayable = 4, + WrongTeamSrc = 5, + IllegalTrajectory = 6, } \ No newline at end of file diff --git a/src/board/mod.rs b/src/board/mod.rs index 53eea94..885b05d 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,3 +1,4 @@ +//! Board module for components related to the checkers board and game structure pub mod enums; use enums::*; @@ -11,9 +12,12 @@ use std::option::Option; extern crate wasm_bindgen; use wasm_bindgen::prelude::*; +/// Standard width of a checkers board is 8 squares pub const STD_WIDTH: usize = 8; +/// Standard height of a checkers board is 8 squares pub const STD_HEIGHT: usize = 8; +/// Model a game piece by its team and strength (normal or kinged) #[wasm_bindgen] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Piece { @@ -29,15 +33,25 @@ impl Piece { } } +/// Model the standard diagonal movements by north west/east etc +/// +/// Used as an absolute measure, i.e. not relative to the team making a move +/// +/// Use options for when movements are blocked/unallowed contextually #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Direction { + /// North West nw: Option, + /// North East ne: Option, + /// South East se: Option, + /// South West sw: Option, } impl Direction { + /// Create an empty direction full of [`Option::None`] pub fn empty() -> Direction { Direction { nw: Option::None, @@ -48,10 +62,13 @@ impl Direction { } } +/// Model board squares by a state and a possible occupying game piece #[wasm_bindgen] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Square { + /// Game piece if square is occupied occupant: Option, + /// Description of whether the square is occupied or an unplayable, i.e. off-lattice square state: SquareState } @@ -64,6 +81,7 @@ impl Square { } } +/// Model a rank 2 tensor index to identify a board square by row and column #[wasm_bindgen] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct BrdIdx { @@ -80,6 +98,12 @@ impl BrdIdx { } } +impl Display for BrdIdx { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "({}, {})", self.row, self.col) + } +} + /////////////// // BOARD /////////////// @@ -88,6 +112,7 @@ impl BrdIdx { #[wasm_bindgen] #[derive(Clone)] pub struct Board { + /// 1D backing array of board squares for the 2D game board cells: Vec, width: usize, height: usize, @@ -96,10 +121,18 @@ pub struct Board { } impl Board { + /// Get a mutable reference to a board square by 1D array index pub fn cell_mut(&mut self, idx: usize) -> &mut Square { &mut self.cells[idx] } + /// Get the 1D array indices for the diagonally adjacent squares of a given board square + /// + /// # Returns + /// [`None`]: If the given square is unplayable + /// + /// Some(Vec): A variable length vector of 1D indices for diagonally adjacent squares. + /// Vector may be between 1 and 4 items long depending on the location of the given square pub fn diagonal_indices(&self, idx: BrdIdx) -> Option> { if self.cell_state(self.cell_idx(idx)) == SquareState::Unplayable { return None; @@ -109,21 +142,18 @@ impl Board { let width_idx = self.width - 1; let mut cells = Vec::with_capacity(4); - let mut dir = Direction::empty(); if idx.row > 0 { if idx.col > 0 { cells.push( self.cell_index(idx.row - 1, idx.col - 1) ); - dir.nw = Option::Some(self.cell(self.cell_index(idx.row - 1, idx.col - 1))); } if idx.col < width_idx { cells.push( self.cell_index(idx.row - 1, idx.col + 1) ); - dir.ne = Option::Some(self.cell(self.cell_index(idx.row - 1, idx.col + 1))); } } @@ -132,22 +162,61 @@ impl Board { cells.push( self.cell_index(idx.row + 1, idx.col - 1) ); - dir.sw = Option::Some(self.cell(self.cell_index(idx.row + 1, idx.col - 1))); } if idx.col < width_idx { cells.push( self.cell_index(idx.row + 1, idx.col + 1) ); - dir.se = Option::Some(self.cell(self.cell_index(idx.row + 1, idx.col + 1))); } } cells.shrink_to_fit(); Some(cells) - // Some(dir) } + /// Get a [`Direction`] structure for the diagonally adjacent squares of a given board square + /// + /// Similar to the [`Board::diagonal_indices`] function with differently formatted results + /// + /// # Returns + /// [`None`]: If the given square is unplayable + /// + /// Some(Direction): A [`Direction`] structure for the diagonally adjacent squares. + pub fn adjacent_dir(&self, idx: BrdIdx) -> Option> { + if self.cell_state(self.cell_idx(idx)) == SquareState::Unplayable { + return None; + } + + let height_idx = self.height - 1; + let width_idx = self.width - 1; + + let mut dir = Direction::empty(); + + if idx.row > 0 { + if idx.col > 0 { + dir.nw = Option::Some(self.cell(self.cell_index(idx.row - 1, idx.col - 1))); + } + + if idx.col < width_idx { + dir.ne = Option::Some(self.cell(self.cell_index(idx.row - 1, idx.col + 1))); + } + } + + if idx.row < height_idx { + if idx.col > 0 { + dir.sw = Option::Some(self.cell(self.cell_index(idx.row + 1, idx.col - 1))); + } + + if idx.col < width_idx { + dir.se = Option::Some(self.cell(self.cell_index(idx.row + 1, idx.col + 1))); + } + } + + Some(dir) + } + + /// Filter an array of diagonal indices (Like those from [`Board::diagonal_indices`], [`Board::jumpable_indices`]) for those that are legal for a team's man, i.e. solely up or down the board pub fn filter_indices(&self, idx: BrdIdx, player: Team, indices: Vec) -> Vec { indices.into_iter().filter(|i| { match player { @@ -164,6 +233,13 @@ impl Board { } } + /// Get the 1D array indices for the diagonally jumpable squares of a given board square + /// + /// # Returns + /// [`None`]: If the given square is unplayable + /// + /// Some(Vec): A variable length vector of 1D indices for diagonally jumpable squares. + /// Vector may be between 1 and 4 items long depending on the location of the given square pub fn jumpable_indices(&self, idx: BrdIdx) -> Option> { if self.cell_state(self.cell_idx(idx)) == SquareState::Unplayable { return None; @@ -206,6 +282,47 @@ impl Board { Some(cells) } + /// Get a [`Direction`] structure for the diagonally jumpable squares of a given board square + /// + /// Similar to the [`Board::jumpable_indices`] function with differently formatted results + /// + /// # Returns + /// [`None`]: If the given square is unplayable + /// + /// Some(Direction): A [`Direction`] structure for the diagonally jumpable squares. + pub fn jumpable_dir(&self, idx: BrdIdx) -> Option> { + if self.cell_state(self.cell_idx(idx)) == SquareState::Unplayable { + return None; + } + + let height_idx = self.height - 1; + let width_idx = self.width - 1; + + let mut dir = Direction::empty(); + + if idx.row > 1 { + if idx.col > 1 { + dir.nw = Option::Some(self.cell(self.cell_index(idx.row - 2, idx.col - 2))); + } + + if idx.col < width_idx - 1 { + dir.ne = Option::Some(self.cell(self.cell_index(idx.row - 2, idx.col + 2))); + } + } + + if idx.row < height_idx - 1 { + if idx.col > 1 { + dir.sw = Option::Some(self.cell(self.cell_index(idx.row + 2, idx.col - 2))); + } + + if idx.col < width_idx - 1 { + dir.se = Option::Some(self.cell(self.cell_index(idx.row + 2, idx.col + 2))); + } + } + + Some(dir) + } + pub fn player_jumpable_indices(&self, idx: BrdIdx, player: Team) -> Option> { match self.jumpable_indices(idx) { Some(x) => Some(self.filter_indices(idx, player, x)), @@ -216,32 +333,98 @@ impl Board { #[wasm_bindgen] impl Board { + /// Get a copy of a board square by 1D array index pub fn cell(&self, idx: usize) -> Square { self.cells[idx] } + /// Get a copy of a board square by 2D [`BrdIdx`] index pub fn grid_cell(&self, idx: BrdIdx) -> Square { self.cell(self.cell_idx(idx)) } + /// Transform a 2D row/column board index into a single 1D index for use with [`Board::cells`] pub fn cell_index(&self, row: usize, col: usize) -> usize { (row * self.width) + col } + /// Similar to [`Board::cell_index`] but with a [`BrdIdx`] instead of separate indices. Transform a 2D row/column board index into a single 1D index for use with [`Board::cells`] pub fn cell_idx(&self, idx: BrdIdx) -> usize { self.cell_index(idx.row, idx.col) } + /// Transform a 1D array index (for [`Board::cells`]) into a 2D game board index (by row/col) pub fn board_index(&self, idx: usize) -> BrdIdx { let row = idx / self.width; let col = idx - (row * self.width); BrdIdx::from(row, col) } - // pub fn can_move(&self, from: BrdIdx, to: BrdIdx) -> bool { - // let diagonals = self.diagonal_indices(from); - // } + pub fn can_move(&self, from: BrdIdx, to: BrdIdx) -> Moveable { + if from.row > self.height - 1 || from.col > self.width - 1 { + return Moveable::OutOfBounds; + } + + let from_square = self.cell(self.cell_idx(from)); + + match from_square.state { + Empty => return Moveable::UnoccupiedSrc, + Unplayable => return Moveable::Unplayable, + Occupied => { + + // if its not the current teams piece then error + match from_square.occupant { + None => panic!("Square is apparently occupied, but no occupant was found from: {}, to: {}, square: {:?}", from, to, from_square), + Some(x) => { + + // piece in the source square is not for the current turn's player + if x.team != self.current_turn { + return Moveable::WrongTeamSrc; + } + + // TODO: refactor to a IsMove()/IsJump() to check whether the move has a legal trajectory + match x.strength { + Man => { + match self.current_turn { + Black => { + + }, + White => { + + }, + }; + }, + King => { + match self.current_turn { + Black => { + + }, + White => { + + }, + }; + }, + }; + + // let diagonal = self.adjacent_dir(from); + // let allowable_squares = Vec::with_capacity(4); + + let jumpable = self.jumpable_dir(from); + } + } + }, + } + + // let is_adjacent = match self.current_turn { + // Team::Black => diagonal.nw, + // Team::White => {}, + // } + + Moveable::Allowed + } + + /// Iniitalise a game board without game pieces pub fn new(width: usize, height: usize) -> Board { let total_cells = width * height; @@ -270,6 +453,7 @@ impl Board { } } + /// Reset the given board to a starting layout with 3 rows of opposing pieces pub fn init_game(board: Board) -> Board { let mut new_board = board.clone(); for (idx, row) in RowSquareIterator::new(&board).enumerate() { @@ -302,18 +486,22 @@ impl Board { new_board } + /// Get the [`Board::current_turn`] parameter pub fn current_turn(&self) -> Team { self.current_turn } + /// Get a pointer to the backing array of board squares, [`Board::cells`] pub fn cells(&self) -> *const Square { self.cells.as_ptr() } + /// Get the number of board squares pub fn num_cells(&self) -> usize { self.cells.len() } + /// Get the state of a board square by 1D array index pub fn cell_state(&self, idx: usize) -> SquareState { self.cell(idx).state } @@ -524,11 +712,11 @@ mod tests { assert_eq!(Some(vec![42]), board.jumpable_indices(BrdIdx::from(7, 0))); } - #[wasm_bindgen_test] - fn init_game() { - let board = Board::init_game(Board::new(8, 8)); - log!("{}", board); - } + // #[wasm_bindgen_test] + // fn init_game() { + // let board = Board::init_game(Board::new(8, 8)); + // log!("{}", board); + // } #[wasm_bindgen_test] fn black_diagonal_indices() { diff --git a/src/game.rs b/src/game.rs index ed2e4cd..71d93e6 100644 --- a/src/game.rs +++ b/src/game.rs @@ -4,6 +4,7 @@ use indextree::Arena; extern crate wasm_bindgen; use wasm_bindgen::prelude::*; +/// Root-level structure for managing the game as a collection of board states #[wasm_bindgen] pub struct Game { current: Board, diff --git a/src/lib.rs b/src/lib.rs index 92184d1..737cc62 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,7 @@ +//! Draught +//! +//! An implementation of checkers/draughts in Rust WebAssembly with a minimax AI player + pub mod board; pub mod utils; pub mod game; @@ -5,12 +9,16 @@ pub mod game; extern crate wasm_bindgen; use wasm_bindgen::prelude::*; +pub use board::Board; +pub use game::Game; + // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. // #[cfg(feature = "wee_alloc")] // #[global_allocator] // static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; +/// Wrap the [`web_sys`] access to the browser console in a macro for easy logging #[macro_export] macro_rules! log { ( $( $t:tt )* ) => {