diff --git a/src/interface/uci.rs b/src/interface/uci.rs index 04701b2..e660742 100644 --- a/src/interface/uci.rs +++ b/src/interface/uci.rs @@ -25,6 +25,7 @@ pub enum Command { UciNewGame, Position, Go, + Stop, Quit, } @@ -35,6 +36,7 @@ fn parse_command(parts: &mut SplitWhitespace) -> anyhow::Result { Some("ucinewgame") => Ok(Command::UciNewGame), Some("position") => Ok(Command::Position), Some("go") => Ok(Command::Go), + Some("stop") => Ok(Command::Stop), Some("quit") => Ok(Command::Quit), _ => bail!("Unrecognised command"), } @@ -47,7 +49,7 @@ pub enum Response { Info(String), } -fn write_response(handle_out: &mut impl Write, response: &Response) -> anyhow::Result<()> { +pub fn write_response(handle_out: &mut impl Write, response: &Response) -> anyhow::Result<()> { writeln!(handle_out, "{response}").map_err(|e| anyhow!(e))?; handle_out.flush().map_err(|e| anyhow!(e))?; Ok(()) @@ -180,36 +182,43 @@ pub fn uci_loop(input: R, mut output: W) -> anyhow::Result let line_str = line.unwrap_or_else(|_| "quit".to_string()); let mut parts = line_str.split_whitespace(); let command = parse_command(&mut parts)?; - let response = match command { - Command::Uci => Response::UciOk, - Command::IsReady => Response::ReadyOk, + match command { + Command::Uci => write_response(&mut output, &Response::UciOk)?, + Command::IsReady => write_response(&mut output, &Response::ReadyOk)?, Command::UciNewGame => { - params.game.as_mut().map_or_else( - || Response::Info("Failed to unwrap from UciParameter".to_string()), - |game| { - game.tt = TranspositionTable::new(MAX_TT_SIZE); - game.board = Board::startpos(); - Response::Info("Initialized new game".to_string()) - }, - ); - Response::Info("Clear cache".to_string()) + if let Some(game) = params.game.as_mut() { + game.tt = TranspositionTable::new(MAX_TT_SIZE); + game.board = Board::startpos(); + } else { + let game = Game::new(); + params.add_game(game); + } + write_response(&mut output, &Response::Info("Clear cache".to_string()))?; } Command::Position => { + //TODO: doesnt have to create a new game every time, we can just update the game params.add_game(uci_position(&mut parts)?); - Response::Info("Initialized position".to_string()) } - Command::Go => params.game.as_mut().map_or_else( - || Response::Info("Failed to unwrap from UciParameter".to_string()), - |game| match uci_go(&mut parts, game) { - Ok(best_move) => Response::BestMove(best_move.parse_into_str()), - Err(e) => Response::Info(e.to_string()), - }, - ), - // TODO: Command::Stop => (), + Command::Go => { + if let Some(game) = params.game.as_mut() { + match uci_go(&mut parts, game) { + Ok(best_move) => write_response( + &mut output, + &Response::BestMove(best_move.parse_into_str()), + ), + Err(e) => write_response(&mut output, &Response::Info(e.to_string())), + }?; + } else { + write_response( + &mut output, + &Response::Info("Failed to unwrap from UciParameter".to_string()), + )?; + }; + } + Command::Stop => break, Command::Quit => break, }; - write_response(&mut output, &response)?; output.flush().map_err(|e| anyhow!(e))? } diff --git a/src/movegen/move.rs b/src/movegen/move.rs index ecc52a8..3758716 100644 --- a/src/movegen/move.rs +++ b/src/movegen/move.rs @@ -66,6 +66,24 @@ impl fmt::Debug for Move { } } +impl fmt::Display for Move { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", to_algebraic(self.src), to_algebraic(self.dst))?; + + if let MoveType::Promotion(piece) | MoveType::PromotionCapture(piece) = &self.move_type { + let promote_char = match piece { + Promote::Knight => 'n', + Promote::Bishop => 'b', + Promote::Rook => 'r', + Promote::Queen => 'q', + }; + write!(f, "{promote_char}")?; + } + + Ok(()) + } +} + impl Move { pub const fn new(src: usize, dst: usize) -> Self { Self { diff --git a/src/search/iterative_deepening.rs b/src/search/iterative_deepening.rs index 599c516..0eaebc1 100644 --- a/src/search/iterative_deepening.rs +++ b/src/search/iterative_deepening.rs @@ -1,6 +1,9 @@ +use std::io; + use crate::{ board::game::Game, evaluation::{MAX_SCORE, MIN_SCORE}, + interface::uci::{write_response, Response}, movegen::r#move::Move, }; @@ -22,6 +25,8 @@ pub fn iterative_deepening( return Ok(best_move); } + let mut total_nodes_searched = 0; + let score = negamax::negamax( game, MIN_SCORE, @@ -29,6 +34,7 @@ pub fn iterative_deepening( depth, 0, &TimeInfo::new(time, remaining_time), + &mut total_nodes_searched, ); if score.is_err() { @@ -37,10 +43,44 @@ pub fn iterative_deepening( best_score = score?; best_move = game.tt.lookup(game.hash).and_then(|entry| entry.mv); + + let nps = ((total_nodes_searched * 1_000_000) as u128).div_ceil(time.elapsed().as_micros()) + as u64; + + write_response( + &mut io::stdout(), + &search_info( + depth, + time.elapsed().as_millis() as u64, + total_nodes_searched, + nps, + best_score, + best_move, + ), + )?; } Ok(best_move) } +fn search_info( + depth: u8, + time: u64, + total_nodes_searched: u64, + nps: u64, + best_score: i32, + best_move: Option, +) -> Response { + Response::Info(format!( + "info depth {} time {} nodes {} nps {} eval {} pv {}", + depth, + time, + total_nodes_searched, + nps, + best_score, + best_move.expect("msg: No best move found") + )) +} + #[cfg(test)] mod tests { use crate::{ diff --git a/src/search/negamax.rs b/src/search/negamax.rs index fd432ea..621b6ac 100644 --- a/src/search/negamax.rs +++ b/src/search/negamax.rs @@ -19,6 +19,7 @@ pub fn negamax( depth: u8, plies: u8, time_info: &TimeInfo, + total_nodes_searched: &mut u64, ) -> Result { if hard_limit(&time_info.time, time_info.remaining_time_in_ms) { bail!("Time is up! In Negamax"); @@ -29,7 +30,8 @@ pub fn negamax( } if depth == 0 { - let q_score = quiescence(game, alpha, beta, time_info).map_err(|e| anyhow!("{e}"))?; + let q_score = quiescence(game, alpha, beta, time_info, total_nodes_searched) + .map_err(|e| anyhow!("{e}"))?; return Ok(q_score); } @@ -49,15 +51,39 @@ pub fn negamax( game.unmake_move(); continue; } - legal_moves += 1; + *total_nodes_searched += 1; let score = if legal_moves == 1 { - -negamax(game, -beta, -alpha, depth - 1, plies + 1, time_info)? + -negamax( + game, + -beta, + -alpha, + depth - 1, + plies + 1, + time_info, + total_nodes_searched, + )? } else { - let mut score = -negamax(game, -alpha - 1, -alpha, depth - 1, plies + 1, time_info)?; + let mut score = -negamax( + game, + -alpha - 1, + -alpha, + depth - 1, + plies + 1, + time_info, + total_nodes_searched, + )?; if score > alpha && score < beta { - score = -negamax(game, -beta, -alpha, depth - 1, plies + 1, time_info)?; + score = -negamax( + game, + -beta, + -alpha, + depth - 1, + plies + 1, + time_info, + total_nodes_searched, + )?; } score }; @@ -110,7 +136,7 @@ mod tests { let e3f2 = Move::new(Square::E3, Square::F2); let time_info = TimeInfo::new(std::time::Instant::now(), 1000); - negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info) + negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info, &mut 0) .expect("Expected a search result"); assert_eq!(e3f2, game.tt.lookup(game.hash).unwrap().mv.unwrap()); @@ -118,7 +144,7 @@ mod tests { 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) + negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info, &mut 0) .expect("Expected a search result"); assert_eq!(e3f2, game.tt.lookup(game.hash).unwrap().mv.unwrap()); diff --git a/src/search/quiescence.rs b/src/search/quiescence.rs index 0644a9d..73618b0 100644 --- a/src/search/quiescence.rs +++ b/src/search/quiescence.rs @@ -7,7 +7,13 @@ use super::{ time::{hard_limit, TimeInfo}, }; -pub fn quiescence(game: &mut Game, mut alpha: i32, beta: i32, time_info: &TimeInfo) -> Result { +pub fn quiescence( + game: &mut Game, + mut alpha: i32, + beta: i32, + time_info: &TimeInfo, + total_nodes_searched: &mut u64, +) -> Result { if hard_limit(&time_info.time, time_info.remaining_time_in_ms) { bail!("Time is up! In Quiescence"); } @@ -36,11 +42,11 @@ pub fn quiescence(game: &mut Game, mut alpha: i32, beta: i32, time_info: &TimeIn game.unmake_move(); continue; } + *total_nodes_searched += 1; - let score = -quiescence(game, -beta, -alpha, time_info)?; + let score = -quiescence(game, -beta, -alpha, time_info, &mut 0)?; game.unmake_move(); - if score > best_score { best_score = score; } diff --git a/src/search/transposition_table.rs b/src/search/transposition_table.rs index c60725f..740bdf1 100644 --- a/src/search/transposition_table.rs +++ b/src/search/transposition_table.rs @@ -60,7 +60,7 @@ mod tests { let mut game = from_fen(FEN).unwrap(); let time_info = TimeInfo::new(std::time::Instant::now(), 30000); - negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info) + negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info, &mut 0) .expect("Expected a search result"); dbg!(time_info.time.elapsed());