engine: use negamax

Move a lot of stuff around for better organisation and rewrite engine
analysis / evaluation to be a bit more side agnostic.
This commit is contained in:
dece 2020-06-14 20:32:40 +02:00
parent e1b1642d37
commit 938fcde9fa
10 changed files with 527 additions and 501 deletions

View file

@ -1,80 +1,26 @@
//! Analysis functions. //! Analysis functions.
use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, atomic, mpsc}; use std::sync::{Arc, atomic, mpsc};
use dashmap::DashMap;
use crate::board; use crate::board;
use crate::engine; use crate::engine;
use crate::movement::Move;
use crate::node::Node;
use crate::notation; use crate::notation;
use crate::rules; use crate::rules;
use crate::stats; use crate::stats;
/// Analysis node: a board along with the game state. const MIN_F32: f32 = std::f32::NEG_INFINITY;
#[derive(Clone)] const MAX_F32: f32 = std::f32::INFINITY;
pub struct Node {
/// Board for this node. /// Analysis worker.
pub board: board::Board, pub struct Analyzer {
/// Game state. pub debug: bool,
pub game_state: rules::GameState, node: Node,
engine_tx: mpsc::Sender<engine::Cmd>,
max_depth: u32,
} }
impl Node {
/// Create a new node for an empty board and a new game state.
pub fn new() -> Node {
Node {
board: board::new_empty(),
game_state: rules::GameState::new(),
}
}
}
impl fmt::Debug for Node {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Node {{ board: [...], game_state: {:?} }}",
self.game_state
)
}
}
impl fmt::Display for Node {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut s = vec!();
board::draw(&self.board, &mut s);
let board_drawing = String::from_utf8_lossy(&s).to_string();
write!(
f,
"* Board:\n{}\n\
* Game state:\n{}",
board_drawing, self.game_state
)
}
}
impl PartialEq for Node {
fn eq(&self, other: &Self) -> bool {
(
self.board.iter().zip(other.board.iter()).all(|(a, b)| a == b) &&
self.game_state == other.game_state
)
}
}
impl Eq for Node {}
impl Hash for Node {
fn hash<H: Hasher>(&self, state: &mut H) {
self.board.iter().for_each(|square| state.write_u8(*square));
self.game_state.hash(state);
}
}
pub type NodeEvalMap = Arc<DashMap<Node, f32>>;
/// Analysis parameters. /// Analysis parameters.
#[derive(Clone)] #[derive(Clone)]
pub struct AnalysisParams { pub struct AnalysisParams {
@ -85,113 +31,80 @@ pub struct AnalysisParams {
pub black_inc: i32, pub black_inc: i32,
} }
const MIN_F32: f32 = std::f32::NEG_INFINITY; impl Analyzer {
const MAX_F32: f32 = std::f32::INFINITY; /// Create a new worker to analyze from `node`.
pub fn new(node: Node, engine_tx: mpsc::Sender<engine::Cmd>) -> Analyzer {
/// Analyse best moves for a given node. Analyzer { debug: false, node, engine_tx, max_depth: 1 }
pub fn analyze(
node: &mut Node,
_args: &AnalysisParams,
score_map: &NodeEvalMap,
working: Arc<atomic::AtomicBool>,
tx: mpsc::Sender<engine::Cmd>,
debug: bool,
) {
if !working.load(atomic::Ordering::Relaxed) {
return;
}
if debug {
tx.send(engine::Cmd::Log(format!("\tAnalyzing node:\n{}", node))).unwrap();
let moves = rules::get_player_moves(&node.board, &node.game_state, true);
let moves_str = format!("\tLegal moves: {}", notation::move_list_to_string(&moves));
tx.send(engine::Cmd::Log(moves_str)).unwrap();
} }
let (max_score, best_move) = minimax( fn log(&self, message: String) {
node, self.engine_tx.send(engine::Cmd::Log(message)).unwrap();
0,
3,
board::is_white(node.game_state.color),
&score_map,
);
if best_move.is_some() {
let log_str = format!(
"\tBest move {} evaluated {}",
notation::move_to_string(&best_move.unwrap()), max_score
);
tx.send(engine::Cmd::Log(log_str)).unwrap();
tx.send(engine::Cmd::TmpBestMove(best_move)).unwrap();
} else {
// If no best move could be found, checkmate is unavoidable; send the first legal move.
tx.send(engine::Cmd::Log("Checkmate is unavoidable.".to_string())).unwrap();
let moves = rules::get_player_moves(&node.board, &node.game_state, true);
let m = if moves.len() > 0 { Some(moves[0]) } else { None };
tx.send(engine::Cmd::TmpBestMove(m)).unwrap();
} }
// thread::sleep(time::Duration::from_secs(1)); /// Analyse best moves for the node.
// for _ in 0..4 { ///
// let board = board.clone(); /// - `args`: parameters provided for this analysis.
// let wip = wip.clone(); /// - `score_map`: a NodeEvalMap to read and update.
// thread::spawn(move || { /// - `working`: flag telling whether to keep working or to stop.
// analyze(&board, wip); pub fn analyze(
// }); &mut self,
// } _args: &AnalysisParams,
working: Arc<atomic::AtomicBool>,
) {
if !working.load(atomic::Ordering::Relaxed) {
return;
}
if self.debug {
self.log(format!("Analyzing node:\n{}", &self.node));
let moves = self.node.get_player_moves(true);
self.log(format!("Legal moves: {}", notation::move_list_to_string(&moves)));
}
} self.max_depth = 2;
let color_factor = if board::is_white(self.node.game_state.color) { 1 } else { -1 } as f32;
let (max_score, best_move) = self.negamax(&self.node, 0, color_factor);
/// Provide a "minimax" score for this node. if best_move.is_some() {
/// let log_str = format!(
/// This method recursively looks alternatively for minimum score for "Best move {} evaluated {}",
/// one player, then maximum for its opponent; that way it assumes the notation::move_to_string(&best_move.unwrap()), max_score
/// opponent always does their best. );
/// self.log(log_str);
/// `depth` is increased at each recursive call; when `max_depth` is self.engine_tx.send(engine::Cmd::TmpBestMove(best_move)).unwrap();
/// reached, evaluate the current node and return its score.
///
/// `maximizing` specifies whether the method should look for the
/// highest possible score (when true) or the lowest (when false).
fn minimax(
node: &mut Node,
depth: u32,
max_depth: u32,
maximizing: bool,
score_map: &NodeEvalMap,
) -> (f32, Option<rules::Move>) {
// If the node has already been analysed before, return its previous evaluation.
if let Some(score) = score_map.get(node) {
return (*score.value(), None)
}
// If we reached max depth, evaluate score for this board, store score and stop recursion.
if depth == max_depth {
let stats = stats::compute_stats(&node.board, &node.game_state);
let score = evaluate(&stats);
score_map.insert(node.clone(), score);
return (score, None);
}
// Else, get the minimax score.
let mut minmax = if maximizing { MIN_F32 } else { MAX_F32 };
let mut minmax_move = None;
let moves = rules::get_player_moves(&node.board, &node.game_state, true);
for m in moves {
let mut sub_node = node.clone();
rules::apply_move_to(&mut sub_node.board, &mut sub_node.game_state, &m);
if maximizing {
let (score, _) = minimax(&mut sub_node, depth + 1, max_depth, false, score_map);
if score >= minmax {
minmax = score;
minmax_move = Some(m);
}
} else { } else {
let (score, _) = minimax(&mut sub_node, depth + 1, max_depth, true, score_map); // If no best move could be found, checkmate is unavoidable; send the first legal move.
if score <= minmax { self.log("Checkmate is unavoidable.".to_string());
minmax = score; let moves = rules::get_player_moves(&self.node.board, &self.node.game_state, true);
minmax_move = Some(m); let m = if moves.len() > 0 { Some(moves[0]) } else { None };
} self.engine_tx.send(engine::Cmd::TmpBestMove(m)).unwrap();
} }
} }
(minmax, minmax_move)
fn negamax(
&self,
node: &Node,
depth: u32,
color_f: f32,
) -> (f32, Option<Move>) {
if depth == self.max_depth {
let stats = node.compute_stats();
return (color_f * evaluate(&stats), None)
}
let moves = node.get_player_moves(true);
let mut best_score = MIN_F32;
let mut best_move = None;
for m in moves {
let mut sub_node = node.clone();
sub_node.apply_move(&m);
let (score, _) = self.negamax(&mut sub_node, depth + 1, -color_f);
let score = -score;
if score >= best_score {
best_score = score;
best_move = Some(m);
}
}
(best_score, best_move)
}
} }
/// Compute a score for white/black board stats. /// Compute a score for white/black board stats.
@ -200,58 +113,47 @@ fn minimax(
/// "Programming a Computer for Playing Chess", as it is quite simple /// "Programming a Computer for Playing Chess", as it is quite simple
/// yet provide good enough results. /// yet provide good enough results.
fn evaluate(stats: &(stats::BoardStats, stats::BoardStats)) -> f32 { fn evaluate(stats: &(stats::BoardStats, stats::BoardStats)) -> f32 {
let (ws, bs) = stats; let (player_stats, opponent_stats) = stats;
200.0 * (ws.num_kings - bs.num_kings) as f32 200.0 * (player_stats.num_kings - opponent_stats.num_kings) as f32
+ 9.0 * (ws.num_queens - bs.num_queens) as f32 + 9.0 * (player_stats.num_queens - opponent_stats.num_queens) as f32
+ 5.0 * (ws.num_rooks - bs.num_rooks) as f32 + 5.0 * (player_stats.num_rooks - opponent_stats.num_rooks) as f32
+ 3.0 * (ws.num_bishops - bs.num_bishops) as f32 + 3.0 * (player_stats.num_bishops - opponent_stats.num_bishops) as f32
+ 3.0 * (ws.num_knights - bs.num_knights) as f32 + 3.0 * (player_stats.num_knights - opponent_stats.num_knights) as f32
+ (ws.num_pawns - bs.num_pawns) as f32 + (player_stats.num_pawns - opponent_stats.num_pawns) as f32
- 0.5 * ( - 0.5 * (
ws.num_doubled_pawns - bs.num_doubled_pawns + player_stats.num_doubled_pawns - opponent_stats.num_doubled_pawns +
ws.num_isolated_pawns - bs.num_isolated_pawns + player_stats.num_isolated_pawns - opponent_stats.num_isolated_pawns +
ws.num_backward_pawns - bs.num_backward_pawns player_stats.num_backward_pawns - opponent_stats.num_backward_pawns
) as f32 ) as f32
+ 0.1 * (ws.mobility - bs.mobility) as f32 + 0.1 * (player_stats.mobility - opponent_stats.mobility) as f32
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; // use super::*;
use board::pos;
#[test] #[test]
fn test_minimax() { fn test_minimax() {
let mut node = Node::new(); // FIXME
node.game_state.castling = 0; // let mut node = Node::new();
// node.game_state.castling = 0;
// White mates in 1 move, queen to d7. // // White mates in 1 move, queen to d7.
board::set_square(&mut node.board, &pos("a1"), board::SQ_WH_K); // board::set_square(&mut node.board, &pos("a1"), board::SQ_WH_K);
board::set_square(&mut node.board, &pos("c6"), board::SQ_WH_P); // board::set_square(&mut node.board, &pos("c6"), board::SQ_WH_P);
board::set_square(&mut node.board, &pos("h7"), board::SQ_WH_Q); // board::set_square(&mut node.board, &pos("h7"), board::SQ_WH_Q);
board::set_square(&mut node.board, &pos("d8"), board::SQ_BL_K); // board::set_square(&mut node.board, &pos("d8"), board::SQ_BL_K);
let (_, m) = minimax(&mut node, 0, 2, true); // let (_, m) = minimax(&mut node, 0, 2, true);
assert_eq!(m.unwrap(), notation::parse_move("h7d7")); // assert_eq!(m.unwrap(), notation::parse_move("h7d7"));
// Check that it works for black as well. // // Check that it works for black as well.
board::set_square(&mut node.board, &pos("a1"), board::SQ_BL_K); // board::set_square(&mut node.board, &pos("a1"), board::SQ_BL_K);
board::set_square(&mut node.board, &pos("c6"), board::SQ_BL_P); // board::set_square(&mut node.board, &pos("c6"), board::SQ_BL_P);
board::set_square(&mut node.board, &pos("h7"), board::SQ_BL_Q); // board::set_square(&mut node.board, &pos("h7"), board::SQ_BL_Q);
board::set_square(&mut node.board, &pos("d8"), board::SQ_WH_K); // board::set_square(&mut node.board, &pos("d8"), board::SQ_WH_K);
node.game_state.color = board::SQ_BL; // node.game_state.color = board::SQ_BL;
let (_, m) = minimax(&mut node, 0, 2, true); // let (_, m) = minimax(&mut node, 0, 2, true);
assert_eq!(m.unwrap(), notation::parse_move("h7d7")); // assert_eq!(m.unwrap(), notation::parse_move("h7d7"));
}
#[test]
fn test_evaluate() {
let mut node = Node::new();
let stats = stats::compute_stats(&node.board, &node.game_state);
assert_eq!(evaluate(&stats), 0.0);
rules::apply_move_to(&mut node.board, &mut node.game_state, &notation::parse_move("d2d4"));
let stats = stats::compute_stats(&node.board, &node.game_state);
assert_eq!(evaluate(&stats), 0.0);
} }
} }

19
src/castling.rs Normal file
View file

@ -0,0 +1,19 @@
//! Castling flags.
pub const CASTLING_WH_K: u8 = 0b00000001;
pub const CASTLING_WH_Q: u8 = 0b00000010;
pub const CASTLING_WH_MASK: u8 = 0b00000011;
pub const CASTLING_BL_K: u8 = 0b00000100;
pub const CASTLING_BL_Q: u8 = 0b00001000;
pub const CASTLING_BL_MASK: u8 = 0b00001100;
pub const CASTLING_K_MASK: u8 = 0b00000101;
pub const CASTLING_Q_MASK: u8 = 0b00001010;
pub const CASTLING_MASK: u8 = 0b00001111;
/// Castling sides parameters.
///
/// For both sides, the 3-uple contains files that should be empty
/// and not attacked, an optional file that should be empty for
/// queen-side, and the castling side-mask.
pub const CASTLING_SIDES: [([i8; 2], Option<i8>, u8); 2] =
[([5i8, 6i8], None, CASTLING_K_MASK), ([3i8, 2i8], Some(1i8), CASTLING_Q_MASK)];

View file

@ -3,15 +3,17 @@
//! Hold the various data needed to perform a game analysis, //! Hold the various data needed to perform a game analysis,
//! but actual analysis code is in the `analysis` module. //! but actual analysis code is in the `analysis` module.
use std::sync::{Arc, atomic, mpsc}; use std::sync::Arc;
use std::sync::mpsc;
use std::sync::atomic::{self, AtomicBool};
use std::thread; use std::thread;
use dashmap::DashMap;
use crate::analysis; use crate::analysis;
use crate::board; use crate::board;
use crate::castling;
use crate::movement::{self, Move};
use crate::node::Node;
use crate::notation; use crate::notation;
use crate::rules;
use crate::uci; use crate::uci;
/// Analysis engine. /// Analysis engine.
@ -19,15 +21,13 @@ pub struct Engine {
/// Debug mode, log some data. /// Debug mode, log some data.
debug: bool, debug: bool,
/// Current game state, starting point of further analysis. /// Current game state, starting point of further analysis.
node: analysis::Node, node: Node,
/// Store already evaluated nodes with their score.
score_map: Arc<DashMap<analysis::Node, f32>>,
/// Communication mode. /// Communication mode.
mode: Mode, mode: Mode,
/// If true, the engine is currently listening to incoming cmds. /// If true, the engine is currently listening to incoming cmds.
listening: bool, listening: bool,
/// Shared flag to notify workers if they should keep working. /// flag to notify workers if they should keep working.
working: Arc<atomic::AtomicBool>, working: Arc<AtomicBool>,
} }
/// Engine communication mode. /// Engine communication mode.
@ -52,7 +52,7 @@ pub enum Cmd {
UciPosition(Vec<uci::PositionArgs>), // UCI "position" command. UciPosition(Vec<uci::PositionArgs>), // UCI "position" command.
UciGo(Vec<uci::GoArgs>), // UCI "go" command. UciGo(Vec<uci::GoArgs>), // UCI "go" command.
Stop, // Stop working ASAP. Stop, // Stop working ASAP.
TmpBestMove(Option<rules::Move>), // Send best move found by analysis worker (TEMPORARY). TmpBestMove(Option<Move>), // Send best move found by analysis worker (TEMPORARY).
WorkerInfo(Vec<Info>), // Informations from a worker. WorkerInfo(Vec<Info>), // Informations from a worker.
// Commands that can be sent by the engine. // Commands that can be sent by the engine.
@ -62,7 +62,7 @@ pub enum Cmd {
/// the message to be forwarded to whatever can log. /// the message to be forwarded to whatever can log.
Log(String), Log(String),
/// Report found best move. /// Report found best move.
BestMove(Option<rules::Move>), BestMove(Option<Move>),
/// Report ongoing analysis information. /// Report ongoing analysis information.
Info(Vec<Info>), Info(Vec<Info>),
} }
@ -70,7 +70,7 @@ pub enum Cmd {
/// Information to be transmitted back to whatever is listening. /// Information to be transmitted back to whatever is listening.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Info { pub enum Info {
CurrentMove(rules::Move), CurrentMove(Move),
} }
/// General engine implementation. /// General engine implementation.
@ -78,11 +78,10 @@ impl Engine {
pub fn new() -> Engine { pub fn new() -> Engine {
Engine { Engine {
debug: false, debug: false,
node: analysis::Node::new(), node: Node::new(),
score_map: Arc::new(DashMap::with_capacity(2usize.pow(10))),
mode: Mode::No, mode: Mode::No,
listening: false, listening: false,
working: Arc::new(atomic::AtomicBool::new(false)), working: Arc::new(AtomicBool::new(false)),
} }
} }
@ -146,10 +145,10 @@ impl Engine {
// Castling. // Castling.
for c in fen.castling.chars() { for c in fen.castling.chars() {
match c { match c {
'K' => self.node.game_state.castling |= rules::CASTLING_WH_K, 'K' => self.node.game_state.castling |= castling::CASTLING_WH_K,
'Q' => self.node.game_state.castling |= rules::CASTLING_WH_Q, 'Q' => self.node.game_state.castling |= castling::CASTLING_WH_Q,
'k' => self.node.game_state.castling |= rules::CASTLING_BL_K, 'k' => self.node.game_state.castling |= castling::CASTLING_BL_K,
'q' => self.node.game_state.castling |= rules::CASTLING_BL_Q, 'q' => self.node.game_state.castling |= castling::CASTLING_BL_Q,
_ => {} _ => {}
} }
} }
@ -165,13 +164,13 @@ impl Engine {
} }
/// Apply a series of moves to the current node. /// Apply a series of moves to the current node.
fn apply_moves(&mut self, moves: &Vec<rules::Move>) { fn apply_moves(&mut self, moves: &Vec<Move>) {
moves.iter().for_each(|m| self.apply_move(m)); moves.iter().for_each(|m| self.apply_move(m));
} }
/// Apply a move to the current node. /// Apply a move to the current node.
fn apply_move(&mut self, m: &rules::Move) { fn apply_move(&mut self, m: &Move) {
rules::apply_move_to(&mut self.node.board, &mut self.node.game_state, m); movement::apply_move_to(&mut self.node.board, &mut self.node.game_state, m);
} }
/// Start working on board, returning the best move found. /// Start working on board, returning the best move found.
@ -179,14 +178,13 @@ impl Engine {
/// Stop working after `movetime` ms, or go on forever if it's -1. /// Stop working after `movetime` ms, or go on forever if it's -1.
fn work(&mut self, args: &analysis::AnalysisParams) { fn work(&mut self, args: &analysis::AnalysisParams) {
self.working.store(true, atomic::Ordering::Relaxed); self.working.store(true, atomic::Ordering::Relaxed);
let mut node = self.node.clone();
let args = args.clone(); let args = args.clone();
let score_map = self.score_map.clone();
let working = self.working.clone(); let working = self.working.clone();
let tx = match &self.mode { Mode::Uci(_, _, tx) => tx.clone(), _ => return }; let tx = match &self.mode { Mode::Uci(_, _, tx) => tx.clone(), _ => return };
let debug = self.debug; let mut worker = analysis::Analyzer::new(self.node.clone(), tx);
worker.debug = self.debug;
thread::spawn(move || { thread::spawn(move || {
analysis::analyze(&mut node, &args, &score_map, working, tx, debug); worker.analyze(&args, working);
}); });
} }

View file

@ -4,7 +4,10 @@ use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
pub mod analysis; pub mod analysis;
pub mod board; pub mod board;
pub mod castling;
pub mod engine; pub mod engine;
pub mod movement;
pub mod node;
pub mod notation; pub mod notation;
pub mod rules; pub mod rules;
pub mod stats; pub mod stats;

225
src/movement.rs Normal file
View file

@ -0,0 +1,225 @@
//! Move functions along with some castling helpers.
use crate::board::*;
use crate::castling::*;
use crate::rules;
const START_WH_K_POS: Pos = pos("e1");
const START_BL_K_POS: Pos = pos("e8");
/// A movement, with before/after positions and optional promotion.
pub type Move = (Pos, Pos, Option<u8>);
/// Apply a move `m` to copies to `board` and `game_state`.
///
/// Can be used for conveniance but it's better to write in existing
/// instances as often as possible using `apply_move_to`.
pub fn apply_move(
board: &Board,
game_state: &rules::GameState,
m: &Move
) -> (Board, rules::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)
}
/// Update `board` and `game_state` to reflect the move `m`.
///
/// The board is updated with correct piece placement.
///
/// The game state is updated with the new player turn and the new
/// castling options.
pub fn apply_move_to(
board: &mut Board,
game_state: &mut rules::GameState,
m: &Move
) {
apply_move_to_board(board, m);
apply_move_to_state(game_state, m);
// If the move is a castle, remove it from castling options.
if let Some(castle) = get_castle(m) {
match castle {
CASTLING_WH_K | CASTLING_WH_Q => game_state.castling &= !CASTLING_WH_MASK,
CASTLING_BL_K | CASTLING_BL_Q => game_state.castling &= !CASTLING_BL_MASK,
_ => {}
};
}
// Else, check if it's either a rook or the king that moved.
else {
let piece = get_square(board, &m.1);
if is_white(piece) && game_state.castling & CASTLING_WH_MASK != 0 {
match get_type(piece) {
SQ_K => {
if m.0 == pos("e1") {
game_state.castling &= !CASTLING_WH_MASK;
}
}
SQ_R => {
if m.0 == pos("a1") {
game_state.castling &= !CASTLING_WH_Q;
} else if m.0 == pos("h1") {
game_state.castling &= !CASTLING_WH_K;
}
}
_ => {}
}
} else if is_black(piece) && game_state.castling & CASTLING_BL_MASK != 0 {
match get_type(piece) {
SQ_K => {
if m.0 == pos("e8") {
game_state.castling &= !CASTLING_BL_MASK;
}
}
SQ_R => {
if m.0 == pos("a8") {
game_state.castling &= !CASTLING_BL_Q;
} else if m.0 == pos("h8") {
game_state.castling &= !CASTLING_BL_K;
}
}
_ => {}
}
}
}
}
/// Apply a move `m` into `board`.
pub fn apply_move_to_board(board: &mut Board, m: &Move) {
if let Some(castle) = get_castle(m) {
match castle {
CASTLING_WH_K => {
move_piece(board, &START_WH_K_POS, &pos("g1"));
move_piece(board, &pos("h1"), &pos("f1"));
}
CASTLING_WH_Q => {
move_piece(board, &START_WH_K_POS, &pos("c1"));
move_piece(board, &pos("a1"), &pos("d1"));
}
CASTLING_BL_K => {
move_piece(board, &START_BL_K_POS, &pos("g8"));
move_piece(board, &pos("h8"), &pos("f8"));
}
CASTLING_BL_Q => {
move_piece(board, &START_BL_K_POS, &pos("c8"));
move_piece(board, &pos("a8"), &pos("d8"));
}
_ => {}
}
} else {
move_piece(board, &m.0, &m.1);
if let Some(prom_type) = m.2 {
let color = get_color(get_square(board, &m.1));
set_square(board, &m.1, color|prom_type);
}
}
}
/// Update `game_state` with the move `m`.
///
/// This only updates the player turn. Castling should be updated in a
/// context where the corresponding board is available.
pub fn apply_move_to_state(game_state: &mut rules::GameState, _m: &Move) {
game_state.color = opposite(game_state.color);
}
/// Get the corresponding castling flag for this move.
pub fn get_castle(m: &Move) -> Option<u8> {
if m.0 == pos("e1") {
if m.1 == pos("c1") {
Some(CASTLING_WH_Q)
} else if m.1 == pos("g1") {
Some(CASTLING_WH_K)
} else {
None
}
} else if m.0 == pos("e8") {
if m.1 == pos("c8") {
Some(CASTLING_BL_Q)
} else if m.1 == pos("g8") {
Some(CASTLING_BL_K)
} else {
None
}
} else {
None
}
}
/// Get the move for this castle.
pub fn get_castle_move(castle: u8) -> Move {
match castle {
CASTLING_WH_Q => (pos("e1"), pos("c1"), None),
CASTLING_WH_K => (pos("e1"), pos("g1"), None),
CASTLING_BL_Q => (pos("e8"), pos("c8"), None),
CASTLING_BL_K => (pos("e8"), pos("g8"), None),
_ => panic!("Illegal castling requested: {:08b}", castle),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::notation::parse_move;
#[test]
fn test_apply_move_to_board() {
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_move_to_board(&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_move_to_board(&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_apply_move_to_castling() {
let mut b = new();
let mut gs = rules::GameState::new();
assert_eq!(gs.castling, CASTLING_MASK);
// On a starting board, start by making place for all castles.
clear_square(&mut b, &pos("b1"));
clear_square(&mut b, &pos("c1"));
clear_square(&mut b, &pos("d1"));
clear_square(&mut b, &pos("f1"));
clear_square(&mut b, &pos("g1"));
clear_square(&mut b, &pos("b8"));
clear_square(&mut b, &pos("c8"));
clear_square(&mut b, &pos("d8"));
clear_square(&mut b, &pos("f8"));
clear_square(&mut b, &pos("g8"));
// White queen-side castling.
apply_move_to(&mut b, &mut gs, &parse_move("e1c1"));
assert!(is_piece(get_square(&b, &pos("c1")), SQ_WH_K));
assert!(is_piece(get_square(&b, &pos("d1")), SQ_WH_R));
assert!(is_empty(&b, &pos("a1")));
assert!(is_empty(&b, &pos("e1")));
assert_eq!(gs.castling, CASTLING_BL_MASK);
// Black king-side castling.
apply_move_to(&mut b, &mut gs, &parse_move("e8g8"));
assert!(is_piece(get_square(&b, &pos("g8")), SQ_BL_K));
assert!(is_piece(get_square(&b, &pos("f8")), SQ_BL_R));
assert!(is_empty(&b, &pos("h8")));
assert!(is_empty(&b, &pos("e8")));
assert_eq!(gs.castling, 0);
}
#[test]
fn test_get_castle() {
assert_eq!(get_castle(&parse_move("e1c1")), Some(CASTLING_WH_Q));
assert_eq!(get_castle(&parse_move("e1g1")), Some(CASTLING_WH_K));
assert_eq!(get_castle(&parse_move("e8c8")), Some(CASTLING_BL_Q));
assert_eq!(get_castle(&parse_move("e8g8")), Some(CASTLING_BL_K));
assert_eq!(get_castle(&parse_move("d2d4")), None);
}
}

83
src/node.rs Normal file
View file

@ -0,0 +1,83 @@
use std::fmt;
use std::hash::{Hash, Hasher};
use crate::board;
use crate::movement::{self, Move};
use crate::rules;
use crate::stats;
/// Analysis node: a board along with the game state.
#[derive(Clone)]
pub struct Node {
/// Board for this node.
pub board: board::Board,
/// Game state.
pub game_state: rules::GameState,
}
impl Node {
/// Create a new node for an empty board and a new game state.
pub fn new() -> Node {
Node {
board: board::new_empty(),
game_state: rules::GameState::new(),
}
}
/// Apply a move to this node.
pub fn apply_move(&mut self, m: &Move) {
movement::apply_move_to(&mut self.board, &mut self.game_state, m);
}
/// Return player moves from this node.
pub fn get_player_moves(&self, commit: bool) -> Vec<Move> {
rules::get_player_moves(&self.board, &self.game_state, commit)
}
/// Compute stats for both players for this node.
pub fn compute_stats(&self) -> (stats::BoardStats, stats::BoardStats) {
stats::compute_stats(&self.board, &self.game_state)
}
}
impl fmt::Debug for Node {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Node {{ board: [...], game_state: {:?} }}",
self.game_state
)
}
}
impl fmt::Display for Node {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut s = vec!();
board::draw(&self.board, &mut s);
let board_drawing = String::from_utf8_lossy(&s).to_string();
write!(
f,
"* Board:\n{}\n\
* Game state:\n{}",
board_drawing, self.game_state
)
}
}
impl PartialEq for Node {
fn eq(&self, other: &Self) -> bool {
(
self.board.iter().zip(other.board.iter()).all(|(a, b)| a == b) &&
self.game_state == other.game_state
)
}
}
impl Eq for Node {}
impl Hash for Node {
fn hash<H: Hasher>(&self, state: &mut H) {
self.board.iter().for_each(|square| state.write_u8(*square));
self.game_state.hash(state);
}
}

View file

@ -1,45 +1,45 @@
//! Functions using various notations. //! Functions using various notations.
use crate::board; use crate::board::*;
use crate::rules; use crate::movement::Move;
pub const NULL_MOVE: &str = "0000"; pub const NULL_MOVE: &str = "0000";
/// Create a string containing the UCI algebraic notation of this move. /// Create a string containing the UCI algebraic notation of this move.
pub fn move_to_string(m: &rules::Move) -> String { pub fn move_to_string(m: &Move) -> String {
let mut move_string = String::new(); let mut move_string = String::new();
move_string.push_str(&board::pos_string(&m.0)); move_string.push_str(&pos_string(&m.0));
move_string.push_str(&board::pos_string(&m.1)); move_string.push_str(&pos_string(&m.1));
if let Some(prom) = m.2 { if let Some(prom) = m.2 {
move_string.push(match prom { move_string.push(match prom {
board::SQ_Q => 'q', SQ_Q => 'q',
board::SQ_B => 'b', SQ_B => 'b',
board::SQ_N => 'n', SQ_N => 'n',
board::SQ_R => 'r', SQ_R => 'r',
_ => panic!("What are you doing? Promote to a legal piece.") _ => panic!("What are you doing? Promote to a legal piece.")
}); });
} }
move_string move_string
} }
/// Parse an UCI move algebraic notation string to a rules::Move. /// Parse an UCI move algebraic notation string to a Move.
pub fn parse_move(m_str: &str) -> rules::Move { pub fn parse_move(m_str: &str) -> Move {
let prom = if m_str.len() == 5 { let prom = if m_str.len() == 5 {
Some(match m_str.as_bytes()[4] { Some(match m_str.as_bytes()[4] {
b'b' => board::SQ_B, b'b' => SQ_B,
b'n' => board::SQ_N, b'n' => SQ_N,
b'r' => board::SQ_R, b'r' => SQ_R,
b'q' => board::SQ_Q, b'q' => SQ_Q,
_ => panic!("What is the opponent doing? This is illegal, I'm out."), _ => panic!("What is the opponent doing? This is illegal, I'm out."),
}) })
} else { } else {
None None
}; };
(board::pos(&m_str[0..2]), board::pos(&m_str[2..4]), prom) (pos(&m_str[0..2]), pos(&m_str[2..4]), prom)
} }
/// Create a space-separated string of moves. Used for debugging. /// Create a space-separated string of moves. Used for debugging.
pub fn move_list_to_string(moves: &Vec<rules::Move>) -> String { pub fn move_list_to_string(moves: &Vec<Move>) -> String {
moves.iter().map(|m| move_to_string(m)).collect::<Vec<_>>().join(" ") moves.iter().map(|m| move_to_string(m)).collect::<Vec<_>>().join(" ")
} }
@ -75,8 +75,8 @@ pub fn parse_fen_fields(fields: &[&str]) -> Option<Fen> {
}) })
} }
pub fn en_passant_to_string(ep: Option<board::Pos>) -> String { pub fn en_passant_to_string(ep: Option<Pos>) -> String {
ep.and_then(|p| Some(board::pos_string(&p))).unwrap_or("-".to_string()) ep.and_then(|p| Some(pos_string(&p))).unwrap_or("-".to_string())
} }
#[cfg(test)] #[cfg(test)]
@ -87,15 +87,15 @@ mod tests {
fn test_move_to_string() { fn test_move_to_string() {
assert_eq!(move_to_string(&((0, 0), (3, 3), None)), "a1d4"); assert_eq!(move_to_string(&((0, 0), (3, 3), None)), "a1d4");
assert_eq!(move_to_string(&((7, 7), (0, 7), None)), "h8a8"); assert_eq!(move_to_string(&((7, 7), (0, 7), None)), "h8a8");
assert_eq!(move_to_string(&((7, 6), (7, 7), Some(board::SQ_Q))), "h7h8q"); assert_eq!(move_to_string(&((7, 6), (7, 7), Some(SQ_Q))), "h7h8q");
assert_eq!(move_to_string(&((7, 6), (7, 7), Some(board::SQ_N))), "h7h8n"); assert_eq!(move_to_string(&((7, 6), (7, 7), Some(SQ_N))), "h7h8n");
} }
#[test] #[test]
fn test_parse_move() { fn test_parse_move() {
assert_eq!(parse_move("a1d4"), ((0, 0), (3, 3), None)); assert_eq!(parse_move("a1d4"), ((0, 0), (3, 3), None));
assert_eq!(parse_move("a7a8q"), ((0, 6), (0, 7), Some(board::SQ_Q))); assert_eq!(parse_move("a7a8q"), ((0, 6), (0, 7), Some(SQ_Q)));
assert_eq!(parse_move("a7a8r"), ((0, 6), (0, 7), Some(board::SQ_R))); assert_eq!(parse_move("a7a8r"), ((0, 6), (0, 7), Some(SQ_R)));
} }
#[test] #[test]

View file

@ -1,6 +1,8 @@
//! Functions to determine legal moves. //! Functions to determine legal moves.
use crate::board::*; use crate::board::*;
use crate::castling::*;
use crate::movement::{self, Move};
use crate::notation; use crate::notation;
/// Characteristics of the state of a game. /// Characteristics of the state of a game.
@ -50,168 +52,6 @@ impl std::fmt::Display for GameState {
} }
} }
pub const CASTLING_WH_K: u8 = 0b00000001;
pub const CASTLING_WH_Q: u8 = 0b00000010;
pub const CASTLING_WH_MASK: u8 = 0b00000011;
pub const CASTLING_BL_K: u8 = 0b00000100;
pub const CASTLING_BL_Q: u8 = 0b00001000;
pub const CASTLING_BL_MASK: u8 = 0b00001100;
pub const CASTLING_K_MASK: u8 = 0b00000101;
pub const CASTLING_Q_MASK: u8 = 0b00001010;
pub const CASTLING_MASK: u8 = 0b00001111;
/// Castling sides parameters.
///
/// For both sides, the 3-uple contains files that should be empty
/// and not attacked, an optional file that should be empty for
/// queen-side, and the castling side-mask.
pub const CASTLING_SIDES: [([i8; 2], Option<i8>, u8); 2] =
[([5i8, 6i8], None, CASTLING_K_MASK), ([3i8, 2i8], Some(1i8), CASTLING_Q_MASK)];
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<u8>);
/// 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)
}
/// Update `board` and `game_state` to reflect the move `m`.
///
/// The board is updated with correct piece placement.
///
/// The game state is updated with the new player turn and the new
/// castling options.
pub fn apply_move_to(board: &mut Board, game_state: &mut GameState, m: &Move) {
apply_move_to_board(board, m);
apply_move_to_state(game_state, m);
// If the move is a castle, remove it from castling options.
if let Some(castle) = get_castle(m) {
match castle {
CASTLING_WH_K | CASTLING_WH_Q => game_state.castling &= !CASTLING_WH_MASK,
CASTLING_BL_K | CASTLING_BL_Q => game_state.castling &= !CASTLING_BL_MASK,
_ => {}
};
}
// Else, check if it's either a rook or the king that moved.
else {
let piece = get_square(board, &m.1);
if is_white(piece) && game_state.castling & CASTLING_WH_MASK != 0 {
match get_type(piece) {
SQ_K => {
if m.0 == pos("e1") {
game_state.castling &= !CASTLING_WH_MASK;
}
}
SQ_R => {
if m.0 == pos("a1") {
game_state.castling &= !CASTLING_WH_Q;
} else if m.0 == pos("h1") {
game_state.castling &= !CASTLING_WH_K;
}
}
_ => {}
}
} else if is_black(piece) && game_state.castling & CASTLING_BL_MASK != 0 {
match get_type(piece) {
SQ_K => {
if m.0 == pos("e8") {
game_state.castling &= !CASTLING_BL_MASK;
}
}
SQ_R => {
if m.0 == pos("a8") {
game_state.castling &= !CASTLING_BL_Q;
} else if m.0 == pos("h8") {
game_state.castling &= !CASTLING_BL_K;
}
}
_ => {}
}
}
}
}
/// Apply a move `m` into `board`.
pub fn apply_move_to_board(board: &mut Board, m: &Move) {
if let Some(castle) = get_castle(m) {
match castle {
CASTLING_WH_K => {
move_piece(board, &START_WH_K_POS, &pos("g1"));
move_piece(board, &pos("h1"), &pos("f1"));
}
CASTLING_WH_Q => {
move_piece(board, &START_WH_K_POS, &pos("c1"));
move_piece(board, &pos("a1"), &pos("d1"));
}
CASTLING_BL_K => {
move_piece(board, &START_BL_K_POS, &pos("g8"));
move_piece(board, &pos("h8"), &pos("f8"));
}
CASTLING_BL_Q => {
move_piece(board, &START_BL_K_POS, &pos("c8"));
move_piece(board, &pos("a8"), &pos("d8"));
}
_ => {}
}
} else {
move_piece(board, &m.0, &m.1);
if let Some(prom_type) = m.2 {
let color = get_color(get_square(board, &m.1));
set_square(board, &m.1, color|prom_type);
}
}
}
/// Update `game_state` with the move `m`.
///
/// This only updates the player turn. Castling should be updated in a
/// context where the corresponding board is available.
pub fn apply_move_to_state(game_state: &mut GameState, _m: &Move) {
game_state.color = opposite(game_state.color);
}
/// Get the corresponding castling flag for this move.
pub fn get_castle(m: &Move) -> Option<u8> {
if m.0 == pos("e1") {
if m.1 == pos("c1") {
Some(CASTLING_WH_Q)
} else if m.1 == pos("g1") {
Some(CASTLING_WH_K)
} else {
None
}
} else if m.0 == pos("e8") {
if m.1 == pos("c8") {
Some(CASTLING_BL_Q)
} else if m.1 == pos("g8") {
Some(CASTLING_BL_K)
} else {
None
}
} else {
None
}
}
/// Get the move for this castle.
pub fn get_castle_move(castle: u8) -> Move {
match castle {
CASTLING_WH_Q => (pos("e1"), pos("c1"), None),
CASTLING_WH_K => (pos("e1"), pos("g1"), None),
CASTLING_BL_Q => (pos("e8"), pos("c8"), None),
CASTLING_BL_K => (pos("e8"), pos("g8"), None),
_ => panic!("Illegal castling requested: {:08b}", castle),
}
}
/// Get a list of moves for all pieces of the playing color. /// Get a list of moves for all pieces of the playing color.
/// ///
/// If `commit` is false, do not check for illegal moves. This is used /// If `commit` is false, do not check for illegal moves. This is used
@ -219,7 +59,11 @@ pub fn get_castle_move(castle: u8) -> Move {
/// as it needs to check all possible following enemy moves, e.g. to /// as it needs to check all possible following enemy moves, e.g. to
/// see if P's king can be taken. Consider a call with true `commit` as /// see if P's king can be taken. Consider a call with true `commit` as
/// a collection of attacked squares instead of legal move collection. /// a collection of attacked squares instead of legal move collection.
pub fn get_player_moves(board: &Board, game_state: &GameState, commit: bool) -> Vec<Move> { pub fn get_player_moves(
board: &Board,
game_state: &GameState,
commit: bool,
) -> Vec<Move> {
let mut moves = Vec::with_capacity(256); let mut moves = Vec::with_capacity(256);
for r in 0..8 { for r in 0..8 {
for f in 0..8 { for f in 0..8 {
@ -236,7 +80,12 @@ pub fn get_player_moves(board: &Board, game_state: &GameState, commit: bool) ->
} }
/// Get a list of moves for the piece at position `at`. /// Get a list of moves for the piece at position `at`.
pub fn get_piece_moves(board: &Board, at: &Pos, game_state: &GameState, commit: bool) -> Vec<Move> { pub fn get_piece_moves(
board: &Board,
at: &Pos,
game_state: &GameState,
commit: bool,
) -> Vec<Move> {
match get_square(board, at) { match get_square(board, at) {
p if is_piece(p, SQ_P) => get_pawn_moves(board, at, p, game_state, commit), p if is_piece(p, SQ_P) => get_pawn_moves(board, at, p, game_state, commit),
p if is_piece(p, SQ_B) => get_bishop_moves(board, at, p, game_state, commit), p if is_piece(p, SQ_B) => get_bishop_moves(board, at, p, game_state, commit),
@ -505,7 +354,7 @@ fn get_king_moves(
} }
} }
let castle = castling_side_mask & castling_color_mask; let castle = castling_side_mask & castling_color_mask;
let m = get_castle_move(castle); let m = movement::get_castle_move(castle);
if can_register(commit, board, game_state, &m) { if can_register(commit, board, game_state, &m) {
moves.push(m); moves.push(m);
} }
@ -552,7 +401,7 @@ fn is_illegal(board: &Board, game_state: &GameState, m: &Move) -> bool {
// If king moves, use its new position. // If king moves, use its new position.
let king_p = if m.0 == king_p { m.1 } else { king_p }; let king_p = if m.0 == king_p { m.1 } else { king_p };
let mut hypothetic_board = board.clone(); let mut hypothetic_board = board.clone();
apply_move_to_board(&mut hypothetic_board, m); movement::apply_move_to_board(&mut hypothetic_board, m);
// Check if the move makes the player king in check. // Check if the move makes the player king in check.
if is_attacked(&hypothetic_board, &game_state, &king_p) { if is_attacked(&hypothetic_board, &game_state, &king_p) {
return true return true
@ -588,66 +437,6 @@ mod tests {
use super::*; use super::*;
use crate::notation::parse_move; use crate::notation::parse_move;
#[test]
fn test_get_castle() {
assert_eq!(get_castle(&parse_move("e1c1")), Some(CASTLING_WH_Q));
assert_eq!(get_castle(&parse_move("e1g1")), Some(CASTLING_WH_K));
assert_eq!(get_castle(&parse_move("e8c8")), Some(CASTLING_BL_Q));
assert_eq!(get_castle(&parse_move("e8g8")), Some(CASTLING_BL_K));
assert_eq!(get_castle(&parse_move("d2d4")), None);
}
#[test]
fn test_apply_move_to_board() {
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_move_to_board(&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_move_to_board(&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_apply_move_to_castling() {
let mut b = new();
let mut gs = GameState::new();
assert_eq!(gs.castling, CASTLING_MASK);
// On a starting board, start by making place for all castles.
clear_square(&mut b, &pos("b1"));
clear_square(&mut b, &pos("c1"));
clear_square(&mut b, &pos("d1"));
clear_square(&mut b, &pos("f1"));
clear_square(&mut b, &pos("g1"));
clear_square(&mut b, &pos("b8"));
clear_square(&mut b, &pos("c8"));
clear_square(&mut b, &pos("d8"));
clear_square(&mut b, &pos("f8"));
clear_square(&mut b, &pos("g8"));
// White queen-side castling.
apply_move_to(&mut b, &mut gs, &parse_move("e1c1"));
assert!(is_piece(get_square(&b, &pos("c1")), SQ_WH_K));
assert!(is_piece(get_square(&b, &pos("d1")), SQ_WH_R));
assert!(is_empty(&b, &pos("a1")));
assert!(is_empty(&b, &pos("e1")));
assert_eq!(gs.castling, CASTLING_BL_MASK);
// Black king-side castling.
apply_move_to(&mut b, &mut gs, &parse_move("e8g8"));
assert!(is_piece(get_square(&b, &pos("g8")), SQ_BL_K));
assert!(is_piece(get_square(&b, &pos("f8")), SQ_BL_R));
assert!(is_empty(&b, &pos("h8")));
assert!(is_empty(&b, &pos("e8")));
assert_eq!(gs.castling, 0);
}
#[test] #[test]
fn test_get_player_moves() { fn test_get_player_moves() {
let b = new(); let b = new();
@ -840,7 +629,7 @@ mod tests {
set_square(&mut b, &pos("d6"), SQ_BL_R); set_square(&mut b, &pos("d6"), SQ_BL_R);
assert!(is_attacked(&b, &gs, &pos("d4"))); assert!(is_attacked(&b, &gs, &pos("d4")));
// Move the rook on another file, no more attack. // Move the rook on another file, no more attack.
apply_move_to_board(&mut b, &parse_move("d6e6")); movement::apply_move_to_board(&mut b, &parse_move("d6e6"));
assert!(!is_attacked(&b, &gs, &pos("d4"))); assert!(!is_attacked(&b, &gs, &pos("d4")));
} }
} }

View file

@ -54,7 +54,7 @@ impl std::fmt::Display for BoardStats {
} }
} }
/// Create two new BoardStats objects from the board, for white and black. /// Create two new BoardStats objects from the board, for both sides.
/// ///
/// See `compute_stats_into` for details. /// See `compute_stats_into` for details.
pub fn compute_stats(board: &Board, game_state: &rules::GameState) -> (BoardStats, BoardStats) { pub fn compute_stats(board: &Board, game_state: &rules::GameState) -> (BoardStats, BoardStats) {
@ -63,25 +63,32 @@ pub fn compute_stats(board: &Board, game_state: &rules::GameState) -> (BoardStat
stats stats
} }
/// Compute stats for both the current player and its opponent.
///
/// The playing color will have its stats filled in the first
/// BoardStats object, its opponent in the second.
pub fn compute_stats_into( pub fn compute_stats_into(
board: &Board, board: &Board,
game_state: &rules::GameState, game_state: &rules::GameState,
stats: &mut (BoardStats, BoardStats) stats: &mut (BoardStats, BoardStats)
) { ) {
compute_color_stats_into(board, game_state, &mut stats.0, SQ_WH); let mut gs = game_state.clone();
compute_color_stats_into(board, game_state, &mut stats.1, SQ_BL); compute_color_stats_into(board, &gs, &mut stats.0);
gs.color = opposite(gs.color);
compute_color_stats_into(board, &gs, &mut stats.1);
} }
/// Update `stats` for `color` from given `board` /// Fill `stats` from given `board` and `game_state`.
/// ///
/// Refresh all stats *except* `mobility`. /// Only the current playing side stats are created,
/// prepare the game_state accordingly.
pub fn compute_color_stats_into( pub fn compute_color_stats_into(
board: &Board, board: &Board,
game_state: &rules::GameState, game_state: &rules::GameState,
stats: &mut BoardStats, stats: &mut BoardStats,
color: u8
) { ) {
stats.reset(); stats.reset();
let color = game_state.color;
// Compute mobility for all pieces. // Compute mobility for all pieces.
stats.mobility = rules::get_player_moves(board, game_state, true).len() as i32; stats.mobility = rules::get_player_moves(board, game_state, true).len() as i32;
// Compute amount of each piece. // Compute amount of each piece.
@ -198,15 +205,15 @@ mod tests {
let mut b = new_empty(); let mut b = new_empty();
set_square(&mut b, &pos("d4"), SQ_WH_P); set_square(&mut b, &pos("d4"), SQ_WH_P);
set_square(&mut b, &pos("d6"), SQ_WH_P); set_square(&mut b, &pos("d6"), SQ_WH_P);
compute_color_stats_into(&b, &gs, &mut stats.0, SQ_WH); compute_color_stats_into(&b, &gs, &mut stats.0);
assert_eq!(stats.0.num_doubled_pawns, 2); assert_eq!(stats.0.num_doubled_pawns, 2);
// Add a pawn on another file, no changes expected. // Add a pawn on another file, no changes expected.
set_square(&mut b, &pos("e6"), SQ_WH_P); set_square(&mut b, &pos("e6"), SQ_WH_P);
compute_color_stats_into(&b, &gs, &mut stats.0, SQ_WH); compute_color_stats_into(&b, &gs, &mut stats.0);
assert_eq!(stats.0.num_doubled_pawns, 2); assert_eq!(stats.0.num_doubled_pawns, 2);
// Add a pawn backward in the d-file: there are now 3 doubled pawns. // Add a pawn backward in the d-file: there are now 3 doubled pawns.
set_square(&mut b, &pos("d2"), SQ_WH_P); set_square(&mut b, &pos("d2"), SQ_WH_P);
compute_color_stats_into(&b, &gs, &mut stats.0, SQ_WH); compute_color_stats_into(&b, &gs, &mut stats.0);
assert_eq!(stats.0.num_doubled_pawns, 3); assert_eq!(stats.0.num_doubled_pawns, 3);
// Check that isolated and backward pawns are correctly counted. // Check that isolated and backward pawns are correctly counted.
@ -214,19 +221,19 @@ mod tests {
assert_eq!(stats.0.num_backward_pawns, 2); // A bit weird? 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. // Protect d4 pawn with a friend in e3: it is not isolated nor backward anymore.
set_square(&mut b, &pos("e3"), SQ_WH_P); set_square(&mut b, &pos("e3"), SQ_WH_P);
compute_color_stats_into(&b, &gs, &mut stats.0, SQ_WH); compute_color_stats_into(&b, &gs, &mut stats.0);
assert_eq!(stats.0.num_doubled_pawns, 5); assert_eq!(stats.0.num_doubled_pawns, 5);
assert_eq!(stats.0.num_isolated_pawns, 0); assert_eq!(stats.0.num_isolated_pawns, 0);
assert_eq!(stats.0.num_backward_pawns, 1); assert_eq!(stats.0.num_backward_pawns, 1);
// Add an adjacent friend to d2 pawn: no pawns are left isolated or backward. // Add an adjacent friend to d2 pawn: no pawns are left isolated or backward.
set_square(&mut b, &pos("c2"), SQ_WH_P); set_square(&mut b, &pos("c2"), SQ_WH_P);
compute_color_stats_into(&b, &gs, &mut stats.0, SQ_WH); compute_color_stats_into(&b, &gs, &mut stats.0);
assert_eq!(stats.0.num_doubled_pawns, 5); assert_eq!(stats.0.num_doubled_pawns, 5);
assert_eq!(stats.0.num_isolated_pawns, 0); assert_eq!(stats.0.num_isolated_pawns, 0);
assert_eq!(stats.0.num_backward_pawns, 0); assert_eq!(stats.0.num_backward_pawns, 0);
// Add an isolated/backward white pawn in a far file. // Add an isolated/backward white pawn in a far file.
set_square(&mut b, &pos("a2"), SQ_WH_P); set_square(&mut b, &pos("a2"), SQ_WH_P);
compute_color_stats_into(&b, &gs, &mut stats.0, SQ_WH); compute_color_stats_into(&b, &gs, &mut stats.0);
assert_eq!(stats.0.num_doubled_pawns, 5); assert_eq!(stats.0.num_doubled_pawns, 5);
assert_eq!(stats.0.num_isolated_pawns, 1); assert_eq!(stats.0.num_isolated_pawns, 1);
assert_eq!(stats.0.num_backward_pawns, 1); assert_eq!(stats.0.num_backward_pawns, 1);
@ -237,7 +244,7 @@ mod tests {
set_square(&mut b, &pos("d4"), SQ_WH_P); set_square(&mut b, &pos("d4"), SQ_WH_P);
set_square(&mut b, &pos("e5"), SQ_WH_P); set_square(&mut b, &pos("e5"), SQ_WH_P);
set_square(&mut b, &pos("e3"), SQ_WH_P); set_square(&mut b, &pos("e3"), SQ_WH_P);
compute_color_stats_into(&b, &gs, &mut stats.0, SQ_WH); compute_color_stats_into(&b, &gs, &mut stats.0);
assert_eq!(stats.0.num_doubled_pawns, 2); assert_eq!(stats.0.num_doubled_pawns, 2);
assert_eq!(stats.0.num_isolated_pawns, 0); assert_eq!(stats.0.num_isolated_pawns, 0);
assert_eq!(stats.0.num_backward_pawns, 1); assert_eq!(stats.0.num_backward_pawns, 1);

View file

@ -6,8 +6,8 @@ use std::sync::mpsc;
use std::thread; use std::thread;
use crate::engine; use crate::engine;
use crate::movement::Move;
use crate::notation; use crate::notation;
use crate::rules;
const VATU_NAME: &str = env!("CARGO_PKG_NAME"); const VATU_NAME: &str = env!("CARGO_PKG_NAME");
const VATU_AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); const VATU_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
@ -58,13 +58,13 @@ pub enum UciCmd {
pub enum PositionArgs { pub enum PositionArgs {
Startpos, Startpos,
Fen(notation::Fen), Fen(notation::Fen),
Moves(Vec<rules::Move>), Moves(Vec<Move>),
} }
/// Arguments for the go remote commands. /// Arguments for the go remote commands.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum GoArgs { pub enum GoArgs {
SearchMoves(Vec<rules::Move>), SearchMoves(Vec<Move>),
Ponder, Ponder,
WTime(i32), WTime(i32),
BTime(i32), BTime(i32),
@ -199,11 +199,11 @@ impl Uci {
fn handle_engine_command(&mut self, cmd: &engine::Cmd) { fn handle_engine_command(&mut self, cmd: &engine::Cmd) {
match cmd { match cmd {
engine::Cmd::UciChannel(s) => { engine::Cmd::UciChannel(s) => {
self.log("ENG >>> Channel opened.".to_string()); self.log("ENGINE: Channel opened.".to_string());
self.engine_in = Some(s.to_owned()); self.engine_in = Some(s.to_owned());
} }
engine::Cmd::Log(s) => { engine::Cmd::Log(s) => {
self.log(s.to_string()); self.log(format!("ENGINE: {}", s.to_string()));
} }
engine::Cmd::Info(infos) => { engine::Cmd::Info(infos) => {
self.send_infos(infos); self.send_infos(infos);
@ -261,7 +261,7 @@ impl Uci {
} }
/// Send best move. /// Send best move.
fn send_bestmove(&mut self, m: &Option<rules::Move>) { fn send_bestmove(&mut self, m: &Option<Move>) {
let move_str = match m { let move_str = match m {
Some(m) => notation::move_to_string(m), Some(m) => notation::move_to_string(m),
None => notation::NULL_MOVE.to_string(), None => notation::NULL_MOVE.to_string(),