From 21aae1d9ba2916796d8b01f1884df3b1a9c3746d Mon Sep 17 00:00:00 2001 From: Adrien Abraham Date: Fri, 5 Jun 2020 18:46:08 +0200 Subject: [PATCH] engine: create Node type for easier analysis --- src/board.rs | 67 +++++------------ src/engine.rs | 195 +++++++++++++++++++++--------------------------- src/notation.rs | 9 ++- src/rules.rs | 173 +++++++++++++++++++++++++++++++----------- src/uci.rs | 8 +- 5 files changed, 243 insertions(+), 209 deletions(-) diff --git a/src/board.rs b/src/board.rs index e90cd1e..c4169c0 100644 --- a/src/board.rs +++ b/src/board.rs @@ -30,43 +30,47 @@ pub const SQ_BL_Q: u8 = SQ_BL|SQ_Q; pub const SQ_BL_K: u8 = SQ_BL|SQ_K; #[inline] -pub fn has_flag(i: u8, flag: u8) -> bool { i & flag == flag } +pub const 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 } +pub const 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 } +pub const 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) } +pub const 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) } +pub const 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) } +pub const 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) } +pub const fn is_black(square: u8) -> bool { is_color(square, SQ_BL) } /// Return the color of the piece on this square. #[inline] -pub fn get_color(square: u8) -> u8 { square & SQ_COLOR_MASK } +pub const fn get_color(square: u8) -> u8 { square & SQ_COLOR_MASK } /// Get opposite color. #[inline] -pub fn opposite(color: u8) -> u8 { color ^ SQ_COLOR_MASK } +pub const fn opposite(color: u8) -> u8 { color ^ SQ_COLOR_MASK } +/// Minimum allowed value for stored Pos components. pub const POS_MIN: i8 = 0; +/// Maximum allowed value for stored Pos components. pub const POS_MAX: i8 = 7; /// Coords (file, rank) of a square on a board, both components are in [0, 7]. pub type Pos = (i8, i8); +/// Check if a Pos component is in the [0, 7] range. #[inline] pub fn is_valid_pos_c(component: i8) -> bool { component >= 0 && component <= 7 } +/// Check if both `pos` components are valid. #[inline] pub fn is_valid_pos(pos: Pos) -> bool { is_valid_pos_c(pos.0) && is_valid_pos_c(pos.1) } @@ -75,7 +79,7 @@ pub fn is_valid_pos(pos: Pos) -> bool { is_valid_pos_c(pos.0) && is_valid_pos_c( /// `s` has to be valid UTF8, or the very least ASCII because chars /// are interpreted as raw bytes, and lowercase. #[inline] -pub fn pos(s: &str) -> Pos { +pub const fn pos(s: &str) -> Pos { let chars = s.as_bytes(); ((chars[0] - 0x61) as i8, (chars[1] - 0x31) as i8) } @@ -95,7 +99,7 @@ pub fn pos_string(p: &Pos) -> String { pub type Board = [u8; 64]; /// Generate the board of a new game. -pub fn new() -> Board { +pub const fn new() -> Board { [ /* 1 2 3 4 5 6 7 8 */ /* A */ SQ_WH_R, SQ_WH_P, SQ_E, SQ_E, SQ_E, SQ_E, SQ_BL_P, SQ_BL_R, @@ -110,7 +114,7 @@ pub fn new() -> Board { } /// Generate an empty board. -pub fn new_empty() -> Board { +pub const fn new_empty() -> Board { [SQ_E; 64] } @@ -147,7 +151,7 @@ pub fn eq(b1: &Board, b2: &Board) -> bool { } #[inline] -pub fn get_square(board: &Board, coords: &Pos) -> u8 { +pub const fn get_square(board: &Board, coords: &Pos) -> u8 { board[(coords.0 * 8 + coords.1) as usize] } @@ -162,7 +166,7 @@ pub fn clear_square(board: &mut Board, coords: &Pos) { } #[inline] -pub fn is_empty(board: &Board, coords: &Pos) -> bool { +pub const fn is_empty(board: &Board, coords: &Pos) -> bool { get_square(board, coords) == SQ_E } @@ -221,22 +225,6 @@ 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 { @@ -253,7 +241,7 @@ pub struct BoardStats { } impl BoardStats { - pub fn new() -> BoardStats { + pub const 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, @@ -458,23 +446,6 @@ mod tests { assert_eq!(find_king(&b, SQ_BL), pos("e8")); } - #[test] - fn test_apply_into() { - let mut b = new_empty(); - // Put 2 enemy knights on board. - set_square(&mut b, &pos("d4"), SQ_WH_N); - set_square(&mut b, &pos("f4"), SQ_BL_N); - // Move white knight in a position attacked by black knight. - apply_into(&mut b, &(pos("d4"), pos("e6"), None)); - assert_eq!(get_square(&b, &pos("d4")), SQ_E); - assert_eq!(get_square(&b, &pos("e6")), SQ_WH_N); - assert_eq!(num_pieces(&b), 2); - // Sack it with black knight - apply_into(&mut b, &(pos("f4"), pos("e6"), None)); - 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. diff --git a/src/engine.rs b/src/engine.rs index 7b53908..08142a3 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -14,7 +14,7 @@ pub struct Engine { /// Debug mode, log some data. debug: bool, /// Current game state, starting point of further analysis. - state: GameState, + node: Node, /// Communication mode. mode: Mode, /// If true, the engine is currently listening to incoming cmds. @@ -23,28 +23,33 @@ pub struct Engine { working: Arc, } -/// Representation of a game state that can cloned to analysis workers. -/// -/// It does not include various parameters such as clocks so that they -/// can be passed separately using `WorkArgs`. +/// Analysis node: a board along with the game state and some stats. #[derive(Clone)] -struct GameState { +struct Node { + /// Board for this node. board: board::Board, - stats: (board::BoardStats, board::BoardStats), // white and black pieces stats. - color: u8, - castling: u8, - en_passant: Option, - halfmove: i32, - fullmove: i32, + /// Game state. + game_state: rules::GameState, + /// White and black pieces stats; have to be recomputed if board changes. + stats: (board::BoardStats, board::BoardStats), // white and black pieces stats } -impl std::fmt::Debug for GameState { +impl Node { + fn new() -> Node { + Node { + board: board::new_empty(), + game_state: rules::GameState::new(), + stats: (board::BoardStats::new(), board::BoardStats::new()), + } + } +} + +impl std::fmt::Debug for Node { 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 + "Node {{ board: [...], game_state: {:?}, stats: {:?} }}", + self.game_state, self.stats ) } } @@ -70,7 +75,7 @@ pub enum Cmd { UciPosition(Vec), // UCI "position" command. UciGo(Vec), // UCI "go" command. Stop, // Stop working ASAP. - TmpBestMove(Option), // Send best move found by analysis worker (TEMPORARY). + TmpBestMove(Option), // Send best move found by analysis worker (TEMPORARY). WorkerInfo(Vec), // Informations from a worker. // Commands that can be sent by the engine. @@ -80,7 +85,7 @@ pub enum Cmd { /// the message to be forwarded to whatever can log. Log(String), /// Report found best move. - BestMove(Option), + BestMove(Option), /// Report ongoing analysis information. Info(Vec), } @@ -98,7 +103,7 @@ struct WorkArgs { /// Information to be transmitted back to whatever is listening. #[derive(Debug, Clone)] pub enum Info { - CurrentMove(board::Move), + CurrentMove(rules::Move), } /// General engine implementation. @@ -106,15 +111,7 @@ 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: rules::CASTLING_MASK, - en_passant: None, - halfmove: 0, - fullmove: 1, - }, + node: Node::new(), mode: Mode::No, listening: false, working: Arc::new(atomic::AtomicBool::new(false)), @@ -168,59 +165,41 @@ impl Engine { /// /// For speed purposes, it assumes values are always valid. fn apply_fen(&mut self, fen: ¬ation::Fen) { - self.set_fen_placement(&fen.placement); - self.set_fen_color(&fen.color); - self.set_fen_castling(&fen.castling); - self.set_fen_en_passant(&fen.en_passant); - self.set_fen_halfmove(&fen.halfmove); - self.set_fen_fullmove(&fen.fullmove); - } - - fn set_fen_placement(&mut self, placement: &str) { - self.state.board = board::new_from_fen(placement); - } - - fn set_fen_color(&mut self, color: &str) { - match color.chars().next().unwrap() { - 'w' => self.state.color = board::SQ_WH, - 'b' => self.state.color = board::SQ_BL, + // Placement. + self.node.board = board::new_from_fen(&fen.placement); + // Color. + match fen.color.chars().next().unwrap() { + 'w' => self.node.game_state.color = board::SQ_WH, + 'b' => self.node.game_state.color = board::SQ_BL, _ => {} - } - } - - fn set_fen_castling(&mut self, castling: &str) { - for c in castling.chars() { + }; + // Castling. + for c in fen.castling.chars() { match c { - 'K' => self.state.castling |= rules::CASTLING_WH_K, - 'Q' => self.state.castling |= rules::CASTLING_WH_Q, - 'k' => self.state.castling |= rules::CASTLING_BL_K, - 'q' => self.state.castling |= rules::CASTLING_BL_Q, + 'K' => self.node.game_state.castling |= rules::CASTLING_WH_K, + 'Q' => self.node.game_state.castling |= rules::CASTLING_WH_Q, + 'k' => self.node.game_state.castling |= rules::CASTLING_BL_K, + 'q' => self.node.game_state.castling |= rules::CASTLING_BL_Q, _ => {} } } - } - - fn set_fen_en_passant(&mut self, en_passant: &str) { - self.state.en_passant = match en_passant { + // En passant. + self.node.game_state.en_passant = match fen.en_passant.as_ref() { "-" => None, p => Some(board::pos(p)), }; + // Half moves. + self.node.game_state.halfmove = fen.halfmove.parse::().ok().unwrap(); + // Full moves. + self.node.game_state.fullmove = fen.fullmove.parse::().ok().unwrap(); } - fn set_fen_halfmove(&mut self, halfmove: &str) { - self.state.halfmove = halfmove.parse::().ok().unwrap(); - } - - fn set_fen_fullmove(&mut self, fullmove: &str) { - self.state.fullmove = fullmove.parse::().ok().unwrap(); - } - - fn apply_moves(&mut self, moves: &Vec) { + fn apply_moves(&mut self, moves: &Vec) { moves.iter().for_each(|m| self.apply_move(m)); } - fn apply_move(&mut self, m: &board::Move) { - board::apply_into(&mut self.state.board, m); + fn apply_move(&mut self, m: &rules::Move) { + rules::apply_move_to(&mut self.node.board, &mut self.node.game_state, m); } /// Start working on board, returning the best move found. @@ -228,17 +207,17 @@ 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.reply(Cmd::Log(format!("Current evaluation: {}", evaluate(&self.node.stats)))); } self.working.store(true, atomic::Ordering::Relaxed); - let state = self.state.clone(); + let node = self.node.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, debug); + analyze(&node, &args, working, tx, debug); }); } @@ -271,15 +250,10 @@ impl Engine { }, uci::PositionArgs::Moves(moves) => { self.apply_moves(&moves); - self.state.color = if moves.len() % 2 == 0 { - board::SQ_WH - } else { - board::SQ_BL - }; } } } - board::compute_stats_into(&self.state.board, &mut self.state.stats); + board::compute_stats_into(&self.node.board, &mut self.node.stats); } /// Start working using parameters passed with a "go" command. @@ -307,41 +281,43 @@ impl Engine { } fn analyze( - state: &GameState, + node: &Node, _args: &WorkArgs, - wip: Arc, + working: Arc, tx: mpsc::Sender, debug: bool, ) { - if !wip.load(atomic::Ordering::Relaxed) { + if !working.load(atomic::Ordering::Relaxed) { return; } - - let moves = rules::get_player_legal_moves(&state.board, state.color); if debug { - let state_str = format!("Current state: {:?}", state); + let state_str = format!("Analysing node: {:?}", node); tx.send(Cmd::Log(state_str)).unwrap(); let mut s = vec!(); - board::draw(&state.board, &mut s); + board::draw(&node.board, &mut s); let draw_str = String::from_utf8_lossy(&s).to_string(); tx.send(Cmd::Log(draw_str)).unwrap(); + } + + let moves = rules::get_player_legal_moves(&node.board, &node.game_state); + if debug { let moves_str = format!("Legal moves: {}", notation::move_list_to_string(&moves)); tx.send(Cmd::Log(moves_str)).unwrap(); } - let mut best_e = if board::is_white(state.color) { -999.0 } else { 999.0 }; + let mut best_e = if board::is_white(node.game_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 mut board = node.board.clone(); + let mut game_state = node.game_state.clone(); + rules::apply_move_to(&mut board, &mut game_state, &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) + (board::is_white(game_state.color) && e > best_e) || + (board::is_black(game_state.color) && e < best_e) { best_e = e; best_move = Some(m.clone()); @@ -363,20 +339,19 @@ 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 - ) + + 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)] @@ -385,12 +360,16 @@ mod tests { #[test] fn test_evaluate() { - let mut b = board::new(); - let stats = board::compute_stats(&b); + let mut node = Node::new(); + let stats = board::compute_stats(&node.board); assert_eq!(evaluate(&stats), 0.0); - board::apply_into(&mut b, &(notation::parse_move("d2d4"))); - let stats = board::compute_stats(&b); + rules::apply_move_to_board( + &mut node.board, + &node.game_state, + ¬ation::parse_move("d2d4") + ); + let stats = board::compute_stats(&node.board); assert_eq!(evaluate(&stats), 0.0); } } diff --git a/src/notation.rs b/src/notation.rs index ad70888..e5aca4b 100644 --- a/src/notation.rs +++ b/src/notation.rs @@ -1,11 +1,12 @@ //! Functions using various notations. use crate::board; +use crate::rules; 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 { +pub fn move_to_string(m: &rules::Move) -> String { let mut move_string = String::new(); move_string.push_str(&board::pos_string(&m.0)); move_string.push_str(&board::pos_string(&m.1)); @@ -21,8 +22,8 @@ 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 { +/// Parse an UCI move algebraic notation string to a rules::Move. +pub fn parse_move(m_str: &str) -> rules::Move { let prom = if m_str.len() == 5 { Some(match m_str.as_bytes()[4] { b'b' => board::SQ_B, @@ -38,7 +39,7 @@ pub fn parse_move(m_str: &str) -> board::Move { } /// Create a space-separated string of moves. Used for debugging. -pub fn move_list_to_string(moves: &Vec) -> String { +pub fn move_list_to_string(moves: &Vec) -> String { moves.iter().map(|m| move_to_string(m)).collect::>().join(" ") } diff --git a/src/rules.rs b/src/rules.rs index 7af34af..200e468 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -2,6 +2,31 @@ use crate::board::*; +/// Characteristics of the state of a game. +/// +/// It does not include various parameters such as clocks that are +/// more aimed for engine analysis than typical rules checking. +#[derive(Debug, Clone)] +pub struct GameState { + pub color: u8, + pub castling: u8, + pub en_passant: Option, + pub halfmove: i32, + pub fullmove: i32, +} + +impl GameState { + pub const fn new() -> GameState { + GameState { + color: SQ_WH, + castling: CASTLING_MASK, + en_passant: None, + halfmove: 0, + fullmove: 1, + } + } +} + pub const CASTLING_WH_K: u8 = 0b00000001; pub const CASTLING_WH_Q: u8 = 0b00000010; pub const CASTLING_BL_K: u8 = 0b00000100; @@ -11,13 +36,41 @@ pub const CASTLING_MASK: u8 = 0b00001111; pub const START_WH_K_POS: Pos = pos("e1"); pub const START_BL_K_POS: Pos = pos("e8"); +/// A movement, with before/after positions and optional promotion. +pub type Move = (Pos, Pos, Option); + +/// Apply a move `m` to copies to `board` and `game_state`. +pub fn apply_move(board: &Board, game_state: &GameState, m: &Move) -> (Board, GameState) { + let mut new_board = board.clone(); + let mut new_state = game_state.clone(); + apply_move_to(&mut new_board, &mut new_state, m); + (new_board, new_state) +} + +pub fn apply_move_to(board: &mut Board, game_state: &mut GameState, m: &Move) { + apply_move_to_board(board, game_state, m); + apply_move_to_state(game_state, m); +} + +/// Apply a move `m` into `board`. +pub fn apply_move_to_board(board: &mut Board, _game_state: &GameState, m: &Move) { + // TODO handle castling + set_square(board, &m.1, get_square(board, &m.0)); + clear_square(board, &m.0) +} + +pub fn apply_move_to_state(game_state: &mut GameState, _m: &Move) { + game_state.color = opposite(game_state.color); + // TODO handle castling +} + /// Get a list of legal moves for all pieces of this color. -pub fn get_player_legal_moves(board: &Board, color: u8, castling: u8) -> Vec { - filter_illegal_moves(board, color, get_player_moves(board, color, castling)) +pub fn get_player_legal_moves(board: &Board, game_state: &GameState) -> Vec { + filter_illegal_moves(board, game_state, get_player_moves(board, game_state)) } /// Get a list of moves for all pieces of this color. -pub fn get_player_moves(board: &Board, color: u8, castling: u8) -> Vec { +pub fn get_player_moves(board: &Board, game_state: &GameState) -> Vec { let mut moves = vec!(); for r in 0..8 { for f in 0..8 { @@ -25,8 +78,8 @@ pub fn get_player_moves(board: &Board, color: u8, castling: u8) -> Vec { if is_empty(board, &p) { continue } - if is_color(get_square(board, &p), color) { - moves.append(&mut get_piece_moves(board, &p, castling)); + if is_color(get_square(board, &p), game_state.color) { + moves.append(&mut get_piece_moves(board, &p, game_state)); } } } @@ -34,14 +87,14 @@ pub fn get_player_moves(board: &Board, color: u8, castling: u8) -> Vec { } /// Get a list of moves for the piece at position `at`. -pub fn get_piece_moves(board: &Board, at: &Pos, castling: u8) -> Vec { +pub fn get_piece_moves(board: &Board, at: &Pos, game_state: &GameState) -> Vec { match get_square(board, at) { p if is_piece(p, SQ_P) => get_pawn_moves(board, at, p), p if is_piece(p, SQ_B) => get_bishop_moves(board, at, p), p if is_piece(p, SQ_N) => get_knight_moves(board, at, p), p if is_piece(p, SQ_R) => get_rook_moves(board, at, p), p if is_piece(p, SQ_Q) => get_queen_moves(board, at, p), - p if is_piece(p, SQ_K) => get_king_moves(board, at, p, castling), + p if is_piece(p, SQ_K) => get_king_moves(board, at, p, game_state), _ => vec!(), } } @@ -171,7 +224,7 @@ fn get_queen_moves(board: &Board, at: &Pos, piece: u8) -> Vec { moves } -fn get_king_moves(board: &Board, at: &Pos, piece: u8, castling: u8) -> Vec { +fn get_king_moves(board: &Board, at: &Pos, piece: u8, _game_state: &GameState) -> Vec { let (f, r) = at; let mut moves = vec!(); for offset in [(-1, 1), (0, 1), (1, 1), (-1, 0), (1, 0), (-1, -1), (0, -1), (1, -1)].iter() { @@ -211,24 +264,24 @@ fn move_on_enemy(piece1: u8, pos1: &Pos, piece2: u8, pos2: &Pos) -> Option /// Return an iterator filtering out illegal moves from given list. /// /// Pass color of moving player to avoid checking it for every move. -fn filter_illegal_moves(board: &Board, color: u8, castling: u8, moves: Vec) -> Vec { - let king_p = find_king(board, color); +fn filter_illegal_moves(board: &Board, game_state: &GameState, moves: Vec) -> Vec { + let king_p = find_king(board, game_state.color); moves.into_iter().filter(|m| { // If king moved, use its new position. let king_p = if m.0 == king_p { m.1 } else { king_p }; - let new_board = apply(board, m); + let (new_board, new_state) = apply_move(board, game_state, m); // Check if the move makes the player king in check. - if is_attacked(&new_board, &king_p, castling) { - return false - } - true + !is_attacked(&new_board, &new_state, &king_p) }).collect() } /// Return true if the piece at position `at` is attacked. -fn is_attacked(board: &Board, at: &Pos, castling: u8) -> bool { - let color = get_color(get_square(board, at)); - let enemy_moves = get_player_moves(board, opposite(color), castling); +/// +/// Beware that the game state must be coherent with the analysed +/// square, i.e. if the piece at `at` is white, the game state should +/// tell that it is black turn. +fn is_attacked(board: &Board, game_state: &GameState, at: &Pos) -> bool { + let enemy_moves = get_player_moves(board, game_state); for m in enemy_moves.iter() { if *at == m.1 { return true @@ -241,26 +294,48 @@ fn is_attacked(board: &Board, at: &Pos, castling: u8) -> bool { mod tests { use super::*; + #[test] + fn test_apply_move_to_board() { + let mut b = new_empty(); + let s = GameState::new(); + + // Put 2 enemy knights on board. + set_square(&mut b, &pos("d4"), SQ_WH_N); + set_square(&mut b, &pos("f4"), SQ_BL_N); + // Move white knight in a position attacked by black knight. + apply_move_to_board(&mut b, &s, &(pos("d4"), pos("e6"), None)); + assert_eq!(get_square(&b, &pos("d4")), SQ_E); + assert_eq!(get_square(&b, &pos("e6")), SQ_WH_N); + assert_eq!(num_pieces(&b), 2); + // Sack it with black knight + apply_move_to_board(&mut b, &s, &(pos("f4"), pos("e6"), None)); + assert_eq!(get_square(&b, &pos("e6")), SQ_BL_N); + assert_eq!(num_pieces(&b), 1); + } + #[test] fn test_get_player_moves() { let b = new(); + let s = GameState::new(); + // At first move, white has 16 pawn moves and 4 knight moves. - let moves = get_player_moves(&b, SQ_WH, CASTLING_MASK); + let moves = get_player_moves(&b, &s); assert_eq!(moves.len(), 20); } #[test] fn test_get_pawn_moves() { let mut b = new_empty(); + let s = GameState::new(); // Check that a pawn (here white queen's pawn) can move forward if the road is free. set_square(&mut b, &pos("d3"), SQ_WH_P); - let moves = get_piece_moves(&b, &pos("d3")); + let moves = get_piece_moves(&b, &pos("d3"), &s); assert!(moves.len() == 1 && moves.contains( &(pos("d3"), pos("d4"), None) )); // Check that a pawn (here white king's pawn) can move 2 square forward on first move. set_square(&mut b, &pos("e2"), SQ_WH_P); - let moves = get_piece_moves(&b, &pos("e2")); + let moves = get_piece_moves(&b, &pos("e2"), &s); assert_eq!(moves.len(), 2); assert!(moves.contains( &(pos("e2"), pos("e3"), None) )); assert!(moves.contains( &(pos("e2"), pos("e4"), None) )); @@ -268,23 +343,23 @@ mod tests { // Check that a pawn cannot move forward if a piece is blocking its path. // 1. black pawn 2 square forward; only 1 square forward available from start pos. set_square(&mut b, &pos("e4"), SQ_BL_P); - let moves = get_piece_moves(&b, &pos("e2")); + let moves = get_piece_moves(&b, &pos("e2"), &s); assert!(moves.len() == 1 && moves.contains( &(pos("e2"), pos("e3"), None) )); // 2. black pawn 1 square forward; no square available. set_square(&mut b, &pos("e3"), SQ_BL_P); - let moves = get_piece_moves(&b, &pos("e2")); + let moves = get_piece_moves(&b, &pos("e2"), &s); assert_eq!(moves.len(), 0); // 3. remove the e4 black pawn; the white pawn should not be able to jump above e3 pawn. clear_square(&mut b, &pos("e4")); - let moves = get_piece_moves(&b, &pos("e2")); + let moves = get_piece_moves(&b, &pos("e2"), &s); assert_eq!(moves.len(), 0); // Check that a pawn can take a piece diagonally. set_square(&mut b, &pos("f3"), SQ_BL_P); - let moves = get_piece_moves(&b, &pos("e2")); + let moves = get_piece_moves(&b, &pos("e2"), &s); assert!(moves.len() == 1 && moves.contains( &(pos("e2"), pos("f3"), None) )); set_square(&mut b, &pos("d3"), SQ_BL_P); - let moves = get_piece_moves(&b, &pos("e2")); + let moves = get_piece_moves(&b, &pos("e2"), &s); assert_eq!(moves.len(), 2); assert!(moves.contains( &(pos("e2"), pos("f3"), None) )); assert!(moves.contains( &(pos("e2"), pos("d3"), None) )); @@ -292,17 +367,18 @@ mod tests { // Check that a pawn moving to the last rank leads to queen promotion. // 1. by simply moving forward. set_square(&mut b, &pos("a7"), SQ_WH_P); - let moves = get_piece_moves(&b, &pos("a7")); + let moves = get_piece_moves(&b, &pos("a7"), &s); assert!(moves.len() == 1 && moves.contains( &(pos("a7"), pos("a8"), Some(SQ_Q)) )); } #[test] fn test_get_bishop_moves() { let mut b = new_empty(); + let s = GameState::new(); // A bishop has maximum range when it's in a center square. set_square(&mut b, &pos("d4"), SQ_WH_B); - let moves = get_piece_moves(&b, &pos("d4")); + let moves = get_piece_moves(&b, &pos("d4"), &s); assert_eq!(moves.len(), 13); // Going top-right. assert!(moves.contains( &(pos("d4"), pos("e5"), None) )); @@ -324,89 +400,96 @@ mod tests { // When blocking sight to one square with friendly piece, lose 2 moves. set_square(&mut b, &pos("b2"), SQ_WH_P); - assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 11); + assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 11); // When blocking sight to one square with enemy piece, lose only 1 move. set_square(&mut b, &pos("b2"), SQ_BL_P); - assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 12); + assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 12); } #[test] fn test_get_knight_moves() { let mut b = new_empty(); + let s = GameState::new(); // A knight never has blocked sight; if it's in the center of the board, it can have up to // 8 moves. set_square(&mut b, &pos("d4"), SQ_WH_N); - assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 8); + assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 8); // If on a side if has only 4 moves. set_square(&mut b, &pos("a4"), SQ_WH_N); - assert_eq!(get_piece_moves(&b, &pos("a4")).len(), 4); + assert_eq!(get_piece_moves(&b, &pos("a4"), &s).len(), 4); // And in a corner, only 2 moves. set_square(&mut b, &pos("a1"), SQ_WH_N); - assert_eq!(get_piece_moves(&b, &pos("a1")).len(), 2); + assert_eq!(get_piece_moves(&b, &pos("a1"), &s).len(), 2); // Add 2 friendly pieces and it is totally blocked. set_square(&mut b, &pos("b3"), SQ_WH_P); set_square(&mut b, &pos("c2"), SQ_WH_P); - assert_eq!(get_piece_moves(&b, &pos("a1")).len(), 0); + assert_eq!(get_piece_moves(&b, &pos("a1"), &s).len(), 0); } #[test] fn test_get_rook_moves() { let mut b = new_empty(); + let s = GameState::new(); set_square(&mut b, &pos("d4"), SQ_WH_R); - assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 14); + assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 14); set_square(&mut b, &pos("d6"), SQ_BL_P); - assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 12); + assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 12); set_square(&mut b, &pos("d6"), SQ_WH_P); - assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 11); + assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 11); } #[test] fn test_get_queen_moves() { let mut b = new_empty(); + let s = GameState::new(); set_square(&mut b, &pos("d4"), SQ_WH_Q); - assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 14 + 13); // Bishop + rook moves. + assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 14 + 13); // Bishop + rook moves. } #[test] fn test_get_king_moves() { let mut b = new_empty(); + let s = GameState::new(); set_square(&mut b, &pos("d4"), SQ_WH_K); - assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 8); + assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 8); set_square(&mut b, &pos("e5"), SQ_WH_P); - assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 7); + assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 7); } #[test] fn test_filter_illegal_moves() { let mut b = new_empty(); + let s = GameState::new(); // Place white's king on first rank. set_square(&mut b, &pos("e1"), SQ_WH_K); // Place black rook in second rank: king can only move left or right. set_square(&mut b, &pos("h2"), SQ_BL_R); - let all_wh_moves = get_piece_moves(&b, &pos("e1")); + let all_wh_moves = get_piece_moves(&b, &pos("e1"), &s); assert_eq!(all_wh_moves.len(), 5); - assert_eq!(filter_illegal_moves(&b, SQ_WH, all_wh_moves).len(), 2); + assert_eq!(filter_illegal_moves(&b, &s, all_wh_moves).len(), 2); } #[test] fn test_is_attacked() { let mut b = new_empty(); + let mut s = GameState::new(); + s.color = SQ_BL; // Place a black rook in white pawn's file. set_square(&mut b, &pos("d4"), SQ_WH_P); set_square(&mut b, &pos("d6"), SQ_BL_R); - assert!(is_attacked(&b, &pos("d4"))); + assert!(is_attacked(&b, &s, &pos("d4"))); // Move the rook on another file, no more attack. - apply_into(&mut b, &(pos("d6"), pos("e6"), None)); - assert!(!is_attacked(&b, &pos("d4"))); + apply_move_to(&mut b, &mut s, &(pos("d6"), pos("e6"), None)); + assert!(!is_attacked(&b, &s, &pos("d4"))); } } diff --git a/src/uci.rs b/src/uci.rs index 0b8f436..91670c1 100644 --- a/src/uci.rs +++ b/src/uci.rs @@ -5,9 +5,9 @@ use std::io::{self, Write}; use std::sync::mpsc; use std::thread; -use crate::board; use crate::engine; use crate::notation; +use crate::rules; const VATU_NAME: &str = env!("CARGO_PKG_NAME"); const VATU_AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); @@ -57,13 +57,13 @@ pub enum UciCmd { pub enum PositionArgs { Startpos, Fen(notation::Fen), - Moves(Vec), + Moves(Vec), } /// Arguments for the go remote commands. #[derive(Debug, Clone)] pub enum GoArgs { - SearchMoves(Vec), + SearchMoves(Vec), Ponder, WTime(i32), BTime(i32), @@ -249,7 +249,7 @@ impl Uci { } /// Send best move. - fn send_bestmove(&mut self, m: &Option) { + fn send_bestmove(&mut self, m: &Option) { let move_str = match m { Some(m) => notation::move_to_string(m), None => notation::NULL_MOVE.to_string(),