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`.
|
||||
pub fn find_king(board: &Board, color: u8) -> Option<Pos> {
|
||||
for f in 0..8 {
|
||||
|
@ -223,6 +212,17 @@ pub fn find_king(board: &Board, color: u8) -> Option<Pos> {
|
|||
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.
|
||||
pub fn draw(board: &Board, f: &mut dyn std::io::Write) {
|
||||
for r in (0..8).rev() {
|
||||
|
@ -315,12 +315,6 @@ mod tests {
|
|||
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]
|
||||
fn test_find_king() {
|
||||
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_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.
|
||||
//!
|
||||
//! 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::thread;
|
||||
|
||||
use crate::analysis;
|
||||
use crate::board;
|
||||
use crate::notation;
|
||||
use crate::rules;
|
||||
use crate::stats;
|
||||
use crate::uci;
|
||||
|
||||
const MIN_F32: f32 = std::f32::NEG_INFINITY;
|
||||
const MAX_F32: f32 = std::f32::INFINITY;
|
||||
|
||||
/// Analysis engine.
|
||||
pub struct Engine {
|
||||
/// Debug mode, log some data.
|
||||
debug: bool,
|
||||
/// 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.
|
||||
mode: Mode,
|
||||
/// If true, the engine is currently listening to incoming cmds.
|
||||
|
@ -26,48 +28,6 @@ pub struct Engine {
|
|||
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.
|
||||
enum Mode {
|
||||
/// No mode, sit here and do nothing.
|
||||
|
@ -105,16 +65,6 @@ pub enum Cmd {
|
|||
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.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Info {
|
||||
|
@ -126,7 +76,8 @@ impl Engine {
|
|||
pub fn new() -> Engine {
|
||||
Engine {
|
||||
debug: false,
|
||||
node: Node::new(),
|
||||
node: analysis::Node::new(),
|
||||
// score_map: HashMap::with_capacity(2usize.pow(10)),
|
||||
mode: Mode::No,
|
||||
listening: 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) {
|
||||
match &self.mode {
|
||||
Mode::Uci(tx, _, _) => {
|
||||
|
@ -210,10 +162,12 @@ impl Engine {
|
|||
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>) {
|
||||
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);
|
||||
}
|
||||
|
@ -221,7 +175,7 @@ impl Engine {
|
|||
/// Start working on board, returning the best move found.
|
||||
///
|
||||
/// 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);
|
||||
let mut node = self.node.clone();
|
||||
let args = args.clone();
|
||||
|
@ -229,10 +183,11 @@ impl Engine {
|
|||
let tx = match &self.mode { Mode::Uci(_, _, tx) => tx.clone(), _ => return };
|
||||
let debug = self.debug;
|
||||
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) {
|
||||
self.working.store(false, atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
@ -269,7 +224,7 @@ impl Engine {
|
|||
|
||||
/// Start working using parameters passed with a "go" command.
|
||||
fn uci_go(&mut self, g_args: &Vec<uci::GoArgs>) {
|
||||
let mut args = WorkArgs {
|
||||
let mut args = analysis::AnalysisParams {
|
||||
move_time: -1,
|
||||
white_time: -1,
|
||||
black_time: -1,
|
||||
|
@ -290,138 +245,3 @@ impl Engine {
|
|||
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};
|
||||
|
||||
pub mod analysis;
|
||||
pub mod board;
|
||||
pub mod engine;
|
||||
pub mod notation;
|
||||
|
|
25
src/rules.rs
25
src/rules.rs
|
@ -10,7 +10,10 @@ use crate::notation;
|
|||
///
|
||||
/// - `color`: current player's turn
|
||||
/// - `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 color: u8,
|
||||
pub castling: u8,
|
||||
|
@ -283,9 +286,8 @@ fn get_pawn_moves(
|
|||
// Check diagonals for pieces to attack.
|
||||
if i == 1 {
|
||||
// First diagonal.
|
||||
let df = f - 1;
|
||||
if df >= POS_MIN {
|
||||
let diag: Pos = (df, forward_r);
|
||||
if f - 1 >= POS_MIN {
|
||||
let diag: Pos = (f - 1, forward_r);
|
||||
if let Some(m) = move_on_enemy(piece, at, get_square(board, &diag), &diag) {
|
||||
if can_register(commit, board, game_state, &m) {
|
||||
moves.push(m);
|
||||
|
@ -293,9 +295,8 @@ fn get_pawn_moves(
|
|||
}
|
||||
}
|
||||
// Second diagonal.
|
||||
let df = f + 1;
|
||||
if df <= POS_MAX {
|
||||
let diag: Pos = (df, forward_r);
|
||||
if f + 1 <= POS_MAX {
|
||||
let diag: Pos = (f + 1, forward_r);
|
||||
if let Some(m) = move_on_enemy(piece, at, get_square(board, &diag), &diag) {
|
||||
if can_register(commit, board, game_state, &m) {
|
||||
moves.push(m);
|
||||
|
@ -317,14 +318,16 @@ fn get_bishop_moves(
|
|||
) -> Vec<Move> {
|
||||
let (f, r) = at;
|
||||
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 (dir, offset) in [(1, -1), (1, 1), (-1, 1), (-1, -1)].iter().enumerate() {
|
||||
if !views[dir] {
|
||||
continue
|
||||
}
|
||||
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) {
|
||||
views[dir] = false;
|
||||
continue
|
||||
}
|
||||
if is_empty(board, &p) {
|
||||
|
@ -353,7 +356,7 @@ fn get_knight_moves(
|
|||
commit: bool,
|
||||
) -> Vec<Move> {
|
||||
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() {
|
||||
let p = (f + offset.0, r + offset.1);
|
||||
if !is_valid_pos(p) {
|
||||
|
@ -381,7 +384,7 @@ fn get_rook_moves(
|
|||
commit: bool,
|
||||
) -> Vec<Move> {
|
||||
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.
|
||||
for dist in 1..=7 {
|
||||
for (dir, offset) in [(0, 1), (1, 0), (0, -1), (-1, 0)].iter().enumerate() {
|
||||
|
@ -389,7 +392,9 @@ fn get_rook_moves(
|
|||
continue
|
||||
}
|
||||
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) {
|
||||
views[dir] = false;
|
||||
continue
|
||||
}
|
||||
if is_empty(board, &p) {
|
||||
|
|
|
@ -82,12 +82,14 @@ pub fn compute_color_stats_into(
|
|||
color: u8
|
||||
) {
|
||||
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) {
|
||||
let (pos_f, pos_r) = p;
|
||||
if piece == SQ_E || !is_color(piece, color) {
|
||||
continue
|
||||
}
|
||||
// For all piece types, increment its number. Pawns have a few additional stats.
|
||||
match get_type(piece) {
|
||||
SQ_R => stats.num_rooks += 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