From 8dbc2eb5863292f2c52de94202221f0a620aadce Mon Sep 17 00:00:00 2001 From: andy Date: Wed, 30 Jun 2021 00:43:39 +0100 Subject: [PATCH] moving working and testing, dev/prod webpack --- .github/workflows/test.yml | 6 +- src/board/iter.rs | 96 ++++- src/board/mod.rs | 248 +++++++++---- src/board/tests.rs | 352 +++++++++++++++++-- src/game.rs | 13 - src/game/mod.rs | 142 ++++++++ src/game/tests.rs | 59 ++++ src/lib.rs | 1 + src/player.rs | 8 + www/package-lock.json | 3 +- www/package.json | 8 +- www/{webpack.config.js => webpack.common.js} | 1 - www/webpack.dev.js | 8 + www/webpack.prod.js | 7 + 14 files changed, 820 insertions(+), 132 deletions(-) delete mode 100644 src/game.rs create mode 100644 src/game/mod.rs create mode 100644 src/game/tests.rs create mode 100644 src/player.rs rename www/{webpack.config.js => webpack.common.js} (94%) create mode 100644 www/webpack.dev.js create mode 100644 www/webpack.prod.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5c8e29a..d3b9558 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,12 +18,12 @@ jobs: - name: Install wasm-pack uses: jetli/wasm-pack-action@v0.3.0 - - name: Build Rust for WASM - run: wasm-pack build - - name: Test WASM in-browser run: wasm-pack test --firefox --chrome --headless + - name: Build Rust for WASM + run: wasm-pack build + - name: Install Node uses: actions/setup-node@v2 with: diff --git a/src/board/iter.rs b/src/board/iter.rs index 6a27496..a67c9eb 100644 --- a/src/board/iter.rs +++ b/src/board/iter.rs @@ -1,4 +1,5 @@ -use crate::board::{Board, Square}; +use crate::board::{Board, Piece, Square}; +use crate::board::enums::*; pub struct RowIndexIterator<'a> { board: &'a Board, @@ -67,17 +68,51 @@ impl<'a> Iterator for RowSquareIterator<'a> { } } +pub struct PieceIterator<'a> { + board: &'a Board, + index_cursor: usize, +} + +impl<'a> PieceIterator<'a> { + pub fn new(board: &'a Board) -> Self { + Self { + board, + index_cursor: 0 + } + } +} + +impl<'a> Iterator for PieceIterator<'a> { + type Item = (usize, Square); + + /// Get next item from the iterator + fn next(&mut self) -> Option<(usize, Square)> { + + while self.index_cursor < self.board.num_cells() - 1 { + self.index_cursor += 1; + match self.board.cell(self.index_cursor).state { + SquareState::Empty | SquareState::Unplayable => continue, + SquareState::Occupied => return Some((self.index_cursor, self.board.cell(self.index_cursor))), + } + } + + None + } +} + #[cfg(test)] -mod tests { +pub mod tests { use super::*; use crate::board::enums::SquareState; use wasm_bindgen_test::*; + use crate::log; + wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] fn index_iterator() { - let board = Board::new(2, 2); + let board = Board::new(2, 2, Team::Black); let iter = RowIndexIterator::new(&board); let collected: Vec> = iter.collect(); assert_eq!(vec![ @@ -88,7 +123,7 @@ mod tests { #[wasm_bindgen_test] fn square_iterator() { - let board = Board::new(2, 2); + let board = Board::new(2, 2, Team::Black); let iter = RowSquareIterator::new(&board); let collected: Vec> = iter.collect(); assert_eq!(vec![ @@ -103,4 +138,57 @@ mod tests { ], collected); } + #[wasm_bindgen_test] + fn piece_iterator_one_piece() { + let idx = 2; + + let mut board = Board::new(4, 4, Team::Black); + board.set_cell(idx, Square::new( + SquareState::Occupied, + Some( + Piece::new(Team::White, Strength::Man) + ) + )); + + let iter = PieceIterator::new(&board); + let collected: Vec<(usize, Square)> = iter.collect(); + + assert_eq!(collected.len(), 1); + assert_eq!(collected[0], (idx, board.cell(idx))); + } + + #[wasm_bindgen_test] + fn piece_iterator_multiple_pieces() { + + let mut board = Board::new(4, 4, Team::Black); + board.set_cell(2, Square::new( + SquareState::Occupied, + Some( + Piece::new(Team::White, Strength::Man) + ) + )); + + board.set_cell(4, Square::new( + SquareState::Occupied, + Some( + Piece::new(Team::Black, Strength::Man) + ) + )); + + board.set_cell(5, Square::new( + SquareState::Occupied, + Some( + Piece::new(Team::White, Strength::King) + ) + )); + + let iter = PieceIterator::new(&board); + let collected: Vec<(usize, Square)> = iter.collect(); + + assert_eq!(collected.len(), 3); + assert_eq!(collected[0], (2, board.cell(2))); + assert_eq!(collected[1], (4, board.cell(4))); + assert_eq!(collected[2], (5, board.cell(5))); + } + } \ No newline at end of file diff --git a/src/board/mod.rs b/src/board/mod.rs index 4889ded..ca8ddf7 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -4,6 +4,9 @@ pub mod enums; use enums::*; +use enums::Team::*; +use enums::Strength::*; +use enums::SquareState::*; pub mod iter; use iter::*; @@ -19,7 +22,7 @@ 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) +/// Game piece given by its team and strength (normal or kinged) #[wasm_bindgen] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Piece { @@ -27,7 +30,9 @@ pub struct Piece { strength: Strength } +#[wasm_bindgen] impl Piece { + #[wasm_bindgen(constructor)] pub fn new(team: Team, strength: Strength) -> Piece { Piece { team, strength @@ -35,7 +40,7 @@ impl Piece { } } -/// Model the standard diagonal movements by north west/east etc +/// Standard diagonal movements given by north west/east etc /// /// Used as an absolute measure, i.e. not relative to the team making a move /// @@ -64,7 +69,7 @@ impl Direction { } } -/// Model board squares by a state and a possible occupying game piece +/// Board squares given by a state and a possible occupying game piece #[wasm_bindgen] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Square { @@ -74,16 +79,32 @@ pub struct Square { state: SquareState } +#[wasm_bindgen] impl Square { + #[wasm_bindgen(constructor)] pub fn new(state: SquareState, occupant: Option) -> Square{ Square { occupant, state } } + + pub fn pc(team: Team, strength: Strength) -> Square { + Square { + occupant: Some(Piece::new(team, strength)), + state: Occupied, + } + } + + pub fn empty() -> Square { + Square { + occupant: None, + state: Empty, + } + } } -/// Model a rank 2 tensor index to identify a board square by row and column +/// Rank 2 tensor index to identify a board square by row and column #[wasm_bindgen] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct BrdIdx { @@ -110,9 +131,9 @@ impl Display for BrdIdx { // BOARD /////////////// -/// Models a single state for a checkers board +/// Single state of a checkers board #[wasm_bindgen] -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Board { /// 1D backing array of board squares for the 2D game board cells: Vec, @@ -122,6 +143,10 @@ pub struct Board { current_turn: Team } +/////////////////// +// PRIV FUNCS +/////////////////// + impl Board { /// Get a mutable reference to a board square by 1D array index pub fn cell_mut(&mut self, idx: usize) -> &mut Square { @@ -136,7 +161,7 @@ impl Board { /// 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 { + if self.cell_state(self.cell_idx(idx)) == Unplayable { return None; } @@ -186,7 +211,7 @@ impl Board { /// /// 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 { + if self.cell_state(self.cell_idx(idx)) == Unplayable { return None; } @@ -222,8 +247,8 @@ impl Board { pub fn filter_indices(&self, idx: BrdIdx, player: Team, indices: Vec) -> Vec { indices.into_iter().filter(|i| { match player { - Team::Black => self.board_index(*i).row < idx.row, - Team::White => self.board_index(*i).row > idx.row, + Black => self.board_index(*i).row < idx.row, + White => self.board_index(*i).row > idx.row, } }).collect() } @@ -243,7 +268,7 @@ impl Board { /// 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 { + if self.cell_state(self.cell_idx(idx)) == Unplayable { return None; } @@ -293,7 +318,7 @@ impl Board { /// /// 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 { + if self.cell_state(self.cell_idx(idx)) == Unplayable { return None; } @@ -331,8 +356,18 @@ impl Board { None => None } } + + /// cast the signed + pub fn idx_diffs(from: BrdIdx, to: BrdIdx) -> (isize, isize) { + // cast to signed ints so that -1 will work for black moves + (to.row as isize - from.row as isize, to.col as isize - from.col as isize) + } } +/////////////////// +// BOUND FUNCS +/////////////////// + #[wasm_bindgen] impl Board { /// Get a copy of a board square by 1D array index @@ -340,6 +375,15 @@ impl Board { self.cells[idx] } + /// Get a copy of a board square by 1D array index + pub fn set_cell(&mut self, idx: usize, square: Square) { + // TODO: handle this error better? + if idx >= self.num_cells() { + panic!("Given index is too large, idx: {}, square: {:?}", idx, square); + } + self.cells[idx] = square; + } + /// 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)) @@ -362,6 +406,7 @@ impl Board { BrdIdx::from(row, col) } + /// Check whether a move given by source and destination indices is legal pub fn can_move(&self, from: BrdIdx, to: BrdIdx) -> Moveable { if from.row > self.height - 1 || from.col > self.width - 1 { @@ -376,9 +421,9 @@ impl Board { // check source square is occupied match from_square.state { - SquareState::Empty => return Moveable::UnoccupiedSrc, - SquareState::Unplayable => return Moveable::Unplayable, - SquareState::Occupied => { + Empty => return Moveable::UnoccupiedSrc, + Unplayable => return Moveable::Unplayable, + Occupied => { // if its not the current teams piece then error match from_square.occupant { @@ -391,29 +436,50 @@ impl Board { return Moveable::WrongTeamSrc; } - // cast to signed ints so that -1 will work for black moves - let row_diff: i32 = to.row as i32 - from.row as i32; - let col_diff: i32 = to.col as i32 - from.col as i32; - // depending on whether the piece is a king or not, the piece can make different moves + // below validate_*_move() functions just check whether the trajectory is valid i.e a single ajacacent diagonal move for moves and 2 squares for a jump + // this includes validating the jumpee when jumping + // + // we use the Allowed type to indicate that the trajectory check passed + // but we catch it instead of returning to allow further checks on + // the destination square // TODO: refactor to a IsMove()/IsJump() to check whether the move has a legal trajectory match from_square_occupant.strength { - Strength::Man => self.validate_man_move(from, to, row_diff, col_diff, from_square_occupant), - Strength::King => self.validate_king_move(from, to, row_diff, col_diff, from_square_occupant), + Man => { + let strength_check = self.validate_man_move(from, to, from_square_occupant); + if strength_check != Moveable::Allowed { + return strength_check; + } + }, + King => { + let strength_check = self.validate_king_move(from, to, from_square_occupant); + if strength_check != Moveable::Allowed { + return strength_check; + } + }, }; + + let to_square = self.cell(self.cell_idx(to)); + match to_square.state { + Empty => { + return Moveable::Allowed; + }, + Unplayable => return Moveable::Unplayable, + Occupied => return Moveable::OccupiedDest, + } } } }, } - - Moveable::Allowed } - pub fn validate_man_move(&self, from: BrdIdx, to: BrdIdx, row_diff: i32, col_diff: i32, from_square_occupant: Piece) -> Moveable { + pub fn validate_man_move(&self, from: BrdIdx, to: BrdIdx, from_square_occupant: Piece) -> Moveable { + let (row_diff, col_diff) = Board::idx_diffs(from, to); + // men can only move forwards, below is row difference for each team - let idx_scale: i32 = match self.current_turn { - Team::Black => -1, - Team::White => 1, + let idx_scale: isize = match self.current_turn { + Black => -1, + White => 1, }; // legal standard move @@ -433,14 +499,14 @@ impl Board { if col_diff.abs() == 2 { // piece to be jumped over - let jumpee = self.get_jumpee(from, row_diff, col_diff); + let jumpee = self.cell(self.jumpee_idx(from, to)); match jumpee.state { - SquareState::Empty => Moveable::NoJumpablePiece, - SquareState::Unplayable => panic!("Found an unplayable piece to try to jump over, from: {}, to: {}, jumpee: {:?}", from, to, jumpee), - SquareState::Occupied => { + Empty => Moveable::NoJumpablePiece, + Unplayable => panic!("Found an unplayable piece to try to jump over, from: {}, to: {}, jumpee: {:?}", from, to, jumpee), + Occupied => { // check whether jumpee is an opponent's piece - return Board::validate_jumpee(jumpee, from, to, from_square_occupant); + return Board::validate_jumpee(jumpee, from_square_occupant); }, } } @@ -455,7 +521,9 @@ impl Board { } } - pub fn validate_king_move(&self, from: BrdIdx, to: BrdIdx, row_diff: i32, col_diff: i32, from_square_occupant: Piece) -> Moveable { + pub fn validate_king_move(&self, from: BrdIdx, to: BrdIdx, from_square_occupant: Piece) -> Moveable { + let (row_diff, col_diff) = Board::idx_diffs(from, to); + // legal standard move if row_diff.abs() == 1 { // destination is directly to the left or right @@ -473,14 +541,14 @@ impl Board { if col_diff.abs() == 2 { // piece to be jumped over - let jumpee = self.get_jumpee(from, row_diff, col_diff); + let jumpee = self.cell(self.jumpee_idx(from, to)); match jumpee.state { - SquareState::Empty => Moveable::NoJumpablePiece, - SquareState::Unplayable => panic!("Found an unplayable piece to try to jump over, from: {}, to: {}, jumpee: {:?}", from, to, jumpee), - SquareState::Occupied => { + Empty => Moveable::NoJumpablePiece, + Unplayable => panic!("Found an unplayable piece to try to jump over, from: {}, to: {}, jumpee: {:?}", from, to, jumpee), + Occupied => { // check whether jumpee is an opponent's piece - return Board::validate_jumpee(jumpee, from, to, from_square_occupant); + return Board::validate_jumpee(jumpee, from_square_occupant); }, } } @@ -495,20 +563,20 @@ impl Board { } } - pub fn get_jumpee(&self, from: BrdIdx, row_diff: i32, col_diff: i32) -> Square { - self.cell( - self.cell_idx( - BrdIdx::from( - ((from.row as i32) + row_diff / 2) as usize, - ((from.col as i32) + col_diff / 2) as usize) - ) + pub fn jumpee_idx(&self, from: BrdIdx, to: BrdIdx) -> usize { + let (row_diff, col_diff) = Board::idx_diffs(from, to); + self.cell_idx( + BrdIdx::from( + ((from.row as isize) + row_diff / 2) as usize, + ((from.col as isize) + col_diff / 2) as usize) ) } - pub fn validate_jumpee(jumpee: Square, from: BrdIdx, to: BrdIdx, from_occ: Piece) -> Moveable { + /// Unwrap the jumpee piece from the square and [`Board::check_jumpee_team`] with [`Moveable`] response + pub fn validate_jumpee(jumpee: Square, from_occ: Piece) -> Moveable { // check whether jumpee is an opponent's piece match jumpee.occupant { - None => panic!("No occupant found when checking the jumpee, from: {}, to: {}, jumpee: {:?}", from, to, jumpee), + None => panic!("No occupant found when checking the from: {:?} , jumpee: {:?}", from_occ, jumpee), Some(jumpee_occupant_uw) => { if Board::check_jumpee_team(from_occ, jumpee_occupant_uw) { return Moveable::Allowed; @@ -520,13 +588,14 @@ impl Board { } } - + /// Check that the source piece and the jumpee are of opposing teams pub fn check_jumpee_team(from: Piece, jumpee: Piece) -> bool { return from.team.opponent() == jumpee.team } - /// Iniitalise a game board without game pieces - pub fn new(width: usize, height: usize) -> Board { + /// Initialise a game board without game pieces + #[wasm_bindgen(constructor)] + pub fn new(width: usize, height: usize, current_turn: Team) -> Board { let total_cells = width * height; let mut cells: Vec = Vec::with_capacity(total_cells); @@ -535,10 +604,10 @@ impl Board { for i in 0..height { for _ in 0..width { if playable { - cells.push(Square::new(SquareState::Empty, None)); + cells.push(Square::new(Empty, None)); } else { - cells.push(Square::new(SquareState::Unplayable, None)); + cells.push(Square::new(Unplayable, None)); } playable = !playable; } @@ -550,33 +619,27 @@ impl Board { width, height, - current_turn: Team::Black + current_turn } } /// 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, piece_rows: usize) -> Board { let mut new_board = board.clone(); for (idx, row) in RowSquareIterator::new(&board).enumerate() { for (jdx, square) in row.iter().enumerate() { - if square.state == SquareState::Empty || square.state == SquareState::Occupied { - if idx < 3 { + if square.state == Empty || square.state == Occupied { + if idx < piece_rows { let cell_idx = new_board.cell_index(idx, jdx); - new_board.cells[cell_idx] = Square::new( - SquareState::Occupied, - Some(Piece::new(Team::White, Strength::Man)) - ); - } else if idx >= board.height - 3 { + new_board.cells[cell_idx] = Square::pc(White, Man); + } else if idx >= board.height - piece_rows { let cell_idx = new_board.cell_index(idx, jdx); - new_board.cells[cell_idx] = Square::new( - SquareState::Occupied, - Some(Piece::new(Team::Black, Strength::Man)) - ); + new_board.cells[cell_idx] = Square::pc(Black, Man); } else { let cell_idx = new_board.cell_index(idx, jdx); new_board.cells[cell_idx] = Square::new( - SquareState::Empty, + Empty, None ); } @@ -592,6 +655,10 @@ impl Board { self.current_turn } + pub fn set_turn(&mut self, new_team: Team) { + self.current_turn = new_team; + } + /// Get a pointer to the backing array of board squares, [`Board::cells`] pub fn cells(&self) -> *const Square { self.cells.as_ptr() @@ -602,6 +669,49 @@ impl Board { self.cells.len() } + /// Get the number of remaining pieces + pub fn num_pieces(&self) -> usize { + let pieces: Vec<_> = PieceIterator::new(self).collect(); + pieces.len() + } + + /// Get the number of remaining pieces for a player + pub fn num_player(&self, team: Team) -> usize { + let mut total = 0; + for (_, square) in PieceIterator::new(self) { + match square.occupant { + None => {}, + Some(x) => { + if x.team == team { + total += 1; + } + }, + } + } + total + } + + /// Get the score value, Black - White pieces + pub fn score(&self) -> isize { + let mut black: isize = 0; + let mut white: isize = 0; + + for (_, square) in PieceIterator::new(self) { + if let Some(x) = square.occupant { + match x.team { + Black => { + black += 1; + }, + White => { + white += 1; + }, + } + } + } + + black - white + } + /// Get the state of a board square by 1D array index pub fn cell_state(&self, idx: usize) -> SquareState { self.cell(idx).state @@ -617,9 +727,9 @@ impl Display for Board { let idx = self.cell_index(i, j); match self.cell_state(idx) { - SquareState::Empty => { write!(string, "{}", SquareState::Empty); }, - SquareState::Occupied => { write!(string, "{}", self.cell(idx).occupant.unwrap().team); }, - SquareState::Unplayable => { write!(string, "{}", SquareState::Unplayable); }, + Empty => { write!(string, "_ "); }, + Occupied => { write!(string, "{} ", self.cell(idx).occupant.unwrap().team); }, + Unplayable => { write!(string, ". "); }, } } string.push('\n'); diff --git a/src/board/tests.rs b/src/board/tests.rs index 98a30c0..e296e07 100644 --- a/src/board/tests.rs +++ b/src/board/tests.rs @@ -7,47 +7,73 @@ wasm_bindgen_test_configure!(run_in_browser); // #[wasm_bindgen_test] // fn init_game() { -// let board = Board::init_game(Board::new(8, 8)); +// let board = Board::init_game(Board::new(8, 8, Black), 3); // log!("{}", board); // } #[wasm_bindgen_test] fn create() { - let board = Board::new(STD_WIDTH, STD_HEIGHT); + let board = Board::new(STD_WIDTH, STD_HEIGHT, Black); assert!(true); } #[wasm_bindgen_test] fn std_num_cells() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(64, board.num_cells()); } +#[wasm_bindgen_test] +fn idx_diffs() { + let from = BrdIdx::from(1, 1); + let to = BrdIdx::from(2, 2); + assert_eq!(Board::idx_diffs(from, to), (1, 1)); + + let from = BrdIdx::from(2, 2); + let to = BrdIdx::from(1, 1); + assert_eq!(Board::idx_diffs(from, to), (-1, -1)); + + let from = BrdIdx::from(5, 0); + let to = BrdIdx::from(0, 10); + assert_eq!(Board::idx_diffs(from, to), (-5, 10)); +} + +#[wasm_bindgen_test] +fn set_cell() { + let idx = 1; + + let mut board = Board::new(8, 8, Black); + let square = Square::new(Occupied, Some(Piece::new(White, Man))); + + board.set_cell(idx, square); + assert_eq!(square, board.cell(idx)); +} + ////////////// // INDEXING ////////////// #[wasm_bindgen_test] fn cell_index_top_left() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(0, board.cell_index(0, 0)); } #[wasm_bindgen_test] fn cell_index_central() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(9, board.cell_index(1, 1)); } #[wasm_bindgen_test] fn cell_index_central_2() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(17, board.cell_index(2, 1)); } #[wasm_bindgen_test] fn board_index() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); // first row assert_eq!(BrdIdx::from(0, 5), board.board_index(5)); @@ -63,14 +89,14 @@ fn board_index() { #[wasm_bindgen_test] fn first_square_unplayable() { - let board = Board::new(8, 8); - assert_eq!(SquareState::Unplayable, board.cell_state(board.cell_index(0, 0))); + let board = Board::new(8, 8, Black); + assert_eq!(Unplayable, board.cell_state(board.cell_index(0, 0))); } #[wasm_bindgen_test] fn first_square_row_5_unplayable() { - let board = Board::new(8, 8); - assert_eq!(SquareState::Empty, board.cell_state(board.cell_index(5, 0))); + let board = Board::new(8, 8, Black); + assert_eq!(Empty, board.cell_state(board.cell_index(5, 0))); } ////////////////////// @@ -79,7 +105,7 @@ fn first_square_row_5_unplayable() { #[wasm_bindgen_test] fn moveable_indices_unplayable() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(None, board.diagonal_indices(BrdIdx::from(7, 7))); assert_eq!(None, board.diagonal_indices(BrdIdx::from(0, 0))); assert_eq!(None, board.diagonal_indices(BrdIdx::from(1, 1))); @@ -87,43 +113,43 @@ fn moveable_indices_unplayable() { #[wasm_bindgen_test] fn moveable_indices_central() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(Some(vec![1, 3, 17, 19]), board.diagonal_indices(BrdIdx::from(1, 2))); } #[wasm_bindgen_test] fn moveable_indices_top_row() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(Some(vec![8, 10]), board.diagonal_indices(BrdIdx::from(0, 1))); } #[wasm_bindgen_test] fn moveable_indices_left_column() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(Some(vec![1, 17]), board.diagonal_indices(BrdIdx::from(1, 0))); } #[wasm_bindgen_test] fn moveable_indices_bottom_row() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(Some(vec![49, 51]), board.diagonal_indices(BrdIdx::from(7, 2))); } #[wasm_bindgen_test] fn moveable_indices_right_column() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(Some(vec![14, 30]), board.diagonal_indices(BrdIdx::from(2, 7))); } #[wasm_bindgen_test] fn moveable_indices_top_right() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(Some(vec![14]), board.diagonal_indices(BrdIdx::from(0, 7))); } #[wasm_bindgen_test] fn moveable_indices_bottom_left() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(Some(vec![49]), board.diagonal_indices(BrdIdx::from(7, 0))); } @@ -133,7 +159,7 @@ fn moveable_indices_bottom_left() { #[wasm_bindgen_test] fn jumpable_indices_unplayable() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(None, board.jumpable_indices(BrdIdx::from(7, 7))); assert_eq!(None, board.jumpable_indices(BrdIdx::from(0, 0))); assert_eq!(None, board.jumpable_indices(BrdIdx::from(1, 1))); @@ -141,70 +167,320 @@ fn jumpable_indices_unplayable() { #[wasm_bindgen_test] fn jumpable_indices() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(Some(vec![24, 28]), board.jumpable_indices(BrdIdx::from(1, 2))); } #[wasm_bindgen_test] fn jumpable_indices_central() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(Some(vec![10, 14, 42, 46]), board.jumpable_indices(BrdIdx::from(3, 4))); } #[wasm_bindgen_test] fn jumpable_indices_top_row() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(Some(vec![19]), board.jumpable_indices(BrdIdx::from(0, 1))); } #[wasm_bindgen_test] fn jumpable_indices_left_column() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(Some(vec![26]), board.jumpable_indices(BrdIdx::from(1, 0))); } #[wasm_bindgen_test] fn jumpable_indices_bottom_row() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(Some(vec![40, 44]), board.jumpable_indices(BrdIdx::from(7, 2))); } #[wasm_bindgen_test] fn jumpable_indices_right_column() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(Some(vec![5, 37]), board.jumpable_indices(BrdIdx::from(2, 7))); } #[wasm_bindgen_test] fn jumpable_indices_top_right() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(Some(vec![21]), board.jumpable_indices(BrdIdx::from(0, 7))); } #[wasm_bindgen_test] fn jumpable_indices_bottom_left() { - let board = Board::new(8, 8); + let board = Board::new(8, 8, Black); assert_eq!(Some(vec![42]), board.jumpable_indices(BrdIdx::from(7, 0))); } #[wasm_bindgen_test] fn black_diagonal_indices() { - let board = Board::new(8, 8); - assert_eq!(Some(vec![1, 3]), board.player_diagonal_indices(BrdIdx::from(1, 2), Team::Black)); + let board = Board::new(8, 8, Black); + assert_eq!(Some(vec![1, 3]), board.player_diagonal_indices(BrdIdx::from(1, 2), Black)); } #[wasm_bindgen_test] fn white_diagonal_indices() { - let board = Board::new(8, 8); - assert_eq!(Some(vec![17, 19]), board.player_diagonal_indices(BrdIdx::from(1, 2), Team::White)); + let board = Board::new(8, 8, Black); + assert_eq!(Some(vec![17, 19]), board.player_diagonal_indices(BrdIdx::from(1, 2), White)); } //////////////// -// JUMPEE +// JUMPEE //////////////// -// #[wasm_bindgen_test] -// fn check_jumpee() { -// let from = -// assert_eq!(Board::check_jumpee_team(from: Piece, jumpee: Piece)); -// } \ No newline at end of file +#[wasm_bindgen_test] +fn check_jumpee_opposing_teams() { + let from = Piece::new(Black, Man); + let jumpee = Piece::new(White, Man); + assert_eq!(Board::check_jumpee_team(from, jumpee), true); + + let from = Piece::new(White, Man); + let jumpee = Piece::new(Black, Man); + assert_eq!(Board::check_jumpee_team(from, jumpee), true); +} + +#[wasm_bindgen_test] +fn check_jumpee_same_teams() { + let from = Piece::new(Black, Man); + let jumpee = Piece::new(Black, Man); + assert_eq!(Board::check_jumpee_team(from, jumpee), false); + + let from = Piece::new(White, Man); + let jumpee = Piece::new(White, Man); + assert_eq!(Board::check_jumpee_team(from, jumpee), false); +} + +#[wasm_bindgen_test] +fn check_validate_jumpee_opposing_teams() { + let jumpee_square = Square::new( + Occupied, + Some( + Piece::new(White, Man) + ) + ); + let from_piece = Piece::new(Black, Man); + + assert_eq!(Board::validate_jumpee(jumpee_square, from_piece), Moveable::Allowed); + + let jumpee_square = Square::new( + Occupied, + Some( + Piece::new(Black, Man) + ) + ); + let from_piece = Piece::new(White, Man); + + assert_eq!(Board::validate_jumpee(jumpee_square, from_piece), Moveable::Allowed); +} + +#[wasm_bindgen_test] +fn check_validate_jumpee_same_teams() { + let jumpee_square = Square::pc(White, Man); + let from_piece = Piece::new(White, Man); + + assert_eq!(Board::validate_jumpee(jumpee_square, from_piece), Moveable::JumpingSameTeam); + + let jumpee_square = Square::pc(Black, Man); + let from_piece = Piece::new(Black, Man); + + assert_eq!(Board::validate_jumpee(jumpee_square, from_piece), Moveable::JumpingSameTeam); +} + +///////////////// +// SCORE +///////////////// + +#[wasm_bindgen_test] +fn score() { + ////////////////////////////////// + let board = Board::new(8, 8, Black); + assert_eq!(0, board.score()); + ////////////////////////////////// + + ////////////////////////////////// + let mut board = Board::new(8, 8, Black); + let square = Square::pc(Black, Man); + board.set_cell(1, square); + assert_eq!(1, board.score()); + ////////////////////////////////// + + ////////////////////////////////// + let square = Square::pc(White, Man); + board.set_cell(5, square); + let square = Square::pc(Black, Man); + board.set_cell(7, square); + let square = Square::pc(Black, Man); + board.set_cell(8, square); + assert_eq!(2, board.score()); + ////////////////////////////////// +} + +/////////////////////// +// MOVE VALIDATION +/////////////////////// + +#[wasm_bindgen_test] +fn validate_man_move_team_directions() { + // WHITE NEEDS INCREASING IDX + + // allowed, white moves down board + let board = Board::new(8, 8, White); + let from = BrdIdx::from(1, 1); + let to = BrdIdx::from(2, 2); + let piece = Piece::new(White, Man); + + assert_eq!(Moveable::Allowed, board.validate_man_move(from, to, piece)); + + // unallowed, white moves up board + let board = Board::new(8, 8, White); + let from = BrdIdx::from(2, 2); + let to = BrdIdx::from(1, 1); + let piece = Piece::new(White, Man); + + assert_eq!(Moveable::IllegalTrajectory, board.validate_man_move(from, to, piece)); + + // allowed, black moves up board + let board = Board::new(8, 8, Black); + let from = BrdIdx::from(2, 2); + let to = BrdIdx::from(1, 1); + let piece = Piece::new(Black, Man); + + assert_eq!(Moveable::Allowed, board.validate_man_move(from, to, piece)); + + // unallowed, black moves down board + let board = Board::new(8, 8, Black); + let from = BrdIdx::from(1, 1); + let to = BrdIdx::from(2, 2); + let piece = Piece::new(Black, Man); + + assert_eq!(Moveable::IllegalTrajectory, board.validate_man_move(from, to, piece)); +} + +#[wasm_bindgen_test] +fn validate_man_move_weird_trajectories() { + // WHITE NEEDS INCREASING IDX + + // allowed, white moves down board + let board = Board::new(8, 8, White); + let from = BrdIdx::from(1, 1); + let to = BrdIdx::from(3, 2); + let piece = Piece::new(White, Man); + + assert_eq!(Moveable::IllegalTrajectory, board.validate_man_move(from, to, piece)); + + // unallowed, white moves up board + let board = Board::new(8, 8, White); + let from = BrdIdx::from(2, 3); + let to = BrdIdx::from(1, 1); + let piece = Piece::new(White, Man); + + assert_eq!(Moveable::IllegalTrajectory, board.validate_man_move(from, to, piece)); + + // allowed, black moves up board + let board = Board::new(8, 8, Black); + let from = BrdIdx::from(5, 2); + let to = BrdIdx::from(1, 1); + let piece = Piece::new(Black, Man); + + assert_eq!(Moveable::IllegalTrajectory, board.validate_man_move(from, to, piece)); + + // unallowed, black moves down board + let board = Board::new(8, 8, Black); + let from = BrdIdx::from(1, 1); + let to = BrdIdx::from(2, 4); + let piece = Piece::new(Black, Man); + + assert_eq!(Moveable::IllegalTrajectory, board.validate_man_move(from, to, piece)); +} + +#[wasm_bindgen_test] +fn can_move() { + // WHITE NEEDS INCREASING IDX + + // allowed, white moves down board + let board = Board::new(8, 8, White); + let mut board = Board::init_game(board, 3); + + // log!("{}", board); + + // white can move down + let from = BrdIdx::from(2, 1); + let to = BrdIdx::from(3, 0); + assert_eq!(board.can_move(from, to), Moveable::Allowed); + + // going straight down + let from = BrdIdx::from(2, 1); + let to = BrdIdx::from(3, 1); + assert_eq!(board.can_move(from, to), Moveable::IllegalTrajectory); + + // going directly right + let from = BrdIdx::from(2, 1); + let to = BrdIdx::from(2, 2); + assert_eq!(board.can_move(from, to), Moveable::IllegalTrajectory); + + // jumping an empty square + let from = BrdIdx::from(2, 1); + let to = BrdIdx::from(4, 3); + assert_eq!(board.can_move(from, to), Moveable::NoJumpablePiece); + + // empty cell + let from = BrdIdx::from(3, 0); + let to = BrdIdx::from(4, 3); + assert_eq!(board.can_move(from, to), Moveable::UnoccupiedSrc); + + // out of board + let from = BrdIdx::from(50, 50); + let to = BrdIdx::from(4, 3); + assert_eq!(board.can_move(from, to), Moveable::OutOfBounds); + let from = BrdIdx::from(5, 5); + let to = BrdIdx::from(50, 50); + assert_eq!(board.can_move(from, to), Moveable::OutOfBounds); + + // unplayable + let from = BrdIdx::from(0, 0); + let to = BrdIdx::from(4, 3); + assert_eq!(board.can_move(from, to), Moveable::Unplayable); + let from = BrdIdx::from(1, 1); + let to = BrdIdx::from(0, 0); + assert_eq!(board.can_move(from, to), Moveable::Unplayable); + + board.set_turn(Black); + + // wrong teams piece + let from = BrdIdx::from(2, 1); + let to = BrdIdx::from(4, 3); + assert_eq!(board.can_move(from, to), Moveable::WrongTeamSrc); + +} + +#[wasm_bindgen_test] +fn can_move_jump() { + // WHITE NEEDS INCREASING IDX + + // allowed, white moves down board + let board = Board::new(8, 8, White); + let mut board = Board::init_game(board, 3); + + board.set_cell(board.cell_index(3, 2), Square::pc(Black, Man)); + + // log!("{}", board); + // log!("{:?}", board.cell(board.cell_index(3, 2))); + + // white can move down + let from = BrdIdx::from(2, 1); + let to = BrdIdx::from(4, 3); + assert_eq!(board.can_move(from, to), Moveable::Allowed); + + board.set_cell(board.cell_index(3, 2), Square::pc(White, Man)); + + let from = BrdIdx::from(2, 1); + let to = BrdIdx::from(4, 3); + assert_eq!(board.can_move(from, to), Moveable::JumpingSameTeam); + + // moving in to full cell + let from = BrdIdx::from(2, 1); + let to = BrdIdx::from(3, 2); + assert_eq!(board.can_move(from, to), Moveable::OccupiedDest); + +} \ No newline at end of file diff --git a/src/game.rs b/src/game.rs deleted file mode 100644 index 71d93e6..0000000 --- a/src/game.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::board::Board; -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, - previous_boards: Vec, - tree: Arena -} \ No newline at end of file diff --git a/src/game/mod.rs b/src/game/mod.rs new file mode 100644 index 0000000..a581088 --- /dev/null +++ b/src/game/mod.rs @@ -0,0 +1,142 @@ +use crate::board::Board; +use indextree::Arena; + +extern crate wasm_bindgen; +use wasm_bindgen::prelude::*; + +use crate::log; + +use crate::board::{Square, BrdIdx}; +use crate::board::enums::{SquareState, Moveable, Team}; + +use Team::*; +use SquareState::*; + +use std::fmt::{Display, Write}; + +#[cfg(test)] pub mod tests; + +/// Root-level structure for managing the game as a collection of board states +#[wasm_bindgen] +#[derive(Debug)] +pub struct Game { + current: Board, + previous_boards: Vec, + // tree: Arena +} + +impl Game { + /// Get a read-only copy of a previous turn's board + pub fn previous_board(&self, turn: usize) -> &Board { + &self.previous_boards[turn] + } + + /// Set current board to given + pub fn current_board(&self) -> &Board { + &self.current + } +} + +#[wasm_bindgen] +impl Game { + /// Attempt to make a move given a source and destination index + pub fn make_move(&mut self, from: BrdIdx, to: BrdIdx) { + let able = self.current.can_move(from, to); + + if let Moveable::Allowed = able { + let (_, col_diff) = Board::idx_diffs(from, to); + // MOVE + if col_diff.abs() == 1 { + self.execute_move(from, to); + } + // JUMP + else { + self.execute_jump(from, to); + } + } else { + log!("Unable to make move, {:?}", able); + } + + // board has been changed, update player turn + self.current.set_turn(self.current.current_turn().opponent()); + } + + /// Update board state with given move and push new board into current state + pub fn execute_move(&mut self, from: BrdIdx, to: BrdIdx) { + let mut new_board = self.current.clone(); + + let from_idx = self.current.cell_idx(from); + let to_idx = self.current.cell_idx(to); + + // make move update + new_board.set_cell( + to_idx, // destination square + self.current.cell(from_idx) // source piece + ); + + // remove old piece + new_board.set_cell( + from_idx, // destination square + Square::empty() // empty piece + ); + + // set new board to current and push current to stack + self.push_new_board(new_board); + } + + /// Update board state with given jump move and push new board into current state + pub fn execute_jump(&mut self, from: BrdIdx, to: BrdIdx) { + let mut new_board = self.current.clone(); + + let from_idx = self.current.cell_idx(from); + let to_idx = self.current.cell_idx(from); + + // make move update + new_board.set_cell( + to_idx, // destination square + self.current.cell(from_idx) // source piece + ); + + // remove old piece + new_board.set_cell( + from_idx, // destination square + Square::empty() // empty piece + ); + + // remove jumpee + new_board.set_cell( + self.current.jumpee_idx(from, to), // destination square + Square::empty() // empty piece + ); + + // set new board to current and push current to stack + self.push_new_board(new_board); + } + + /// Push current board into the previous turns and set given board to current + pub fn push_new_board(&mut self, board: Board) { + self.previous_boards.push(self.current.clone()); + self.set_current(board); + } + + /// Set current board to given + pub fn set_current(&mut self, board: Board) { + self.current = board; + } + + #[wasm_bindgen(constructor)] + pub fn new(width: usize, height: usize, piece_rows: usize, first_turn: Team) -> Game { + Game { + current: Board::init_game( + Board::new(width, height, first_turn), piece_rows, + ), + previous_boards: Vec::with_capacity(10), + } + } +} + +impl Display for Game { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result{ + write!(f, "{}", self.current) + } +} \ No newline at end of file diff --git a/src/game/tests.rs b/src/game/tests.rs new file mode 100644 index 0000000..f8d8532 --- /dev/null +++ b/src/game/tests.rs @@ -0,0 +1,59 @@ +use super::*; +use wasm_bindgen_test::*; +use crate::log; + +wasm_bindgen_test_configure!(run_in_browser); + +use crate::board::{Piece}; +use crate::board::enums::Strength::*; + + +#[wasm_bindgen_test] +fn make_move() { + let mut game = Game::new(8, 8, 3, Black); + log!("{}", game); + // log!("{:?}", game); + + let from = BrdIdx::from(5, 2); + let to = BrdIdx::from(4, 1); + + game.make_move(from, to); + let board = game.current_board(); + + assert_eq!(board.cell(board.cell_index(4, 1)), Square::pc(Black, Man)); + + log!("{}", game); + + let from = BrdIdx::from(2, 1); + let to = BrdIdx::from(3, 2); + + game.make_move(from, to); + let board = game.current_board(); + + assert_eq!(board.cell(board.cell_index(3, 2)), Square::pc(White, Man)); + + log!("{}", game); + // log!("{}", game.previous_board(0)); +} + +#[wasm_bindgen_test] +fn make_jump() { + let mut game = Game::new(8, 8, 3, Black); + // log!("{}", game); + // log!("{:?}", game); + + let square = Square::pc(White, Man); + game.current.set_cell( + game.current.cell_idx(BrdIdx::from(4, 1)), + square + ); + + let from = BrdIdx::from(5, 2); + let to = BrdIdx::from(3, 0); + + game.make_move(from, to); + + // log!("{}", game); + // log!("{}", game.previous_board(0)); +} + diff --git a/src/lib.rs b/src/lib.rs index 737cc62..1c5706d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod board; pub mod utils; pub mod game; +pub mod player; extern crate wasm_bindgen; use wasm_bindgen::prelude::*; diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..668d080 --- /dev/null +++ b/src/player.rs @@ -0,0 +1,8 @@ +extern crate wasm_bindgen; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +#[derive(Clone)] +pub struct Player { + score: usize, +} \ No newline at end of file diff --git a/www/package-lock.json b/www/package-lock.json index 6baf3d0..8ed31df 100644 --- a/www/package-lock.json +++ b/www/package-lock.json @@ -14,7 +14,8 @@ "copy-webpack-plugin": "^9.0.0", "webpack": "^5.40.0", "webpack-cli": "^4.7.2", - "webpack-dev-server": "^3.11.2" + "webpack-dev-server": "^3.11.2", + "webpack-merge": "^5.8.0" } }, "../pkg": { diff --git a/www/package.json b/www/package.json index 8a8913b..ace9d57 100644 --- a/www/package.json +++ b/www/package.json @@ -4,8 +4,9 @@ "description": "Rust wasm-based checkers game", "main": "index.js", "scripts": { - "build": "webpack --config webpack.config.js", - "start": "webpack serve --config webpack.config.js --progress" + "build": "webpack --config webpack.prod.js --env production", + "devbuild": "webpack --config webpack.dev.js", + "start": "webpack serve --config webpack.dev.js --progress" }, "repository": { "type": "git", @@ -29,6 +30,7 @@ "copy-webpack-plugin": "^9.0.0", "webpack": "^5.40.0", "webpack-cli": "^4.7.2", - "webpack-dev-server": "^3.11.2" + "webpack-dev-server": "^3.11.2", + "webpack-merge": "^5.8.0" } } diff --git a/www/webpack.config.js b/www/webpack.common.js similarity index 94% rename from www/webpack.config.js rename to www/webpack.common.js index e0a4eee..88227f3 100644 --- a/www/webpack.config.js +++ b/www/webpack.common.js @@ -7,7 +7,6 @@ module.exports = { path: path.resolve(__dirname, "dist"), filename: "bootstrap.js", }, - mode: "development", experiments: { asyncWebAssembly: true }, diff --git a/www/webpack.dev.js b/www/webpack.dev.js new file mode 100644 index 0000000..21c89f3 --- /dev/null +++ b/www/webpack.dev.js @@ -0,0 +1,8 @@ +const { merge } = require('webpack-merge'); +const common = require('./webpack.common.js'); + +module.exports = merge(common, { + mode: 'development', + devtool: 'inline-source-map', + watch: true +}); \ No newline at end of file diff --git a/www/webpack.prod.js b/www/webpack.prod.js new file mode 100644 index 0000000..5a3c558 --- /dev/null +++ b/www/webpack.prod.js @@ -0,0 +1,7 @@ +const { merge } = require('webpack-merge'); +const common = require('./webpack.common.js'); + +module.exports = merge(common, { + mode: 'production', + devtool: 'source-map' +}); \ No newline at end of file