adding docs, adj/jumpable direction funcs, starting can move

This commit is contained in:
andy 2021-06-26 22:57:01 +01:00
parent 3734d68a64
commit 74708f147f
4 changed files with 217 additions and 16 deletions

View File

@ -52,6 +52,10 @@ impl Display for SquareState {
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Moveable { pub enum Moveable {
Allowed = 0, Allowed = 0,
Occupied = 1, UnoccupiedSrc = 1,
OutOfBounds = 2, OccupiedDest = 2,
OutOfBounds = 3,
Unplayable = 4,
WrongTeamSrc = 5,
IllegalTrajectory = 6,
} }

View File

@ -1,3 +1,4 @@
//! Board module for components related to the checkers board and game structure
pub mod enums; pub mod enums;
use enums::*; use enums::*;
@ -11,9 +12,12 @@ use std::option::Option;
extern crate wasm_bindgen; extern crate wasm_bindgen;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
/// Standard width of a checkers board is 8 squares
pub const STD_WIDTH: usize = 8; pub const STD_WIDTH: usize = 8;
/// Standard height of a checkers board is 8 squares
pub const STD_HEIGHT: usize = 8; pub const STD_HEIGHT: usize = 8;
/// Model a game piece by its team and strength (normal or kinged)
#[wasm_bindgen] #[wasm_bindgen]
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Piece { 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)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Direction<T: Clone + Copy> { pub struct Direction<T: Clone + Copy> {
/// North West
nw: Option<T>, nw: Option<T>,
/// North East
ne: Option<T>, ne: Option<T>,
/// South East
se: Option<T>, se: Option<T>,
/// South West
sw: Option<T>, sw: Option<T>,
} }
impl<T: Clone + Copy> Direction<T> { impl<T: Clone + Copy> Direction<T> {
/// Create an empty direction full of [`Option::None`]
pub fn empty() -> Direction<T> { pub fn empty() -> Direction<T> {
Direction { Direction {
nw: Option::None, nw: Option::None,
@ -48,10 +62,13 @@ impl<T: Clone + Copy> Direction<T> {
} }
} }
/// Model board squares by a state and a possible occupying game piece
#[wasm_bindgen] #[wasm_bindgen]
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Square { pub struct Square {
/// Game piece if square is occupied
occupant: Option<Piece>, occupant: Option<Piece>,
/// Description of whether the square is occupied or an unplayable, i.e. off-lattice square
state: SquareState 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] #[wasm_bindgen]
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BrdIdx { 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 // BOARD
/////////////// ///////////////
@ -88,6 +112,7 @@ impl BrdIdx {
#[wasm_bindgen] #[wasm_bindgen]
#[derive(Clone)] #[derive(Clone)]
pub struct Board { pub struct Board {
/// 1D backing array of board squares for the 2D game board
cells: Vec<Square>, cells: Vec<Square>,
width: usize, width: usize,
height: usize, height: usize,
@ -96,10 +121,18 @@ pub struct Board {
} }
impl 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 { pub fn cell_mut(&mut self, idx: usize) -> &mut Square {
&mut self.cells[idx] &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<usize>): 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<Vec<usize>> { pub fn diagonal_indices(&self, idx: BrdIdx) -> Option<Vec<usize>> {
if self.cell_state(self.cell_idx(idx)) == SquareState::Unplayable { if self.cell_state(self.cell_idx(idx)) == SquareState::Unplayable {
return None; return None;
@ -109,21 +142,18 @@ impl Board {
let width_idx = self.width - 1; let width_idx = self.width - 1;
let mut cells = Vec::with_capacity(4); let mut cells = Vec::with_capacity(4);
let mut dir = Direction::empty();
if idx.row > 0 { if idx.row > 0 {
if idx.col > 0 { if idx.col > 0 {
cells.push( cells.push(
self.cell_index(idx.row - 1, idx.col - 1) 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 { if idx.col < width_idx {
cells.push( cells.push(
self.cell_index(idx.row - 1, idx.col + 1) 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( cells.push(
self.cell_index(idx.row + 1, idx.col - 1) 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 { if idx.col < width_idx {
cells.push( cells.push(
self.cell_index(idx.row + 1, idx.col + 1) 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(); cells.shrink_to_fit();
Some(cells) 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<Square>): A [`Direction`] structure for the diagonally adjacent squares.
pub fn adjacent_dir(&self, idx: BrdIdx) -> Option<Direction<Square>> {
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<usize>) -> Vec<usize> { pub fn filter_indices(&self, idx: BrdIdx, player: Team, indices: Vec<usize>) -> Vec<usize> {
indices.into_iter().filter(|i| { indices.into_iter().filter(|i| {
match player { 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<usize>): 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<Vec<usize>> { pub fn jumpable_indices(&self, idx: BrdIdx) -> Option<Vec<usize>> {
if self.cell_state(self.cell_idx(idx)) == SquareState::Unplayable { if self.cell_state(self.cell_idx(idx)) == SquareState::Unplayable {
return None; return None;
@ -206,6 +282,47 @@ impl Board {
Some(cells) 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<Square>): A [`Direction`] structure for the diagonally jumpable squares.
pub fn jumpable_dir(&self, idx: BrdIdx) -> Option<Direction<Square>> {
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<Vec<usize>> { pub fn player_jumpable_indices(&self, idx: BrdIdx, player: Team) -> Option<Vec<usize>> {
match self.jumpable_indices(idx) { match self.jumpable_indices(idx) {
Some(x) => Some(self.filter_indices(idx, player, x)), Some(x) => Some(self.filter_indices(idx, player, x)),
@ -216,32 +333,98 @@ impl Board {
#[wasm_bindgen] #[wasm_bindgen]
impl Board { impl Board {
/// Get a copy of a board square by 1D array index
pub fn cell(&self, idx: usize) -> Square { pub fn cell(&self, idx: usize) -> Square {
self.cells[idx] self.cells[idx]
} }
/// Get a copy of a board square by 2D [`BrdIdx`] index
pub fn grid_cell(&self, idx: BrdIdx) -> Square { pub fn grid_cell(&self, idx: BrdIdx) -> Square {
self.cell(self.cell_idx(idx)) 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 { pub fn cell_index(&self, row: usize, col: usize) -> usize {
(row * self.width) + col (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 { pub fn cell_idx(&self, idx: BrdIdx) -> usize {
self.cell_index(idx.row, idx.col) 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 { pub fn board_index(&self, idx: usize) -> BrdIdx {
let row = idx / self.width; let row = idx / self.width;
let col = idx - (row * self.width); let col = idx - (row * self.width);
BrdIdx::from(row, col) BrdIdx::from(row, col)
} }
// pub fn can_move(&self, from: BrdIdx, to: BrdIdx) -> bool { pub fn can_move(&self, from: BrdIdx, to: BrdIdx) -> Moveable {
// let diagonals = self.diagonal_indices(from);
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 { pub fn new(width: usize, height: usize) -> Board {
let total_cells = width * height; 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 { pub fn init_game(board: Board) -> Board {
let mut new_board = board.clone(); let mut new_board = board.clone();
for (idx, row) in RowSquareIterator::new(&board).enumerate() { for (idx, row) in RowSquareIterator::new(&board).enumerate() {
@ -302,18 +486,22 @@ impl Board {
new_board new_board
} }
/// Get the [`Board::current_turn`] parameter
pub fn current_turn(&self) -> Team { pub fn current_turn(&self) -> Team {
self.current_turn self.current_turn
} }
/// Get a pointer to the backing array of board squares, [`Board::cells`]
pub fn cells(&self) -> *const Square { pub fn cells(&self) -> *const Square {
self.cells.as_ptr() self.cells.as_ptr()
} }
/// Get the number of board squares
pub fn num_cells(&self) -> usize { pub fn num_cells(&self) -> usize {
self.cells.len() self.cells.len()
} }
/// Get the state of a board square by 1D array index
pub fn cell_state(&self, idx: usize) -> SquareState { pub fn cell_state(&self, idx: usize) -> SquareState {
self.cell(idx).state self.cell(idx).state
} }
@ -524,11 +712,11 @@ mod tests {
assert_eq!(Some(vec![42]), board.jumpable_indices(BrdIdx::from(7, 0))); assert_eq!(Some(vec![42]), board.jumpable_indices(BrdIdx::from(7, 0)));
} }
#[wasm_bindgen_test] // #[wasm_bindgen_test]
fn init_game() { // fn init_game() {
let board = Board::init_game(Board::new(8, 8)); // let board = Board::init_game(Board::new(8, 8));
log!("{}", board); // log!("{}", board);
} // }
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn black_diagonal_indices() { fn black_diagonal_indices() {

View File

@ -4,6 +4,7 @@ use indextree::Arena;
extern crate wasm_bindgen; extern crate wasm_bindgen;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
/// Root-level structure for managing the game as a collection of board states
#[wasm_bindgen] #[wasm_bindgen]
pub struct Game { pub struct Game {
current: Board, current: Board,

View File

@ -1,3 +1,7 @@
//! Draught
//!
//! An implementation of checkers/draughts in Rust WebAssembly with a minimax AI player
pub mod board; pub mod board;
pub mod utils; pub mod utils;
pub mod game; pub mod game;
@ -5,12 +9,16 @@ pub mod game;
extern crate wasm_bindgen; extern crate wasm_bindgen;
use wasm_bindgen::prelude::*; 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 // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator. // allocator.
// #[cfg(feature = "wee_alloc")] // #[cfg(feature = "wee_alloc")]
// #[global_allocator] // #[global_allocator]
// static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; // 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_export]
macro_rules! log { macro_rules! log {
( $( $t:tt )* ) => { ( $( $t:tt )* ) => {