From 48cf03fd9da520fd7259f56998b2eebfd176cc13 Mon Sep 17 00:00:00 2001 From: stefiosif Date: Wed, 31 Jul 2024 23:59:09 +0300 Subject: [PATCH] Add basic UCI support --- src/main.rs | 2 ++ src/move.rs | 85 ++++++++++++++++++++++++++++++++++++++++++-- src/search.rs | 20 +++++++++++ src/square.rs | 7 ++++ src/uci.rs | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 src/search.rs create mode 100644 src/uci.rs diff --git a/src/main.rs b/src/main.rs index 8d867b7..9001fc8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,8 +7,10 @@ pub mod magic; pub mod r#move; pub mod movegen; pub mod perft; +pub mod search; pub mod square; pub mod state; +pub mod uci; fn main() { attack::init_attacks(); diff --git a/src/move.rs b/src/move.rs index 422bf5a..0ab7348 100644 --- a/src/move.rs +++ b/src/move.rs @@ -1,7 +1,9 @@ +use core::fmt; + use crate::{ bitboard::{have_common_bit, lsb, square_to_bitboard}, board::{Board, Color, Kind, Piece}, - square::Square, + square::{coords_to_square, square_to_algebraic, Square}, state::Castle, }; @@ -24,13 +26,36 @@ pub enum MoveType { Castle, } -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] pub struct Move { pub src: usize, pub dst: usize, pub move_type: MoveType, } +impl fmt::Debug for Move { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}{}", + square_to_algebraic(self.src), + square_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 { @@ -47,6 +72,39 @@ impl Move { move_type, } } + + pub fn parse_from_str(mv: &str) -> Result { + if mv.len() != 4 && mv.len() != 5 { + return Err("Invalid move characters length".to_string()); + } + + let mut mv_chars = mv.chars(); + let src_file = (mv_chars.next().unwrap() as usize) - ('a' as usize); + let src_rank = (mv_chars.next().unwrap() as usize) - ('1' as usize); + let dst_file = (mv_chars.next().unwrap() as usize) - ('a' as usize); + let dst_rank = (mv_chars.next().unwrap() as usize) - ('1' as usize); + + let src = coords_to_square(src_rank, src_file); + let dst = coords_to_square(dst_rank, dst_file); + + if mv.len() == 5 { + let promotion_move = match mv_chars.next().unwrap() { + 'n' => Self::new_with_type(src, dst, MoveType::Promotion(Promote::Knight)), + 'b' => Self::new_with_type(src, dst, MoveType::Promotion(Promote::Bishop)), + 'r' => Self::new_with_type(src, dst, MoveType::Promotion(Promote::Rook)), + 'q' => Self::new_with_type(src, dst, MoveType::Promotion(Promote::Queen)), + _ => return Err("Unrecongisable character in promotion piece index".to_string()), + }; + + return Ok(promotion_move); + } + + Ok(Self::new(src, dst)) + } + + pub fn parse_into_str(&self) -> String { + format!("{:?}", self) + } } impl Board { @@ -170,6 +228,7 @@ mod tests { board::Color, fen::from_fen, r#move::{Move, MoveType, Promote}, + square::Square, }; const FEN_QUIET: [&str; 2] = [ @@ -274,4 +333,26 @@ mod tests { assert_eq!(game, from_fen(FEN_CASTLE[1])?); Ok(()) } + + #[test] + fn test_parse_from_str() -> Result<(), String> { + let mv_str = "a1a8"; + let actual = Move::parse_from_str(&mv_str)?; + let expected = Move::new(Square::A1, Square::A8); + assert_eq!(actual, expected); + + let mv_str = "b7b8q"; + let actual = Move::parse_from_str(&mv_str)?; + let expected = + Move::new_with_type(Square::B7, Square::B8, MoveType::Promotion(Promote::Queen)); + assert_eq!(actual, expected); + + Ok(()) + } + + #[test] + #[should_panic(expected = "Invalid move characters length")] + fn test_parse_from_str_panic() -> () { + Move::parse_from_str(&"a7a8qk").unwrap(); + } } diff --git a/src/search.rs b/src/search.rs new file mode 100644 index 0000000..7407351 --- /dev/null +++ b/src/search.rs @@ -0,0 +1,20 @@ +use crate::game::Game; + +pub fn search(game: &mut Game, depth: u8) { + if depth == 0 { + return; + } + + let pseudo_moves = game.board.pseudo_moves_all(game.board.state.next_turn()); + + for mv in pseudo_moves { + let original_board = game.board.clone(); + if !game.board.make_move(mv, game.board.state.next_turn()) { + game.board = original_board; + continue; + } + + search(game, depth - 1); + game.board = original_board; + } +} diff --git a/src/square.rs b/src/square.rs index d95bec7..ae77eb4 100644 --- a/src/square.rs +++ b/src/square.rs @@ -80,6 +80,13 @@ pub const fn coords_to_square(rank: usize, file: usize) -> usize { rank * 8 + file } +pub fn square_to_algebraic(square: usize) -> String { + let file = (square % 8) as u8; + let rank = (square / 8) as u8; + + format!("{}{}", (file + b'a') as char, (rank + b'1') as char) +} + impl Square { pub const fn to_bitboard(square: usize) -> Bitboard { 1_u64 << square diff --git a/src/uci.rs b/src/uci.rs new file mode 100644 index 0000000..2ffd859 --- /dev/null +++ b/src/uci.rs @@ -0,0 +1,97 @@ +use std::str::SplitWhitespace; + +use crate::{game::Game, r#move::Move, search::search}; + +pub enum Command { + Uci, + IsReady, + UciNewGame, + Position(String), + Go, + Stop, + Quit, +} + +pub enum Response { + Id(String), + UciOk, + ReadyOk, + BestMove(String), + Info(String), +} + +struct SearchParameters { + movetime: Option, + depth: Option, +} + +impl SearchParameters { + const fn new() -> Self { + Self { + movetime: None, + depth: None, + } + } + + fn add_movetime(&mut self, movetime: usize) { + self.movetime = Some(movetime); + } + + fn add_depth(&mut self, depth: u8) { + self.depth = Some(depth); + } +} + +impl Default for SearchParameters { + fn default() -> Self { + Self::new() + } + } + +pub fn uci_position(mut position: SplitWhitespace) -> Result { + let state = position.next().ok_or("Expected startpos or fen")?; + let mut game = match state { + "startpos" => Game::new(), + "fen" => Game::new_from_fen(position.next().ok_or("Expected fen string")?)?, + _ => return Err("Expected startpos or fen".to_string()), + }; + + for mv_str in position { + let mv = Move::parse_from_str(mv_str)?; + game.board.make_move(mv, game.board.state.next_turn()); + } + + Ok(game) +} + +const MAX_DEPTH: u8 = 5; + +pub fn uci_go(mut go: SplitWhitespace, game: &mut Game) -> Response { + let mut params = SearchParameters::new(); + while let Some(subcommand) = go.next() { + match subcommand { + "depth" => params.add_depth(go.next().unwrap().parse::().ok().unwrap()), + "movetime" => params.add_movetime(go.next().unwrap().parse::().ok().unwrap()), + _ => (), + } + } + + search(game, params.depth.unwrap_or(MAX_DEPTH)); + Response::BestMove(String::from("value")) +} + +#[cfg(test)] +mod tests { + use crate::fen::from_fen; + + const FEN: [&str; 1] = ["r3k2r/2p1p1qp/2npb3/1p3p2/p3P1pP/P1PB1Q2/1P1PNPP1/R3K2R w KQkq - 0 1"]; + + #[test] + fn test_uci_position() -> Result<(), String> { + let mut _game = from_fen(FEN[0])?; + + // check that the board state is as expected after using uci position and compare + // with a separate fen that created the update instance by itself + Ok(()) + } +}