Add basic UCI support
This commit is contained in:
@@ -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();
|
||||
|
||||
85
src/move.rs
85
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<Self, String> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
20
src/search.rs
Normal file
20
src/search.rs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
97
src/uci.rs
Normal file
97
src/uci.rs
Normal file
@@ -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<usize>,
|
||||
depth: Option<u8>,
|
||||
}
|
||||
|
||||
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<Game, String> {
|
||||
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::<u8>().ok().unwrap()),
|
||||
"movetime" => params.add_movetime(go.next().unwrap().parse::<usize>().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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user