From 0400d8253296d7d304c3ee82d38672c09e435852 Mon Sep 17 00:00:00 2001 From: Lubos Kozmon Date: Mon, 29 Mar 2021 09:28:11 +0000 Subject: [PATCH] Implement game termination Also, disable overflow check due to bug in rustc: https://github.com/rust-lang/rust/issues/78744 --- dot_chess/Cargo.toml | 3 + dot_chess/board/mod.rs | 98 ++++++++++--------- dot_chess/lib.rs | 216 +++++++++++++++++++++++------------------ 3 files changed, 177 insertions(+), 140 deletions(-) diff --git a/dot_chess/Cargo.toml b/dot_chess/Cargo.toml index b55d175..39be74c 100755 --- a/dot_chess/Cargo.toml +++ b/dot_chess/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.0" authors = ["Lubos Kozmon "] edition = "2018" +[profile.release] +overflow-checks = false + [dependencies] ink_primitives = { version = "3.0.0-rc3", default-features = false } ink_metadata = { version = "3.0.0-rc3", default-features = false, features = ["derive"], optional = true } diff --git a/dot_chess/board/mod.rs b/dot_chess/board/mod.rs index 84e41e7..c20b1a7 100644 --- a/dot_chess/board/mod.rs +++ b/dot_chess/board/mod.rs @@ -47,6 +47,12 @@ pub use square::Square; )] pub struct Flags(u16); +impl core::convert::Into for Flags { + fn into(self) -> u16 { + self.0 + } +} + impl Flags { const WHITES_TURN_INDEX: usize = 12; @@ -285,6 +291,51 @@ impl Board { pieces } + pub fn get_king_square(&self, side: Side) -> Square { + let pieces = match side { + Side::White => self.white, + Side::Black => self.black, + }; + + (pieces & self.kings).pop_square() + } + + // TODO test + pub fn is_attacked(&self, square: Square, by_side: Side) -> bool { + let bitboard = BitBoard::square(square); + let attack_pieces = match by_side { + Side::White => self.white, + Side::Black => self.black, + }; + + let pawns = attack_pieces & self.pawns; + if (bitboard.black_pawn_any_attacks_mask() & pawns).not_empty() { + return true; + } + + let knights = attack_pieces & self.knights; + if (BitBoard::knight_attacks_mask(square) & knights).not_empty() { + return true; + } + + let kings = attack_pieces & self.kings; + if (BitBoard::king_attacks_mask(square) & kings).not_empty() { + return true; + } + + let bishops_queens = attack_pieces & (self.bishops | self.queens); + if (self.bishop_attacks(square) & bishops_queens).not_empty() { + return true; + } + + let rooks_queens = attack_pieces & (self.rooks | self.queens); + if (self.rook_attacks(square) & rooks_queens).not_empty() { + return true; + } + + false + } + pub fn get_side_turn(&self) -> Side { match self.get_flags().get_whites_turn() { true => Side::White, @@ -367,51 +418,6 @@ impl Board { self.rook_attacks(square) | self.bishop_attacks(square) } - fn get_king_square(&self, side: Side) -> Square { - let pieces = match side { - Side::White => self.white, - Side::Black => self.black, - }; - - (pieces & self.kings).pop_square() - } - - // TODO test - fn is_attacked(&self, square: Square, by_side: Side) -> bool { - let bitboard = BitBoard::square(square); - let attack_pieces = match by_side { - Side::White => self.white, - Side::Black => self.black, - }; - - let pawns = attack_pieces & self.pawns; - if (bitboard.black_pawn_any_attacks_mask() & pawns).not_empty() { - return true; - } - - let knights = attack_pieces & self.knights; - if (BitBoard::knight_attacks_mask(square) & knights).not_empty() { - return true; - } - - let kings = attack_pieces & self.kings; - if (BitBoard::king_attacks_mask(square) & kings).not_empty() { - return true; - } - - let bishops_queens = attack_pieces & (self.bishops | self.queens); - if (self.bishop_attacks(square) & bishops_queens).not_empty() { - return true; - } - - let rooks_queens = attack_pieces & (self.rooks | self.queens); - if (self.rook_attacks(square) & rooks_queens).not_empty() { - return true; - } - - false - } - // TODO test fn try_make_pseudo_legal_move(&self, ply: Ply) -> Result<(Self, Vec), Error> { // Assert sides turn @@ -852,7 +858,7 @@ mod tests { .into_iter() .collect(); - let board = Board::new(pieces, Flags::default()); + let board = Board::new(pieces, Flags::default(), 0); let square = Square::new(File::H, Rank::_8); assert_eq!( diff --git a/dot_chess/lib.rs b/dot_chess/lib.rs index faa8410..be2a6dc 100755 --- a/dot_chess/lib.rs +++ b/dot_chess/lib.rs @@ -13,12 +13,27 @@ mod dot_chess { use ink_storage::Vec; use scale::{Decode, Encode}; + const BALANCE_DISTRIBUTION_RATIO: Balance = 98; + const FEE_BENEFICIARY: [u8; 32] = [ + 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, + 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125, + ]; + + pub type Result = core::result::Result; + #[derive(Encode, Decode, Debug, Copy, Clone)] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub enum Error { InvalidArgument, InvalidCaller, IllegalMove, + Other, + } + + impl core::convert::From for Error { + fn from(error: ink_env::Error) -> Self { + Self::Other + } } #[derive(Encode, Decode, Debug, Copy, Clone)] @@ -107,7 +122,7 @@ mod dot_chess { /// 1 << 11 Black King Castling Right /// 1 << 12 Whites Turn #[ink(message)] - pub fn get_board(&self) -> ([i8; 64], BoardFlags) { + pub fn get_board(&self) -> ([i8; 64], u16) { let mut board = [0i8; 64]; for (side, piece, square) in self.board.get_pieces().iter() { @@ -128,130 +143,143 @@ mod dot_chess { board[square.index() as usize] = n; } - let flags = self.board.get_flags(); + let flags: u16 = (*self.board.get_flags()).into(); - (board, *flags) + (board, flags) } /// Makes a move /// /// Returns true if move was successful #[ink(message)] - pub fn make_move(&mut self, from: Square, to: Square, flags: PlyFlags) -> bool { + pub fn make_move(&mut self, from: Square, to: Square, flags: PlyFlags) -> Result<()> { if !self.is_callers_turn() { - todo!("Emit event: Invalid caller"); - return false; + return Err(Error::InvalidCaller); } let side = self.board.get_side_turn(); let ply = Ply::new(from, to, flags); - match self.board.try_make_move(ply) { - Ok((board_new, events)) => { - // Update board - self.board = ink_storage::Pack::new(board_new); - - let opponent_side = side.flip(); - let opponent_has_legal_moves = self.board.side_has_legal_move(opponent_side); - if !opponent_has_legal_moves { - let opponent_king_square = self.board.get_king_square(opponent_side); - - if self.board.is_attacked(opponent_king_square, side) { - // Checkmate - // TODO Announce result - } else { - // Stalemate - // TODO Announce result - } - } - - // Is insufficient mating material? - let mut white_score = 0; - let mut black_score = 0; - let mut insufficient_mating_material = true; - - for (side, piece, square) in self.board.get_pieces().iter() { - let ref_score = match side { - Side::White => &mut white_score, - Side::Black => &mut black_score, - }; - - match piece { - Piece::Knight | Piece::Bishop => *ref_score += 1, - Piece::Pawn | Piece::Rook | Piece::Queen => *ref_score = i32::MAX, - Piece::King => {} - } - - drop(ref_score); - - if white_score > 1 && black_score > 1 { - insufficient_mating_material = false; - break; - } - } - - if insufficient_mating_material { - // TODO Announce result - } - - // Clear board history to save space - if self.board.halfmove_clock() == 0 { - self.board_history.clear() - } - - // Is repetition? - let new_hash = self.board_history.last().unwrap().apply(events); - - let is_repetition = self - .board_history - .iter() - .filter(|hash| hash == new_hash) - .take(2) - .count() - == 2; - - if is_repetition { - // TODO Announce result - } - - // Update history - self.board_history.push(new_hash); - - // TODO Emit events - - true + let (board_new, events) = self.board.try_make_move(ply)?; + + // Update board + self.board = ink_storage::Pack::new(board_new); + + let opponent_side = side.flip(); + let opponent_has_legal_moves = self.board.side_has_legal_move(opponent_side); + if !opponent_has_legal_moves { + let opponent_king_square = self.board.get_king_square(opponent_side); + + if self.board.is_attacked(opponent_king_square, side) { + // Checkmate + return self.terminate_game(Some(side), GameOverReason::Checkmate); + } else { + // Stalemate + return self.terminate_game(None, GameOverReason::Stalemate); } - Err(error) => { - todo!("Emit event"); + } + + // Is insufficient mating material? + let mut white_score = 0; + let mut black_score = 0; + let mut insufficient_mating_material = true; - false + for (side, piece, square) in self.board.get_pieces().iter() { + let ref_score = match side { + Side::White => &mut white_score, + Side::Black => &mut black_score, + }; + + match piece { + Piece::Knight | Piece::Bishop => *ref_score += 1, + Piece::Pawn | Piece::Rook | Piece::Queen => *ref_score = i32::MAX, + Piece::King => {} } + + drop(ref_score); + + if white_score > 1 && black_score > 1 { + insufficient_mating_material = false; + break; + } + } + + if insufficient_mating_material { + return self.terminate_game(None, GameOverReason::InsufficientMatingMaterial); + } + + // Clear board history to save space + if self.board.halfmove_clock() == 0 { + self.board_history.clear() + } + + // Is repetition? + let new_hash = self.board_history.last().unwrap().apply(events); + + let is_repetition = self + .board_history + .iter() + .filter(|hash| **hash == new_hash) + .take(2) + .count() + == 2; + + if is_repetition { + return self.terminate_game(None, GameOverReason::Repetition); } + + // Update history + self.board_history.push(new_hash); + + // Emit event + self.env().emit_event(PlayerMoved { side, from, to }); + + Ok(()) } - pub fn claim_draw(&mut self, reason: GameOverReason) -> bool { + #[ink(message)] + pub fn claim_draw(&mut self, reason: GameOverReason) -> Result<()> { if !self.is_callers_turn() { - todo!("Emit event: Invalid caller"); - return false; + return Err(Error::InvalidCaller); } match reason { GameOverReason::FiftyMoveRule if self.board.halfmove_clock() >= 100 => { - // TODO Announce draw - } - _ => { - panic!("Invalid claim") + self.terminate_game(None, GameOverReason::FiftyMoveRule) } + _ => Err(Error::InvalidArgument), } } - pub fn resign(&mut self) -> bool { + #[ink(message)] + pub fn resign(&mut self) -> Result<()> { if !self.is_callers_turn() { - todo!("Emit event: Invalid caller"); - return false; + return Err(Error::InvalidCaller); + } + + let resignee_side = self.board.get_side_turn(); + + self.terminate_game(Some(resignee_side.flip()), GameOverReason::Resignation) + } + + fn terminate_game(&mut self, winner: Option, reason: GameOverReason) -> Result<()> { + self.env().emit_event(GameOver { winner, reason }); + + let balance = self.env().balance(); + let fee = balance / BALANCE_DISTRIBUTION_RATIO; + let pot = balance - fee; + + match winner { + Some(Side::White) => self.env().transfer(self.white, pot)?, + Some(Side::Black) => self.env().transfer(self.black, pot)?, + None => { + let split = pot / 2; + self.env().transfer(self.white, split)?; + self.env().transfer(self.black, split)?; + } } - // TODO Announce result + self.env().terminate_contract(FEE_BENEFICIARY.into()) } fn is_callers_turn(&self) -> bool {