Add Null-move pruning

This commit is contained in:
stefiosif
2025-01-29 23:09:01 +02:00
parent 91ad3de8b1
commit 888b3866b9
7 changed files with 125 additions and 12 deletions

View File

@@ -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;

View File

@@ -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(())
}
}

View File

@@ -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);
}

View File

@@ -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<usize> {
self.en_passant_square
}

View File

@@ -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?,

View File

@@ -20,6 +20,7 @@ pub fn negamax(
plies: u8,
time: &TimeInfo,
nodes: &mut u64,
do_nmp: bool,
) -> Result<i32> {
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());

View File

@@ -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());