analysis: move analysis code to its own module
This commit is contained in:
parent
ea2e7ead91
commit
39c3bc8786
237
src/analysis.rs
Normal file
237
src/analysis.rs
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
//! Analysis functions.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::sync::{Arc, atomic, mpsc};
|
||||||
|
|
||||||
|
use crate::board;
|
||||||
|
use crate::engine;
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Node {
|
||||||
|
/// Create a new node for an empty board and a new game state.
|
||||||
|
pub fn new() -> Node {
|
||||||
|
Node {
|
||||||
|
board: board::new_empty(),
|
||||||
|
game_state: rules::GameState::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Node {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Node {{ board: [...], game_state: {:?} }}",
|
||||||
|
self.game_state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Node {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let mut s = vec!();
|
||||||
|
board::draw(&self.board, &mut s);
|
||||||
|
let board_drawing = String::from_utf8_lossy(&s).to_string();
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"* Board:\n{}\n\
|
||||||
|
* Game state:\n{}",
|
||||||
|
board_drawing, self.game_state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Node {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
(
|
||||||
|
self.board.iter().zip(other.board.iter()).all(|(a, b)| a == b) &&
|
||||||
|
self.game_state == other.game_state
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Node {}
|
||||||
|
|
||||||
|
impl Hash for Node {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.board.iter().for_each(|square| state.write_u8(*square));
|
||||||
|
self.game_state.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Analysis parameters.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AnalysisParams {
|
||||||
|
pub move_time: i32,
|
||||||
|
pub white_time: i32,
|
||||||
|
pub black_time: i32,
|
||||||
|
pub white_inc: i32,
|
||||||
|
pub black_inc: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIN_F32: f32 = std::f32::NEG_INFINITY;
|
||||||
|
const MAX_F32: f32 = std::f32::INFINITY;
|
||||||
|
|
||||||
|
/// Analyse best moves for a given node.
|
||||||
|
pub fn analyze(
|
||||||
|
node: &mut Node,
|
||||||
|
_args: &AnalysisParams,
|
||||||
|
working: Arc<atomic::AtomicBool>,
|
||||||
|
tx: mpsc::Sender<engine::Cmd>,
|
||||||
|
debug: bool,
|
||||||
|
) {
|
||||||
|
if !working.load(atomic::Ordering::Relaxed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if debug {
|
||||||
|
tx.send(engine::Cmd::Log(format!("\tAnalyzing node:\n{}", node))).unwrap();
|
||||||
|
let moves = rules::get_player_moves(&node.board, &node.game_state, true);
|
||||||
|
let moves_str = format!("\tLegal moves: {}", notation::move_list_to_string(&moves));
|
||||||
|
tx.send(engine::Cmd::Log(moves_str)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (max_score, best_move) = minimax(node, 0, 2, board::is_white(node.game_state.color));
|
||||||
|
|
||||||
|
if best_move.is_some() {
|
||||||
|
let log_str = format!(
|
||||||
|
"\tBest move {} evaluated {}",
|
||||||
|
notation::move_to_string(&best_move.unwrap()), max_score
|
||||||
|
);
|
||||||
|
tx.send(engine::Cmd::Log(log_str)).unwrap();
|
||||||
|
tx.send(engine::Cmd::TmpBestMove(best_move)).unwrap();
|
||||||
|
} else {
|
||||||
|
// If no best move could be found, checkmate is unavoidable; send the first legal move.
|
||||||
|
tx.send(engine::Cmd::Log("Checkmate is unavoidable.".to_string())).unwrap();
|
||||||
|
let moves = rules::get_player_moves(&node.board, &node.game_state, true);
|
||||||
|
let m = if moves.len() > 0 { Some(moves[0]) } else { None };
|
||||||
|
tx.send(engine::Cmd::TmpBestMove(m)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// thread::sleep(time::Duration::from_secs(1));
|
||||||
|
// 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,
|
||||||
|
depth: u32,
|
||||||
|
max_depth: u32,
|
||||||
|
maximizing: bool
|
||||||
|
) -> (f32, Option<rules::Move>) {
|
||||||
|
if depth == max_depth {
|
||||||
|
let stats = stats::compute_stats(&node.board, &node.game_state);
|
||||||
|
return (evaluate(&stats), None);
|
||||||
|
}
|
||||||
|
let mut minmax = if maximizing { MIN_F32 } else { MAX_F32 };
|
||||||
|
let mut minmax_move = None;
|
||||||
|
let moves = rules::get_player_moves(&node.board, &node.game_state, true);
|
||||||
|
for m in moves {
|
||||||
|
let mut sub_node = node.clone();
|
||||||
|
rules::apply_move_to(&mut sub_node.board, &mut sub_node.game_state, &m);
|
||||||
|
if maximizing {
|
||||||
|
let (score, _) = minimax(&mut sub_node, depth + 1, max_depth, false);
|
||||||
|
if score >= minmax {
|
||||||
|
minmax = score;
|
||||||
|
minmax_move = Some(m);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let (score, _) = minimax(&mut sub_node, depth + 1, max_depth, true);
|
||||||
|
if score <= minmax {
|
||||||
|
minmax = score;
|
||||||
|
minmax_move = Some(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(minmax, minmax_move)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute a score for white/black board stats.
|
||||||
|
///
|
||||||
|
/// This uses the formula proposed by Shannon in his 1949 paper called
|
||||||
|
/// "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;
|
||||||
|
|
||||||
|
200.0 * (ws.num_kings - bs.num_kings) as f32
|
||||||
|
+ 9.0 * (ws.num_queens - bs.num_queens) as f32
|
||||||
|
+ 5.0 * (ws.num_rooks - bs.num_rooks) as f32
|
||||||
|
+ 3.0 * (ws.num_bishops - bs.num_bishops) as f32
|
||||||
|
+ 3.0 * (ws.num_knights - bs.num_knights) as f32
|
||||||
|
+ (ws.num_pawns - bs.num_pawns) as f32
|
||||||
|
- 0.5 * (
|
||||||
|
ws.num_doubled_pawns - bs.num_doubled_pawns +
|
||||||
|
ws.num_isolated_pawns - bs.num_isolated_pawns +
|
||||||
|
ws.num_backward_pawns - bs.num_backward_pawns
|
||||||
|
) as f32
|
||||||
|
+ 0.1 * (ws.mobility - bs.mobility) as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use board::pos;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_minimax() {
|
||||||
|
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"));
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
34
src/board.rs
34
src/board.rs
|
@ -199,17 +199,6 @@ pub fn get_piece_iterator<'a>(board: &'a Board) -> Box<dyn Iterator<Item = (u8,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Count number of pieces on board. Used for debugging.
|
|
||||||
pub fn num_pieces(board: &Board) -> u8 {
|
|
||||||
let mut count = 0;
|
|
||||||
for i in board.iter() {
|
|
||||||
if *i != SQ_E {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
count
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find the king of `color`.
|
/// Find the king of `color`.
|
||||||
pub fn find_king(board: &Board, color: u8) -> Option<Pos> {
|
pub fn find_king(board: &Board, color: u8) -> Option<Pos> {
|
||||||
for f in 0..8 {
|
for f in 0..8 {
|
||||||
|
@ -223,6 +212,17 @@ pub fn find_king(board: &Board, color: u8) -> Option<Pos> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Count number of pieces on board. Used for debugging.
|
||||||
|
pub fn num_pieces(board: &Board) -> u8 {
|
||||||
|
let mut count = 0;
|
||||||
|
for i in board.iter() {
|
||||||
|
if *i != SQ_E {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}
|
||||||
|
|
||||||
/// Write a text view of the board. Used for debugging.
|
/// Write a text view of the board. Used for debugging.
|
||||||
pub fn draw(board: &Board, f: &mut dyn std::io::Write) {
|
pub fn draw(board: &Board, f: &mut dyn std::io::Write) {
|
||||||
for r in (0..8).rev() {
|
for r in (0..8).rev() {
|
||||||
|
@ -315,12 +315,6 @@ mod tests {
|
||||||
assert_eq!(is_empty(&b, &pos("a3")), true);
|
assert_eq!(is_empty(&b, &pos("a3")), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_num_pieces() {
|
|
||||||
assert_eq!(num_pieces(&new_empty()), 0);
|
|
||||||
assert_eq!(num_pieces(&new()), 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_king() {
|
fn test_find_king() {
|
||||||
let b = new_empty();
|
let b = new_empty();
|
||||||
|
@ -329,4 +323,10 @@ mod tests {
|
||||||
assert_eq!(find_king(&b, SQ_WH), Some(pos("e1")));
|
assert_eq!(find_king(&b, SQ_WH), Some(pos("e1")));
|
||||||
assert_eq!(find_king(&b, SQ_BL), Some(pos("e8")));
|
assert_eq!(find_king(&b, SQ_BL), Some(pos("e8")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_num_pieces() {
|
||||||
|
assert_eq!(num_pieces(&new_empty()), 0);
|
||||||
|
assert_eq!(num_pieces(&new()), 32);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
212
src/engine.rs
212
src/engine.rs
|
@ -1,23 +1,25 @@
|
||||||
//! Vatu engine.
|
//! Vatu engine.
|
||||||
|
//!
|
||||||
|
//! 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, atomic, mpsc};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
|
use crate::analysis;
|
||||||
use crate::board;
|
use crate::board;
|
||||||
use crate::notation;
|
use crate::notation;
|
||||||
use crate::rules;
|
use crate::rules;
|
||||||
use crate::stats;
|
|
||||||
use crate::uci;
|
use crate::uci;
|
||||||
|
|
||||||
const MIN_F32: f32 = std::f32::NEG_INFINITY;
|
|
||||||
const MAX_F32: f32 = std::f32::INFINITY;
|
|
||||||
|
|
||||||
/// Analysis engine.
|
/// Analysis engine.
|
||||||
pub struct Engine {
|
pub struct Engine {
|
||||||
/// Debug mode, log some data.
|
/// Debug mode, log some data.
|
||||||
debug: bool,
|
debug: bool,
|
||||||
/// Current game state, starting point of further analysis.
|
/// Current game state, starting point of further analysis.
|
||||||
node: Node,
|
node: analysis::Node,
|
||||||
|
/// Store already evaluated nodes with their score.
|
||||||
|
// score_map: Arc<RwLock<HashMap<Node, f32>>>,
|
||||||
/// Communication mode.
|
/// Communication mode.
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
/// If true, the engine is currently listening to incoming cmds.
|
/// If true, the engine is currently listening to incoming cmds.
|
||||||
|
@ -26,48 +28,6 @@ pub struct Engine {
|
||||||
working: Arc<atomic::AtomicBool>,
|
working: Arc<atomic::AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Analysis node: a board along with the game state.
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct Node {
|
|
||||||
/// Board for this node.
|
|
||||||
board: board::Board,
|
|
||||||
/// Game state.
|
|
||||||
game_state: rules::GameState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Node {
|
|
||||||
fn new() -> Node {
|
|
||||||
Node {
|
|
||||||
board: board::new_empty(),
|
|
||||||
game_state: rules::GameState::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for Node {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Node {{ board: [...], game_state: {:?} }}",
|
|
||||||
self.game_state
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Node {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Engine communication mode.
|
/// Engine communication mode.
|
||||||
enum Mode {
|
enum Mode {
|
||||||
/// No mode, sit here and do nothing.
|
/// No mode, sit here and do nothing.
|
||||||
|
@ -105,16 +65,6 @@ pub enum Cmd {
|
||||||
Info(Vec<Info>),
|
Info(Vec<Info>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parameters for starting work.
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct WorkArgs {
|
|
||||||
move_time: i32,
|
|
||||||
white_time: i32,
|
|
||||||
black_time: i32,
|
|
||||||
white_inc: i32,
|
|
||||||
black_inc: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Information to be transmitted back to whatever is listening.
|
/// Information to be transmitted back to whatever is listening.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Info {
|
pub enum Info {
|
||||||
|
@ -126,7 +76,8 @@ impl Engine {
|
||||||
pub fn new() -> Engine {
|
pub fn new() -> Engine {
|
||||||
Engine {
|
Engine {
|
||||||
debug: false,
|
debug: false,
|
||||||
node: Node::new(),
|
node: analysis::Node::new(),
|
||||||
|
// score_map: HashMap::with_capacity(2usize.pow(10)),
|
||||||
mode: Mode::No,
|
mode: Mode::No,
|
||||||
listening: false,
|
listening: false,
|
||||||
working: Arc::new(atomic::AtomicBool::new(false)),
|
working: Arc::new(atomic::AtomicBool::new(false)),
|
||||||
|
@ -168,6 +119,7 @@ impl Engine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a command back to the controlling interface.
|
||||||
fn reply(&mut self, cmd: Cmd) {
|
fn reply(&mut self, cmd: Cmd) {
|
||||||
match &self.mode {
|
match &self.mode {
|
||||||
Mode::Uci(tx, _, _) => {
|
Mode::Uci(tx, _, _) => {
|
||||||
|
@ -210,10 +162,12 @@ impl Engine {
|
||||||
self.node.game_state.fullmove = fen.fullmove.parse::<i32>().ok().unwrap();
|
self.node.game_state.fullmove = fen.fullmove.parse::<i32>().ok().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<rules::Move>) {
|
||||||
moves.iter().for_each(|m| self.apply_move(m));
|
moves.iter().for_each(|m| self.apply_move(m));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply a move to the current node.
|
||||||
fn apply_move(&mut self, m: &rules::Move) {
|
fn apply_move(&mut self, m: &rules::Move) {
|
||||||
rules::apply_move_to(&mut self.node.board, &mut self.node.game_state, m);
|
rules::apply_move_to(&mut self.node.board, &mut self.node.game_state, m);
|
||||||
}
|
}
|
||||||
|
@ -221,7 +175,7 @@ impl Engine {
|
||||||
/// Start working on board, returning the best move found.
|
/// Start working on board, returning the best move found.
|
||||||
///
|
///
|
||||||
/// Stop working after `movetime` ms, or go on forever if it's -1.
|
/// Stop working after `movetime` ms, or go on forever if it's -1.
|
||||||
fn work(&mut self, args: &WorkArgs) {
|
fn work(&mut self, args: &analysis::AnalysisParams) {
|
||||||
self.working.store(true, atomic::Ordering::Relaxed);
|
self.working.store(true, atomic::Ordering::Relaxed);
|
||||||
let mut node = self.node.clone();
|
let mut node = self.node.clone();
|
||||||
let args = args.clone();
|
let args = args.clone();
|
||||||
|
@ -229,10 +183,11 @@ impl Engine {
|
||||||
let tx = match &self.mode { Mode::Uci(_, _, tx) => tx.clone(), _ => return };
|
let tx = match &self.mode { Mode::Uci(_, _, tx) => tx.clone(), _ => return };
|
||||||
let debug = self.debug;
|
let debug = self.debug;
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
analyze(&mut node, &args, working, tx, debug);
|
analysis::analyze(&mut node, &args, working, tx, debug);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unset the work flag, stopping workers.
|
||||||
fn stop(&mut self) {
|
fn stop(&mut self) {
|
||||||
self.working.store(false, atomic::Ordering::SeqCst);
|
self.working.store(false, atomic::Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
@ -269,7 +224,7 @@ impl Engine {
|
||||||
|
|
||||||
/// Start working using parameters passed with a "go" command.
|
/// Start working using parameters passed with a "go" command.
|
||||||
fn uci_go(&mut self, g_args: &Vec<uci::GoArgs>) {
|
fn uci_go(&mut self, g_args: &Vec<uci::GoArgs>) {
|
||||||
let mut args = WorkArgs {
|
let mut args = analysis::AnalysisParams {
|
||||||
move_time: -1,
|
move_time: -1,
|
||||||
white_time: -1,
|
white_time: -1,
|
||||||
black_time: -1,
|
black_time: -1,
|
||||||
|
@ -290,138 +245,3 @@ impl Engine {
|
||||||
self.work(&args);
|
self.work(&args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn analyze(
|
|
||||||
node: &mut Node,
|
|
||||||
_args: &WorkArgs,
|
|
||||||
working: Arc<atomic::AtomicBool>,
|
|
||||||
tx: mpsc::Sender<Cmd>,
|
|
||||||
debug: bool,
|
|
||||||
) {
|
|
||||||
if !working.load(atomic::Ordering::Relaxed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if debug {
|
|
||||||
tx.send(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(Cmd::Log(moves_str)).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let (max_score, best_move) = minimax(node, 0, 2, board::is_white(node.game_state.color));
|
|
||||||
|
|
||||||
if best_move.is_some() {
|
|
||||||
let log_str = format!(
|
|
||||||
"\tBest move {} evaluated {}",
|
|
||||||
notation::move_to_string(&best_move.unwrap()), max_score
|
|
||||||
);
|
|
||||||
tx.send(Cmd::Log(log_str)).unwrap();
|
|
||||||
tx.send(Cmd::TmpBestMove(best_move)).unwrap();
|
|
||||||
} else {
|
|
||||||
// If no best move could be found, checkmate is unavoidable; send the first legal move.
|
|
||||||
tx.send(Cmd::Log("Checkmate is unavoidable.".to_string())).unwrap();
|
|
||||||
let moves = rules::get_player_moves(&node.board, &node.game_state, true);
|
|
||||||
let m = if moves.len() > 0 { Some(moves[0]) } else { None };
|
|
||||||
tx.send(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);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fn minimax(
|
|
||||||
node: &mut Node,
|
|
||||||
depth: u32,
|
|
||||||
max_depth: u32,
|
|
||||||
maximizing: bool
|
|
||||||
) -> (f32, Option<rules::Move>) {
|
|
||||||
if depth == max_depth {
|
|
||||||
let stats = stats::compute_stats(&node.board, &node.game_state);
|
|
||||||
return (evaluate(&stats), None);
|
|
||||||
}
|
|
||||||
let mut minmax = if maximizing { MIN_F32 } else { MAX_F32 };
|
|
||||||
let mut minmax_move = None;
|
|
||||||
let moves = rules::get_player_moves(&node.board, &node.game_state, true);
|
|
||||||
for m in moves {
|
|
||||||
let mut sub_node = node.clone();
|
|
||||||
rules::apply_move_to(&mut sub_node.board, &mut sub_node.game_state, &m);
|
|
||||||
if maximizing {
|
|
||||||
let (score, _) = minimax(&mut sub_node, depth + 1, max_depth, false);
|
|
||||||
if score > minmax {
|
|
||||||
minmax = score;
|
|
||||||
minmax_move = Some(m);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let (score, _) = minimax(&mut sub_node, depth + 1, max_depth, true);
|
|
||||||
if score < minmax {
|
|
||||||
minmax = score;
|
|
||||||
minmax_move = Some(m);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(minmax, minmax_move)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn evaluate(stats: &(stats::BoardStats, stats::BoardStats)) -> f32 {
|
|
||||||
let (ws, bs) = stats;
|
|
||||||
|
|
||||||
200.0 * (ws.num_kings - bs.num_kings) as f32
|
|
||||||
+ 9.0 * (ws.num_queens - bs.num_queens) as f32
|
|
||||||
+ 5.0 * (ws.num_rooks - bs.num_rooks) as f32
|
|
||||||
+ 3.0 * (ws.num_bishops - bs.num_bishops) as f32
|
|
||||||
+ 3.0 * (ws.num_knights - bs.num_knights) as f32
|
|
||||||
+ (ws.num_pawns - bs.num_pawns) as f32
|
|
||||||
- 0.5 * (
|
|
||||||
ws.num_doubled_pawns - bs.num_doubled_pawns +
|
|
||||||
ws.num_isolated_pawns - bs.num_isolated_pawns +
|
|
||||||
ws.num_backward_pawns - bs.num_backward_pawns
|
|
||||||
) as f32
|
|
||||||
+ 0.1 * (ws.mobility - bs.mobility) as f32
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use board::pos;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_minimax() {
|
|
||||||
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"));
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ use std::process;
|
||||||
|
|
||||||
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
|
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||||
|
|
||||||
|
pub mod analysis;
|
||||||
pub mod board;
|
pub mod board;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod notation;
|
pub mod notation;
|
||||||
|
|
25
src/rules.rs
25
src/rules.rs
|
@ -10,7 +10,10 @@ use crate::notation;
|
||||||
///
|
///
|
||||||
/// - `color`: current player's turn
|
/// - `color`: current player's turn
|
||||||
/// - `castling`: which castling options are available; updated throughout the game.
|
/// - `castling`: which castling options are available; updated throughout the game.
|
||||||
#[derive(Debug, Clone)]
|
/// - `en_passant`: position of a pawn that can be taken using en passant attack.
|
||||||
|
/// - `halfmove`: eh not sure
|
||||||
|
/// - `fullmove`: same
|
||||||
|
#[derive(Debug, PartialEq, Clone, Hash)]
|
||||||
pub struct GameState {
|
pub struct GameState {
|
||||||
pub color: u8,
|
pub color: u8,
|
||||||
pub castling: u8,
|
pub castling: u8,
|
||||||
|
@ -283,9 +286,8 @@ fn get_pawn_moves(
|
||||||
// Check diagonals for pieces to attack.
|
// Check diagonals for pieces to attack.
|
||||||
if i == 1 {
|
if i == 1 {
|
||||||
// First diagonal.
|
// First diagonal.
|
||||||
let df = f - 1;
|
if f - 1 >= POS_MIN {
|
||||||
if df >= POS_MIN {
|
let diag: Pos = (f - 1, forward_r);
|
||||||
let diag: Pos = (df, forward_r);
|
|
||||||
if let Some(m) = move_on_enemy(piece, at, get_square(board, &diag), &diag) {
|
if let Some(m) = move_on_enemy(piece, at, get_square(board, &diag), &diag) {
|
||||||
if can_register(commit, board, game_state, &m) {
|
if can_register(commit, board, game_state, &m) {
|
||||||
moves.push(m);
|
moves.push(m);
|
||||||
|
@ -293,9 +295,8 @@ fn get_pawn_moves(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Second diagonal.
|
// Second diagonal.
|
||||||
let df = f + 1;
|
if f + 1 <= POS_MAX {
|
||||||
if df <= POS_MAX {
|
let diag: Pos = (f + 1, forward_r);
|
||||||
let diag: Pos = (df, forward_r);
|
|
||||||
if let Some(m) = move_on_enemy(piece, at, get_square(board, &diag), &diag) {
|
if let Some(m) = move_on_enemy(piece, at, get_square(board, &diag), &diag) {
|
||||||
if can_register(commit, board, game_state, &m) {
|
if can_register(commit, board, game_state, &m) {
|
||||||
moves.push(m);
|
moves.push(m);
|
||||||
|
@ -317,14 +318,16 @@ fn get_bishop_moves(
|
||||||
) -> Vec<Move> {
|
) -> Vec<Move> {
|
||||||
let (f, r) = at;
|
let (f, r) = at;
|
||||||
let mut views = [true; 4]; // Store diagonals where a piece blocks commit.
|
let mut views = [true; 4]; // Store diagonals where a piece blocks commit.
|
||||||
let mut moves = vec!();
|
let mut moves = Vec::with_capacity(8);
|
||||||
for dist in 1..=7 {
|
for dist in 1..=7 {
|
||||||
for (dir, offset) in [(1, -1), (1, 1), (-1, 1), (-1, -1)].iter().enumerate() {
|
for (dir, offset) in [(1, -1), (1, 1), (-1, 1), (-1, -1)].iter().enumerate() {
|
||||||
if !views[dir] {
|
if !views[dir] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let p = (f + offset.0 * dist, r + offset.1 * dist);
|
let p = (f + offset.0 * dist, r + offset.1 * dist);
|
||||||
|
// If this position is out of the board, stop looking in that direction.
|
||||||
if !is_valid_pos(p) {
|
if !is_valid_pos(p) {
|
||||||
|
views[dir] = false;
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if is_empty(board, &p) {
|
if is_empty(board, &p) {
|
||||||
|
@ -353,7 +356,7 @@ fn get_knight_moves(
|
||||||
commit: bool,
|
commit: bool,
|
||||||
) -> Vec<Move> {
|
) -> Vec<Move> {
|
||||||
let (f, r) = at;
|
let (f, r) = at;
|
||||||
let mut moves = vec!();
|
let mut moves = Vec::with_capacity(8);
|
||||||
for offset in [(1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2)].iter() {
|
for offset in [(1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2)].iter() {
|
||||||
let p = (f + offset.0, r + offset.1);
|
let p = (f + offset.0, r + offset.1);
|
||||||
if !is_valid_pos(p) {
|
if !is_valid_pos(p) {
|
||||||
|
@ -381,7 +384,7 @@ fn get_rook_moves(
|
||||||
commit: bool,
|
commit: bool,
|
||||||
) -> Vec<Move> {
|
) -> Vec<Move> {
|
||||||
let (f, r) = at;
|
let (f, r) = at;
|
||||||
let mut moves = vec!();
|
let mut moves = Vec::with_capacity(8);
|
||||||
let mut views = [true; 4]; // Store lines where a piece blocks commit.
|
let mut views = [true; 4]; // Store lines where a piece blocks commit.
|
||||||
for dist in 1..=7 {
|
for dist in 1..=7 {
|
||||||
for (dir, offset) in [(0, 1), (1, 0), (0, -1), (-1, 0)].iter().enumerate() {
|
for (dir, offset) in [(0, 1), (1, 0), (0, -1), (-1, 0)].iter().enumerate() {
|
||||||
|
@ -389,7 +392,9 @@ fn get_rook_moves(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let p = (f + offset.0 * dist, r + offset.1 * dist);
|
let p = (f + offset.0 * dist, r + offset.1 * dist);
|
||||||
|
// If this position is out of the board, stop looking in that direction.
|
||||||
if !is_valid_pos(p) {
|
if !is_valid_pos(p) {
|
||||||
|
views[dir] = false;
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if is_empty(board, &p) {
|
if is_empty(board, &p) {
|
||||||
|
|
|
@ -82,12 +82,14 @@ pub fn compute_color_stats_into(
|
||||||
color: u8
|
color: u8
|
||||||
) {
|
) {
|
||||||
stats.reset();
|
stats.reset();
|
||||||
|
// Compute mobility for all pieces.
|
||||||
|
stats.mobility = rules::get_player_moves(board, game_state, true).len() as i32;
|
||||||
|
// Compute amount of each piece.
|
||||||
for (piece, p) in get_piece_iterator(board) {
|
for (piece, p) in get_piece_iterator(board) {
|
||||||
let (pos_f, pos_r) = p;
|
let (pos_f, pos_r) = p;
|
||||||
if piece == SQ_E || !is_color(piece, color) {
|
if piece == SQ_E || !is_color(piece, color) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// For all piece types, increment its number. Pawns have a few additional stats.
|
|
||||||
match get_type(piece) {
|
match get_type(piece) {
|
||||||
SQ_R => stats.num_rooks += 1,
|
SQ_R => stats.num_rooks += 1,
|
||||||
SQ_N => stats.num_knights += 1,
|
SQ_N => stats.num_knights += 1,
|
||||||
|
@ -162,8 +164,6 @@ pub fn compute_color_stats_into(
|
||||||
},
|
},
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
// Compute mobility for all pieces.
|
|
||||||
stats.mobility += rules::get_piece_moves(board, &p, game_state, true).len() as i32;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Reference in a new issue