From 769f7e0d9485a24e1876e7305a01c4eff965d33c Mon Sep 17 00:00:00 2001 From: dece Date: Fri, 5 Jun 2020 09:00:07 +0200 Subject: [PATCH] engine: basic Shannon board evaluation and stats --- src/board.rs | 279 +++++++++++++++++++++++++++++++++++++++++++----- src/engine.rs | 113 +++++++++++++++++++- src/notation.rs | 7 ++ src/uci.rs | 21 +++- 4 files changed, 388 insertions(+), 32 deletions(-) diff --git a/src/board.rs b/src/board.rs index 3af81ae..e90cd1e 100644 --- a/src/board.rs +++ b/src/board.rs @@ -1,13 +1,14 @@ //! Basic type definitions and functions. // Piece type flags. -pub const SQ_E: u8 = 0; -pub const SQ_P: u8 = 0b00000001; -pub const SQ_B: u8 = 0b00000010; -pub const SQ_N: u8 = 0b00000100; -pub const SQ_R: u8 = 0b00001000; -pub const SQ_Q: u8 = 0b00010000; -pub const SQ_K: u8 = 0b00100000; +pub const SQ_E: u8 = 0; +pub const SQ_P: u8 = 0b00000001; +pub const SQ_B: u8 = 0b00000010; +pub const SQ_N: u8 = 0b00000100; +pub const SQ_R: u8 = 0b00001000; +pub const SQ_Q: u8 = 0b00010000; +pub const SQ_K: u8 = 0b00100000; +pub const SQ_TYPE_MASK: u8 = 0b00111111; // Piece color flags. pub const SQ_WH: u8 = 0b01000000; @@ -32,18 +33,28 @@ pub const SQ_BL_K: u8 = SQ_BL|SQ_K; pub fn has_flag(i: u8, flag: u8) -> bool { i & flag == flag } // Wrappers for clearer naming. +/// Get type of piece on square, without color. +#[inline] +pub fn get_type(square: u8) -> u8 { square & SQ_TYPE_MASK } +/// Return true if the piece on this square is of type `piece_type`. +#[inline] +pub fn is_type(square: u8, piece_type: u8) -> bool { get_type(square) == piece_type } +/// Return true if the piece on this square is the same as `piece`. #[inline] pub fn is_piece(square: u8, piece: u8) -> bool { has_flag(square, piece) } +/// Return true if the piece on this square has this color. #[inline] pub fn is_color(square: u8, color: u8) -> bool { has_flag(square, color) } +/// Return true if this square has a white piece. #[inline] pub fn is_white(square: u8) -> bool { is_color(square, SQ_WH) } +/// Return true if this square has a black piece. #[inline] pub fn is_black(square: u8) -> bool { is_color(square, SQ_BL) } - -/// Get piece color. +/// Return the color of the piece on this square. #[inline] pub fn get_color(square: u8) -> u8 { square & SQ_COLOR_MASK } + /// Get opposite color. #[inline] pub fn opposite(color: u8) -> u8 { color ^ SQ_COLOR_MASK } @@ -155,7 +166,15 @@ pub fn is_empty(board: &Board, coords: &Pos) -> bool { get_square(board, coords) == SQ_E } -/// Count number of pieces on board +pub fn get_piece_iterator<'a>(board: &'a Board) -> Box + 'a> { + Box::new( + board.iter().enumerate() + .filter(|(_, s)| **s != SQ_E) + .map(|(i, s)| (*s, ((i / 8) as i8, (i % 8) as i8))) + ) +} + +/// Count number of pieces on board. Used for debugging. pub fn num_pieces(board: &Board) -> u8 { let mut count = 0; for i in board.iter() { @@ -180,22 +199,7 @@ pub fn find_king(board: &Board, color: u8) -> Pos { (0, 0) } -/// A movement, with before/after positions and optional promotion. -pub type Move = (Pos, Pos, Option); - -/// Apply a move `m` to a copy of `board`. -pub fn apply(board: &Board, m: &Move) -> Board { - let mut new_board = board.clone(); - apply_into(&mut new_board, m); - new_board -} - -/// Apply a move `m` into `board`. -pub fn apply_into(board: &mut Board, m: &Move) { - set_square(board, &m.1, get_square(board, &m.0)); - clear_square(board, &m.0) -} - +/// Write a text view of the board. Used for debugging. pub fn draw(board: &Board, f: &mut dyn std::io::Write) { for r in (0..8).rev() { let mut rank = String::with_capacity(8); @@ -217,6 +221,160 @@ pub fn draw(board: &Board, f: &mut dyn std::io::Write) { writeln!(f, " abcdefgh").unwrap(); } +/// A movement, with before/after positions and optional promotion. +pub type Move = (Pos, Pos, Option); + +/// Apply a move `m` to a copy of `board`. +pub fn apply(board: &Board, m: &Move) -> Board { + let mut new_board = board.clone(); + apply_into(&mut new_board, m); + new_board +} + +/// Apply a move `m` into `board`. +pub fn apply_into(board: &mut Board, m: &Move) { + set_square(board, &m.1, get_square(board, &m.0)); + clear_square(board, &m.0) +} + +/// Storage for precomputed board pieces stats. +#[derive(Debug, Clone, PartialEq)] +pub struct BoardStats { + pub num_pawns: i8, + pub num_bishops: i8, + pub num_knights: i8, + pub num_rooks: i8, + pub num_queens: i8, + pub num_kings: i8, + pub num_doubled_pawns: i8, // Pawns that are on the same file as a friend. + pub num_backward_pawns: i8, // Pawns behind all other pawns on adjacent files. + pub num_isolated_pawns: i8, // Pawns that have no friend pawns on adjacent files. + pub mobility: i32, +} + +impl BoardStats { + pub fn new() -> BoardStats { + BoardStats { + num_pawns: 0, num_bishops: 0, num_knights: 0, num_rooks: 0, num_queens: 0, + num_kings: 0, num_doubled_pawns: 0, num_backward_pawns: 0, num_isolated_pawns: 0, + mobility: 0, + } + } + + pub fn reset(&mut self) { + self.num_pawns = 0; + self.num_bishops = 0; + self.num_knights = 0; + self.num_rooks = 0; + self.num_queens = 0; + self.num_kings = 0; + self.num_doubled_pawns = 0; + self.num_backward_pawns = 0; + self.num_isolated_pawns = 0; + self.mobility = 0; + } +} + +/// Create two new BoardStats objects from the board, for white and black. +/// +/// See `compute_stats_into` for details. +pub fn compute_stats(board: &Board) -> (BoardStats, BoardStats) { + let mut stats = (BoardStats::new(), BoardStats::new()); + compute_stats_into(board, &mut stats); + stats +} + +pub fn compute_stats_into(board: &Board, stats: &mut (BoardStats, BoardStats)) { + compute_color_stats_into(board, &mut stats.0, SQ_WH); + compute_color_stats_into(board, &mut stats.1, SQ_BL); +} + +/// Update `stats` for `color` from given `board` +/// +/// Refresh all stats *except* `mobility`. +pub fn compute_color_stats_into(board: &Board, stats: &mut BoardStats, color: u8) { + stats.reset(); + for (piece, (pos_f, pos_r)) in get_piece_iterator(board) { + if piece == SQ_E || !is_color(piece, color) { + continue + } + match get_type(piece) { + SQ_P => { + stats.num_pawns += 1; + let mut doubled = false; + let mut isolated = true; + let mut backward = true; + for r in 0..8 { + // Check for doubled pawns. + if + !doubled && + is_piece(get_square(board, &(pos_f, r)), color|SQ_P) && r != pos_r + { + doubled = true; + } + // Check for isolated pawns. + if + isolated && + ( + // Check on the left file if not on a-file... + ( + pos_f > POS_MIN && + is_piece(get_square(board, &(pos_f - 1, r)), color|SQ_P) + ) || + // Check on the right file if not on h-file... + ( + pos_f < POS_MAX && + is_piece(get_square(board, &(pos_f + 1, r)), color|SQ_P) + ) + ) + { + isolated = false; + } + // Check for backward pawns. + if backward { + if color == SQ_WH && r <= pos_r { + if ( + pos_f > POS_MIN && + is_type(get_square(board, &(pos_f - 1, r)), SQ_P) + ) || ( + pos_f < POS_MAX && + is_type(get_square(board, &(pos_f + 1, r)), SQ_P) + ) { + backward = false; + } + } else if color == SQ_BL && r >= pos_r { + if ( + pos_f > POS_MIN && + is_type(get_square(board, &(pos_f - 1, r)), SQ_P) + ) || ( + pos_f < POS_MAX && + is_type(get_square(board, &(pos_f + 1, r)), SQ_P) + ) { + backward = false; + } + } + } + } + if doubled { + stats.num_doubled_pawns += 1; + } + if isolated { + stats.num_isolated_pawns += 1; + } + if backward { + stats.num_backward_pawns += 1; + } + }, + SQ_R => stats.num_rooks += 1, + SQ_N => stats.num_knights += 1, + SQ_B => stats.num_bishops += 1, + SQ_Q => stats.num_queens += 1, + SQ_K => stats.num_kings += 1, + _ => {} + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -316,4 +474,73 @@ mod tests { assert_eq!(get_square(&b, &pos("e6")), SQ_BL_N); assert_eq!(num_pieces(&b), 1); } + + #[test] + fn test_compute_stats() { + // Check that initial stats are correct. + let b = new(); + let initial_stats = BoardStats { + num_pawns: 8, + num_bishops: 2, + num_knights: 2, + num_rooks: 2, + num_queens: 1, + num_kings: 1, + num_doubled_pawns: 0, + num_backward_pawns: 0, + num_isolated_pawns: 0, + mobility: 0, + }; + let mut stats = compute_stats(&b); + assert!(stats.0 == stats.1); + assert!(stats.0 == initial_stats); + + // Check that doubled pawns are correctly counted. + let mut b = new_empty(); + set_square(&mut b, &pos("d4"), SQ_WH_P); + set_square(&mut b, &pos("d6"), SQ_WH_P); + compute_color_stats_into(&b, &mut stats.0, SQ_WH); + assert_eq!(stats.0.num_doubled_pawns, 2); + // Add a pawn on another file, no changes expected. + set_square(&mut b, &pos("e6"), SQ_WH_P); + compute_color_stats_into(&b, &mut stats.0, SQ_WH); + assert_eq!(stats.0.num_doubled_pawns, 2); + // Add a pawn backward in the d-file: there are now 3 doubled pawns. + set_square(&mut b, &pos("d2"), SQ_WH_P); + compute_color_stats_into(&b, &mut stats.0, SQ_WH); + assert_eq!(stats.0.num_doubled_pawns, 3); + + // Check that isolated and backward pawns are correctly counted. + assert_eq!(stats.0.num_isolated_pawns, 0); + assert_eq!(stats.0.num_backward_pawns, 2); // A bit weird? + // Protect d4 pawn with a friend in e3: it is not isolated nor backward anymore. + set_square(&mut b, &pos("e3"), SQ_WH_P); + compute_color_stats_into(&b, &mut stats.0, SQ_WH); + assert_eq!(stats.0.num_doubled_pawns, 5); + assert_eq!(stats.0.num_isolated_pawns, 0); + assert_eq!(stats.0.num_backward_pawns, 1); + // Add an adjacent friend to d2 pawn: no pawns are left isolated or backward. + set_square(&mut b, &pos("c2"), SQ_WH_P); + compute_color_stats_into(&b, &mut stats.0, SQ_WH); + assert_eq!(stats.0.num_doubled_pawns, 5); + assert_eq!(stats.0.num_isolated_pawns, 0); + assert_eq!(stats.0.num_backward_pawns, 0); + // Add an isolated/backward white pawn in a far file. + set_square(&mut b, &pos("a2"), SQ_WH_P); + compute_color_stats_into(&b, &mut stats.0, SQ_WH); + assert_eq!(stats.0.num_doubled_pawns, 5); + assert_eq!(stats.0.num_isolated_pawns, 1); + assert_eq!(stats.0.num_backward_pawns, 1); + + // Check for pawns that are backward but not isolated. + let mut b = new_empty(); + // Here, d4 pawn protects both e5 and e3, but it is backward. + set_square(&mut b, &pos("d4"), SQ_WH_P); + set_square(&mut b, &pos("e5"), SQ_WH_P); + set_square(&mut b, &pos("e3"), SQ_WH_P); + compute_color_stats_into(&b, &mut stats.0, SQ_WH); + assert_eq!(stats.0.num_doubled_pawns, 2); + assert_eq!(stats.0.num_isolated_pawns, 0); + assert_eq!(stats.0.num_backward_pawns, 1); + } } diff --git a/src/engine.rs b/src/engine.rs index b618f7e..5047fbe 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -13,6 +13,8 @@ use crate::uci; /// Analysis engine. pub struct Engine { + /// Debug mode, log some data. + debug: bool, /// Current game state, starting point of further analysis. state: GameState, /// Communication mode. @@ -30,6 +32,7 @@ pub struct Engine { #[derive(Clone)] struct GameState { board: board::Board, + stats: (board::BoardStats, board::BoardStats), // white and black pieces stats. color: u8, castling: u8, en_passant: Option, @@ -37,6 +40,17 @@ struct GameState { fullmove: i32, } +impl std::fmt::Debug for GameState { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "GameState {{ board: [...], stats: {:?}, color: {:08b}, castling: {:08b}, \ + en_passant: {:?}, halfmove: {}, fullmove: {} }}", + self.stats, self.color, self.castling, self.en_passant, self.halfmove, self.fullmove + ) + } +} + /// Engine communication mode. enum Mode { /// No mode, sit here and do nothing. @@ -59,11 +73,21 @@ pub enum Cmd { UciGo(Vec), // UCI "go" command. Stop, // Stop working ASAP. TmpBestMove(Option), // Send best move found by analysis worker (TEMPORARY). + WorkerInfo(Vec), // Informations from a worker. // Commands that can be sent by the engine. + /// Ask for a string to be logged or printed. + /// + /// Note that workers can send this command to engine, expecting + /// the message to be forwarded to whatever can log. + Log(String), + /// Report found best move. BestMove(Option), + /// Report ongoing analysis information. + Info(Vec), } +/// Parameters for starting work. #[derive(Clone)] struct WorkArgs { move_time: i32, @@ -73,6 +97,12 @@ struct WorkArgs { black_inc: i32, } +/// Information to be transmitted back to whatever is listening. +#[derive(Debug, Clone)] +pub enum Info { + CurrentMove(board::Move), +} + pub const CASTLING_WH_K: u8 = 0b00000001; pub const CASTLING_WH_Q: u8 = 0b00000010; pub const CASTLING_BL_K: u8 = 0b00000100; @@ -83,8 +113,10 @@ pub const CASTLING_MASK: u8 = 0b00001111; impl Engine { pub fn new() -> Engine { Engine { + debug: true, state: GameState { board: board::new_empty(), + stats: (board::BoardStats::new(), board::BoardStats::new()), color: board::SQ_WH, castling: CASTLING_MASK, en_passant: None, @@ -124,6 +156,8 @@ impl Engine { Cmd::UciGo(args) => self.uci_go(&args), Cmd::Stop => self.stop(), // Workers commands. + Cmd::Log(s) => self.reply(Cmd::Log(s.to_string())), + Cmd::WorkerInfo(infos) => self.reply(Cmd::Info(infos.to_vec())), Cmd::TmpBestMove(m) => self.reply(Cmd::BestMove(*m)), _ => eprintln!("Not an engine input command: {:?}", cmd), } @@ -142,7 +176,6 @@ impl Engine { /// /// For speed purposes, it assumes values are always valid. fn apply_fen(&mut self, fen: ¬ation::Fen) { - eprintln!("Applying FEN {:?}", fen); self.set_fen_placement(&fen.placement); self.set_fen_color(&fen.color); self.set_fen_castling(&fen.castling); @@ -202,13 +235,18 @@ impl Engine { /// /// Stop working after `movetime` ms, or go on forever if it's -1. fn work(&mut self, args: &WorkArgs) { + if self.debug { + self.reply(Cmd::Log(format!("Current evaluation: {}", evaluate(&self.state.stats)))); + } + self.working.store(true, atomic::Ordering::Relaxed); let state = self.state.clone(); let args = args.clone(); let working = self.working.clone(); let tx = match &self.mode { Mode::Uci(_, _, tx) => tx.clone(), _ => return }; + let debug = self.debug; thread::spawn(move || { - analyze(&state, &args, working, tx); + analyze(&state, &args, working, tx, debug); }); } @@ -249,6 +287,7 @@ impl Engine { } } } + board::compute_stats_into(&self.state.board, &mut self.state.stats); } /// Start working using parameters passed with a "go" command. @@ -280,6 +319,7 @@ fn analyze( _args: &WorkArgs, wip: Arc, tx: mpsc::Sender, + debug: bool, ) { if !wip.load(atomic::Ordering::Relaxed) { return; @@ -287,9 +327,38 @@ fn analyze( // Stupid engine! Return a random move. let moves = rules::get_player_legal_moves(&state.board, state.color); - let mut rng = rand::thread_rng(); - let best_move = moves.iter().choose(&mut rng).and_then(|m| Some(*m)); - thread::sleep(time::Duration::from_millis(300u64)); + if debug { + let state_str = format!("Current state: {:?}", state); + tx.send(Cmd::Log(state_str)).unwrap(); + let mut s = vec!(); + board::draw(&state.board, &mut s); + let draw_str = String::from_utf8_lossy(&s).to_string(); + tx.send(Cmd::Log(draw_str)).unwrap(); + let moves_str = format!("Legal moves: {}", notation::move_list_to_string(&moves)); + tx.send(Cmd::Log(moves_str)).unwrap(); + } + // let mut rng = rand::thread_rng(); + // let best_move = moves.iter().choose(&mut rng).and_then(|m| Some(*m)); + + let mut best_e = if board::is_white(state.color) { -999.0 } else { 999.0 }; + let mut best_move = None; + for m in moves { + // tx.send(Cmd::WorkerInfo(vec!(Info::CurrentMove(m)))).unwrap(); + let mut board = state.board.clone(); + board::apply_into(&mut board, &m); + let stats = board::compute_stats(&board); + let e = evaluate(&stats); + tx.send(Cmd::Log(format!("Move {} eval {}", notation::move_to_string(&m), e))).unwrap(); + + if + (board::is_white(state.color) && e > best_e) || + (board::is_black(state.color) && e < best_e) + { + best_e = e; + best_move = Some(m.clone()); + } + } + thread::sleep(time::Duration::from_millis(500u64)); tx.send(Cmd::TmpBestMove(best_move)).unwrap(); // thread::sleep(time::Duration::from_secs(1)); @@ -302,3 +371,37 @@ fn analyze( // } } + +fn evaluate(stats: &(board::BoardStats, board::BoardStats)) -> f32 { + let (ws, bs) = stats; + ( + 200.0 * (ws.num_kings - bs.num_kings) as f32 + + 9.0 * (ws.num_queens - bs.num_queens) as f32 + + 5.0 * (ws.num_rooks - bs.num_rooks) as f32 + + 3.0 * (ws.num_bishops - bs.num_bishops) as f32 + + 3.0 * (ws.num_knights - bs.num_knights) as f32 + + (ws.num_pawns - bs.num_pawns) as f32 + + 0.5 * ( // FIXME + ws.num_doubled_pawns - bs.num_doubled_pawns + + ws.num_isolated_pawns - bs.num_isolated_pawns + + ws.num_backward_pawns - bs.num_backward_pawns + ) as f32 + + 0.1 * (ws.mobility - bs.mobility) as f32 + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_evaluate() { + let mut b = board::new(); + let stats = board::compute_stats(&b); + assert_eq!(evaluate(&stats), 0.0); + + board::apply_into(&mut b, &(notation::parse_move("d2d4"))); + let stats = board::compute_stats(&b); + assert_eq!(evaluate(&stats), 0.0); + } +} diff --git a/src/notation.rs b/src/notation.rs index dfced69..ad70888 100644 --- a/src/notation.rs +++ b/src/notation.rs @@ -4,6 +4,7 @@ use crate::board; pub const NULL_MOVE: &str = "0000"; +/// Create a string containing the UCI algebraic notation of this move. pub fn move_to_string(m: &board::Move) -> String { let mut move_string = String::new(); move_string.push_str(&board::pos_string(&m.0)); @@ -20,6 +21,7 @@ pub fn move_to_string(m: &board::Move) -> String { move_string } +/// Parse an UCI move algebraic notation string to a board::Move. pub fn parse_move(m_str: &str) -> board::Move { let prom = if m_str.len() == 5 { Some(match m_str.as_bytes()[4] { @@ -35,6 +37,11 @@ pub fn parse_move(m_str: &str) -> board::Move { (board::pos(&m_str[0..2]), board::pos(&m_str[2..4]), prom) } +/// Create a space-separated string of moves. Used for debugging. +pub fn move_list_to_string(moves: &Vec) -> String { + moves.iter().map(|m| move_to_string(m)).collect::>().join(" ") +} + pub const FEN_START: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; /// FEN notation for positions, split into fields. diff --git a/src/uci.rs b/src/uci.rs index 8dddde7..0b8f436 100644 --- a/src/uci.rs +++ b/src/uci.rs @@ -195,11 +195,17 @@ impl Uci { /// Handle an engine command. fn handle_engine_command(&mut self, cmd: &engine::Cmd) { - self.log(format!("ENG >>> {:?}", cmd)); match cmd { engine::Cmd::UciChannel(s) => { + self.log("ENG >>> Channel opened.".to_string()); self.engine_in = Some(s.to_owned()); } + engine::Cmd::Log(s) => { + self.log(s.to_string()); + } + engine::Cmd::Info(infos) => { + self.send_infos(infos); + } engine::Cmd::BestMove(m) => { self.state = State::Ready; self.send_bestmove(m); @@ -229,6 +235,19 @@ impl Uci { self.send("readyok"); } + /// Send engine analysis information. + fn send_infos(&mut self, infos: &Vec) { + let mut s = "info".to_string(); + for i in infos { + match i { + engine::Info::CurrentMove(m) => { + s.push_str(&format!(" currmove {}", notation::move_to_string(m))); + } + } + } + self.send(&s); + } + /// Send best move. fn send_bestmove(&mut self, m: &Option) { let move_str = match m {