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:
parent
e1b1642d37
commit
938fcde9fa
272
src/analysis.rs
272
src/analysis.rs
|
@ -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, ¬ation::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
19
src/castling.rs
Normal 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)];
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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
225
src/movement.rs
Normal 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
83
src/node.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
|
|
243
src/rules.rs
243
src/rules.rs
|
@ -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")));
|
||||
}
|
||||
}
|
||||
|
|
33
src/stats.rs
33
src/stats.rs
|
@ -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);
|
||||
|
|
12
src/uci.rs
12
src/uci.rs
|
@ -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(),
|
||||
|
|
Reference in a new issue