diff --git a/src/board/board.rs b/src/board/board.rs index 6f470b0..d3a056a 100644 --- a/src/board/board.rs +++ b/src/board/board.rs @@ -119,6 +119,13 @@ impl Board { } } + pub fn non_pawn_materials(&self) -> u64 { + self.pieces[PieceType::Knight] + | self.pieces[PieceType::Bishop] + | self.pieces[PieceType::Rook] + | self.pieces[PieceType::Queen] + } + pub fn set_piece(&mut self, bb: Bitboard, piece_type: PieceType, color: Color) { self.pieces[piece_type] |= bb; self.color[color] |= bb; diff --git a/src/board/game.rs b/src/board/game.rs index 1e62b04..ae6c6b2 100644 --- a/src/board/game.rs +++ b/src/board/game.rs @@ -249,6 +249,41 @@ impl Game { } } + pub fn make_null_move(&mut self) { + let move_params = MoveParameters::build_null(self); + self.history.push_move_parameters(move_params); + self.hash.update_side_to_move_key(); + + self.board + .state + .update_null_game_state(self.current_player()); + } + + pub fn unmake_null_move(&mut self) { + let board = &mut self.board; + let move_parameters = &mut self + .history + .pop_move_parameters() + .expect("History stack is empty"); + + let color_before_move = board.state.change_side(); + board.state.revert_full_move(color_before_move); + + board.state.en_passant_square = move_parameters.en_passant_square; + + if let Some(hash) = move_parameters.zobrist_hash { + self.hash = hash; + } + + if let Some(new_castling_ability) = move_parameters.castling_ability { + board.state.castling_ability = new_castling_ability; + } + + if let Some(new_halfmove_clock) = move_parameters.halfmove_clock { + board.state.halfmove_clock = new_halfmove_clock; + } + } + pub fn in_repetition(&self) -> bool { if self.board.state.halfmove_clock < 4 { return false; @@ -462,4 +497,22 @@ mod tests { Ok(()) } + + const FEN_W: &str = "rnbqkbnr/pppppp1p/8/5Pp1/8/8/PPPPP1PP/RNBQKBNR w KQkq g6 0 1"; + const FEN_B: &str = "rnbqkbnr/pppppp1p/8/5Pp1/8/8/PPPPP1PP/RNBQKBNR b KQkq - 1 1"; + + #[test] + fn test_make_and_unmake_null_move() -> Result<(), String> { + let mut game = from_fen(FEN_W)?; + + game.make_null_move(); + + assert_eq!(game, from_fen(FEN_B)?); + + game.unmake_null_move(); + + assert!(game == from_fen(FEN_W)?); + + Ok(()) + } } diff --git a/src/board/history.rs b/src/board/history.rs index 33eb58d..3847b75 100644 --- a/src/board/history.rs +++ b/src/board/history.rs @@ -76,6 +76,14 @@ impl MoveParameters { move_parameters } + pub fn build_null(game: &Game) -> Self { + let mut move_parameters = Self::new(); + move_parameters.add_irreversible_parameters(&game.board.state); + move_parameters.add_zobrist_hash(&game.hash); + + move_parameters + } + fn add_move(&mut self, mv: Move) { self.mv = Some(mv); } diff --git a/src/board/state.rs b/src/board/state.rs index 1bc4905..74518f4 100644 --- a/src/board/state.rs +++ b/src/board/state.rs @@ -54,6 +54,13 @@ impl State { self.change_side(); } + pub fn update_null_game_state(&mut self, color: Color) { + self.set_en_passant_square(None); + self.update_half_move(MoveType::Quiet, false); + self.update_full_move(color); + self.change_side(); + } + pub const fn en_passant_square(&self) -> Option { self.en_passant_square } diff --git a/src/search/iterative_deepening.rs b/src/search/iterative_deepening.rs index f7eb55c..5c13023 100644 --- a/src/search/iterative_deepening.rs +++ b/src/search/iterative_deepening.rs @@ -18,12 +18,15 @@ pub fn iterative_deepening( for depth in 1..=max_depth { if time.exceed_soft_limit() { - write_response(&mut io::stdout(), &Response::Info("Soft limit exceeded in negamax".to_string()))?; + write_response( + &mut io::stdout(), + &Response::Info("Soft limit exceeded in negamax".to_string()), + )?; return Ok(best_move); } let mut nodes = 0; - let score = negamax::negamax(game, MIN_SCORE, MAX_SCORE, depth, 0, time, &mut nodes); + let score = negamax::negamax(game, MIN_SCORE, MAX_SCORE, depth, 0, time, &mut nodes, true); if let Err(e) = score { write_response(&mut io::stdout(), &Response::Info(format!("{e}")))?; @@ -36,7 +39,7 @@ pub fn iterative_deepening( &mut io::stdout(), &log_depth_results( depth, - time.instant.elapsed().as_millis() as u64, + time.instant.elapsed().as_secs(), nodes, time.nps(nodes), score?, diff --git a/src/search/negamax.rs b/src/search/negamax.rs index 22d9cc2..438b4ce 100644 --- a/src/search/negamax.rs +++ b/src/search/negamax.rs @@ -20,6 +20,7 @@ pub fn negamax( plies: u8, time: &TimeInfo, nodes: &mut u64, + do_nmp: bool, ) -> Result { if time.exceed_hard_limit() { bail!("Hard limit exceeded in negamax"); @@ -41,6 +42,25 @@ pub fn negamax( return Ok(q_score); } + if plies != 0 && !in_check && depth >= 3 && do_nmp && game.board.non_pawn_materials() > 0 { + game.make_null_move(); + let score = -negamax( + game, + -beta, + -beta + 1, + depth - 3, + plies + 1, + time, + nodes, + false, + )?; + game.unmake_null_move(); + + if score >= beta { + return Ok(score); + } + } + let mut legal_moves = 0; let mut best_score = MIN_SCORE; let mut best_move = None; @@ -78,11 +98,20 @@ pub fn negamax( *nodes += 1; let score = if legal_moves == 1 { - -negamax(game, -beta, -alpha, depth - 1, plies + 1, time, nodes)? + -negamax(game, -beta, -alpha, depth - 1, plies + 1, time, nodes, true)? } else { - let mut score = -negamax(game, -alpha - 1, -alpha, depth - 1, plies + 1, time, nodes)?; + let mut score = -negamax( + game, + -alpha - 1, + -alpha, + depth - 1, + plies + 1, + time, + nodes, + true, + )?; if score > alpha && score < beta { - score = -negamax(game, -beta, -alpha, depth - 1, plies + 1, time, nodes)?; + score = -negamax(game, -beta, -alpha, depth - 1, plies + 1, time, nodes, true)?; } score }; @@ -147,16 +176,20 @@ mod tests { let e3f2 = Move::new(Square::E3, Square::F2); let time_info = TimeInfo::new(std::time::Instant::now(), TIME, INC); - negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info, &mut 0) - .expect("Expected a search result"); + negamax( + &mut game, MIN_SCORE, MAX_SCORE, 5, 0, &time_info, &mut 0, true, + ) + .expect("Expected a search result"); assert_eq!(e3f2, game.tt.lookup(game.hash).unwrap().mv.unwrap()); let mut game = from_fen(FEN_MATE_IN_1[1]).unwrap(); let e3f2 = Move::new(Square::E3, Square::F2); - negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info, &mut 0) - .expect("Expected a search result"); + negamax( + &mut game, MIN_SCORE, MAX_SCORE, 5, 0, &time_info, &mut 0, true, + ) + .expect("Expected a search result"); assert_eq!(e3f2, game.tt.lookup(game.hash).unwrap().mv.unwrap()); diff --git a/src/search/transposition_table.rs b/src/search/transposition_table.rs index 5ac30da..0288803 100644 --- a/src/search/transposition_table.rs +++ b/src/search/transposition_table.rs @@ -79,8 +79,10 @@ mod tests { let mut game = from_fen(FEN).unwrap(); let time_info = TimeInfo::new(std::time::Instant::now(), 30000, 100); - negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info, &mut 0) - .expect("Expected a search result"); + negamax( + &mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info, &mut 0, true, + ) + .expect("Expected a search result"); dbg!(time_info.instant.elapsed());