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,79 +1,25 @@
//! Analysis functions.
use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, atomic, mpsc};
use dashmap::DashMap;
use crate::board;
use crate::engine;
use crate::movement::Move;
use crate::node::Node;
use crate::notation;
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,
}
const MIN_F32: f32 = std::f32::NEG_INFINITY;
const MAX_F32: f32 = std::f32::INFINITY;
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(),
/// Analysis worker.
pub struct Analyzer {
pub debug: bool,
node: Node,
engine_tx: mpsc::Sender<engine::Cmd>,
max_depth: u32,
}
}
}
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.
#[derive(Clone)]
@ -85,113 +31,80 @@ pub struct AnalysisParams {
pub black_inc: i32,
}
const MIN_F32: f32 = std::f32::NEG_INFINITY;
const MAX_F32: f32 = std::f32::INFINITY;
impl Analyzer {
/// Create a new worker to analyze from `node`.
pub fn new(node: Node, engine_tx: mpsc::Sender<engine::Cmd>) -> Analyzer {
Analyzer { debug: false, node, engine_tx, max_depth: 1 }
}
/// Analyse best moves for a given node.
fn log(&self, message: String) {
self.engine_tx.send(engine::Cmd::Log(message)).unwrap();
}
/// Analyse best moves for the node.
///
/// - `args`: parameters provided for this analysis.
/// - `score_map`: a NodeEvalMap to read and update.
/// - `working`: flag telling whether to keep working or to stop.
pub fn analyze(
node: &mut Node,
&mut self,
_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();
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)));
}
let (max_score, best_move) = minimax(
node,
0,
3,
board::is_white(node.game_state.color),
&score_map,
);
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);
if best_move.is_some() {
let log_str = format!(
"\tBest move {} evaluated {}",
"Best 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();
self.log(log_str);
self.engine_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);
self.log("Checkmate is unavoidable.".to_string());
let moves = rules::get_player_moves(&self.node.board, &self.node.game_state, true);
let m = if moves.len() > 0 { Some(moves[0]) } else { None };
tx.send(engine::Cmd::TmpBestMove(m)).unwrap();
self.engine_tx.send(engine::Cmd::TmpBestMove(m)).unwrap();
}
}
// thread::sleep(time::Duration::from_secs(1));
// for _ in 0..4 {
// let board = board.clone();
// let wip = wip.clone();
// thread::spawn(move || {
// analyze(&board, wip);
// });
// }
}
/// Provide a "minimax" score for this node.
///
/// This method recursively looks alternatively for minimum score for
/// one player, then maximum for its opponent; that way it assumes the
/// opponent always does their best.
///
/// `depth` is increased at each recursive call; when `max_depth` is
/// 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,
fn negamax(
&self,
node: &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)
color_f: f32,
) -> (f32, Option<Move>) {
if depth == self.max_depth {
let stats = node.compute_stats();
return (color_f * evaluate(&stats), 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);
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();
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 {
let (score, _) = minimax(&mut sub_node, depth + 1, max_depth, true, score_map);
if score <= minmax {
minmax = score;
minmax_move = Some(m);
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)
}
(minmax, minmax_move)
}
/// 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
/// yet provide good enough results.
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
+ 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
200.0 * (player_stats.num_kings - opponent_stats.num_kings) as f32
+ 9.0 * (player_stats.num_queens - opponent_stats.num_queens) as f32
+ 5.0 * (player_stats.num_rooks - opponent_stats.num_rooks) as f32
+ 3.0 * (player_stats.num_bishops - opponent_stats.num_bishops) as f32
+ 3.0 * (player_stats.num_knights - opponent_stats.num_knights) as f32
+ (player_stats.num_pawns - opponent_stats.num_pawns) as f32
- 0.5 * (
ws.num_doubled_pawns - bs.num_doubled_pawns +
ws.num_isolated_pawns - bs.num_isolated_pawns +
ws.num_backward_pawns - bs.num_backward_pawns
player_stats.num_doubled_pawns - opponent_stats.num_doubled_pawns +
player_stats.num_isolated_pawns - opponent_stats.num_isolated_pawns +
player_stats.num_backward_pawns - opponent_stats.num_backward_pawns
) as f32
+ 0.1 * (ws.mobility - bs.mobility) as f32
+ 0.1 * (player_stats.mobility - opponent_stats.mobility) as f32
}
#[cfg(test)]
mod tests {
use super::*;
use board::pos;
// use super::*;
#[test]
fn test_minimax() {
let mut node = Node::new();
node.game_state.castling = 0;
// FIXME
// let mut node = Node::new();
// node.game_state.castling = 0;
// 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("c6"), board::SQ_WH_P);
board::set_square(&mut node.board, &pos("h7"), board::SQ_WH_Q);
board::set_square(&mut node.board, &pos("d8"), board::SQ_BL_K);
let (_, m) = minimax(&mut node, 0, 2, true);
assert_eq!(m.unwrap(), notation::parse_move("h7d7"));
// // 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("c6"), board::SQ_WH_P);
// board::set_square(&mut node.board, &pos("h7"), board::SQ_WH_Q);
// board::set_square(&mut node.board, &pos("d8"), board::SQ_BL_K);
// let (_, m) = minimax(&mut node, 0, 2, true);
// assert_eq!(m.unwrap(), notation::parse_move("h7d7"));
// 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("c6"), board::SQ_BL_P);
board::set_square(&mut node.board, &pos("h7"), board::SQ_BL_Q);
board::set_square(&mut node.board, &pos("d8"), board::SQ_WH_K);
node.game_state.color = board::SQ_BL;
let (_, m) = minimax(&mut node, 0, 2, true);
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);
// // 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("c6"), board::SQ_BL_P);
// board::set_square(&mut node.board, &pos("h7"), board::SQ_BL_Q);
// board::set_square(&mut node.board, &pos("d8"), board::SQ_WH_K);
// node.game_state.color = board::SQ_BL;
// let (_, m) = minimax(&mut node, 0, 2, true);
// assert_eq!(m.unwrap(), notation::parse_move("h7d7"));
}
}

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,
//! 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 dashmap::DashMap;
use crate::analysis;
use crate::board;
use crate::castling;
use crate::movement::{self, Move};
use crate::node::Node;
use crate::notation;
use crate::rules;
use crate::uci;
/// Analysis engine.
@ -19,15 +21,13 @@ pub struct Engine {
/// Debug mode, log some data.
debug: bool,
/// Current game state, starting point of further analysis.
node: analysis::Node,
/// Store already evaluated nodes with their score.
score_map: Arc<DashMap<analysis::Node, f32>>,
node: Node,
/// Communication mode.
mode: Mode,
/// If true, the engine is currently listening to incoming cmds.
listening: bool,
/// Shared flag to notify workers if they should keep working.
working: Arc<atomic::AtomicBool>,
/// flag to notify workers if they should keep working.
working: Arc<AtomicBool>,
}
/// Engine communication mode.
@ -52,7 +52,7 @@ pub enum Cmd {
UciPosition(Vec<uci::PositionArgs>), // UCI "position" command.
UciGo(Vec<uci::GoArgs>), // UCI "go" command.
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.
// Commands that can be sent by the engine.
@ -62,7 +62,7 @@ pub enum Cmd {
/// the message to be forwarded to whatever can log.
Log(String),
/// Report found best move.
BestMove(Option<rules::Move>),
BestMove(Option<Move>),
/// Report ongoing analysis information.
Info(Vec<Info>),
}
@ -70,7 +70,7 @@ pub enum Cmd {
/// Information to be transmitted back to whatever is listening.
#[derive(Debug, Clone)]
pub enum Info {
CurrentMove(rules::Move),
CurrentMove(Move),
}
/// General engine implementation.
@ -78,11 +78,10 @@ impl Engine {
pub fn new() -> Engine {
Engine {
debug: false,
node: analysis::Node::new(),
score_map: Arc::new(DashMap::with_capacity(2usize.pow(10))),
node: Node::new(),
mode: Mode::No,
listening: false,
working: Arc::new(atomic::AtomicBool::new(false)),
working: Arc::new(AtomicBool::new(false)),
}
}
@ -146,10 +145,10 @@ impl Engine {
// Castling.
for c in fen.castling.chars() {
match c {
'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,
'K' => self.node.game_state.castling |= castling::CASTLING_WH_K,
'Q' => self.node.game_state.castling |= castling::CASTLING_WH_Q,
'k' => self.node.game_state.castling |= castling::CASTLING_BL_K,
'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.
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));
}
/// Apply a move to the current node.
fn apply_move(&mut self, m: &rules::Move) {
rules::apply_move_to(&mut self.node.board, &mut self.node.game_state, m);
fn apply_move(&mut self, m: &Move) {
movement::apply_move_to(&mut self.node.board, &mut self.node.game_state, m);
}
/// 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.
fn work(&mut self, args: &analysis::AnalysisParams) {
self.working.store(true, atomic::Ordering::Relaxed);
let mut node = self.node.clone();
let args = args.clone();
let score_map = self.score_map.clone();
let working = self.working.clone();
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 || {
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 board;
pub mod castling;
pub mod engine;
pub mod movement;
pub mod node;
pub mod notation;
pub mod rules;
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.
use crate::board;
use crate::rules;
use crate::board::*;
use crate::movement::Move;
pub const NULL_MOVE: &str = "0000";
/// 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();
move_string.push_str(&board::pos_string(&m.0));
move_string.push_str(&board::pos_string(&m.1));
move_string.push_str(&pos_string(&m.0));
move_string.push_str(&pos_string(&m.1));
if let Some(prom) = m.2 {
move_string.push(match prom {
board::SQ_Q => 'q',
board::SQ_B => 'b',
board::SQ_N => 'n',
board::SQ_R => 'r',
SQ_Q => 'q',
SQ_B => 'b',
SQ_N => 'n',
SQ_R => 'r',
_ => panic!("What are you doing? Promote to a legal piece.")
});
}
move_string
}
/// Parse an UCI move algebraic notation string to a rules::Move.
pub fn parse_move(m_str: &str) -> rules::Move {
/// Parse an UCI move algebraic notation string to a Move.
pub fn parse_move(m_str: &str) -> Move {
let prom = if m_str.len() == 5 {
Some(match m_str.as_bytes()[4] {
b'b' => board::SQ_B,
b'n' => board::SQ_N,
b'r' => board::SQ_R,
b'q' => board::SQ_Q,
b'b' => SQ_B,
b'n' => SQ_N,
b'r' => SQ_R,
b'q' => SQ_Q,
_ => panic!("What is the opponent doing? This is illegal, I'm out."),
})
} else {
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.
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(" ")
}
@ -75,8 +75,8 @@ pub fn parse_fen_fields(fields: &[&str]) -> Option<Fen> {
})
}
pub fn en_passant_to_string(ep: Option<board::Pos>) -> String {
ep.and_then(|p| Some(board::pos_string(&p))).unwrap_or("-".to_string())
pub fn en_passant_to_string(ep: Option<Pos>) -> String {
ep.and_then(|p| Some(pos_string(&p))).unwrap_or("-".to_string())
}
#[cfg(test)]
@ -87,15 +87,15 @@ mod tests {
fn test_move_to_string() {
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, 6), (7, 7), Some(board::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_Q))), "h7h8q");
assert_eq!(move_to_string(&((7, 6), (7, 7), Some(SQ_N))), "h7h8n");
}
#[test]
fn test_parse_move() {
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("a7a8r"), ((0, 6), (0, 7), Some(board::SQ_R)));
assert_eq!(parse_move("a7a8q"), ((0, 6), (0, 7), Some(SQ_Q)));
assert_eq!(parse_move("a7a8r"), ((0, 6), (0, 7), Some(SQ_R)));
}
#[test]

View file

@ -1,6 +1,8 @@
//! Functions to determine legal moves.
use crate::board::*;
use crate::castling::*;
use crate::movement::{self, Move};
use crate::notation;
/// 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.
///
/// 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
/// 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.
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);
for r 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`.
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) {
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),
@ -505,7 +354,7 @@ fn get_king_moves(
}
}
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) {
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.
let king_p = if m.0 == king_p { m.1 } else { king_p };
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.
if is_attacked(&hypothetic_board, &game_state, &king_p) {
return true
@ -588,66 +437,6 @@ mod tests {
use super::*;
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]
fn test_get_player_moves() {
let b = new();
@ -840,7 +629,7 @@ mod tests {
set_square(&mut b, &pos("d6"), SQ_BL_R);
assert!(is_attacked(&b, &gs, &pos("d4")));
// 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")));
}
}

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.
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
}
/// 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(
board: &Board,
game_state: &rules::GameState,
stats: &mut (BoardStats, BoardStats)
) {
compute_color_stats_into(board, game_state, &mut stats.0, SQ_WH);
compute_color_stats_into(board, game_state, &mut stats.1, SQ_BL);
let mut gs = game_state.clone();
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(
board: &Board,
game_state: &rules::GameState,
stats: &mut BoardStats,
color: u8
) {
stats.reset();
let color = game_state.color;
// Compute mobility for all pieces.
stats.mobility = rules::get_player_moves(board, game_state, true).len() as i32;
// Compute amount of each piece.
@ -198,15 +205,15 @@ mod tests {
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, &gs, &mut stats.0, SQ_WH);
compute_color_stats_into(&b, &gs, &mut stats.0);
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, &gs, &mut stats.0, SQ_WH);
compute_color_stats_into(&b, &gs, &mut stats.0);
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, &gs, &mut stats.0, SQ_WH);
compute_color_stats_into(&b, &gs, &mut stats.0);
assert_eq!(stats.0.num_doubled_pawns, 3);
// 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?
// 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, &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_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, &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_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, &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_isolated_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("e5"), 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_isolated_pawns, 0);
assert_eq!(stats.0.num_backward_pawns, 1);

View file

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