engine: basic Shannon board evaluation and stats
This commit is contained in:
parent
ecf122ab6e
commit
769f7e0d94
265
src/board.rs
265
src/board.rs
|
@ -8,6 +8,7 @@ pub const SQ_N: u8 = 0b00000100;
|
||||||
pub const SQ_R: u8 = 0b00001000;
|
pub const SQ_R: u8 = 0b00001000;
|
||||||
pub const SQ_Q: u8 = 0b00010000;
|
pub const SQ_Q: u8 = 0b00010000;
|
||||||
pub const SQ_K: u8 = 0b00100000;
|
pub const SQ_K: u8 = 0b00100000;
|
||||||
|
pub const SQ_TYPE_MASK: u8 = 0b00111111;
|
||||||
|
|
||||||
// Piece color flags.
|
// Piece color flags.
|
||||||
pub const SQ_WH: u8 = 0b01000000;
|
pub const SQ_WH: u8 = 0b01000000;
|
||||||
|
@ -32,18 +33,28 @@ pub const SQ_BL_K: u8 = SQ_BL|SQ_K;
|
||||||
pub fn has_flag(i: u8, flag: u8) -> bool { i & flag == flag }
|
pub fn has_flag(i: u8, flag: u8) -> bool { i & flag == flag }
|
||||||
|
|
||||||
// Wrappers for clearer naming.
|
// Wrappers for clearer naming.
|
||||||
|
/// Get type of piece on square, without color.
|
||||||
|
#[inline]
|
||||||
|
pub fn get_type(square: u8) -> u8 { square & SQ_TYPE_MASK }
|
||||||
|
/// Return true if the piece on this square is of type `piece_type`.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_type(square: u8, piece_type: u8) -> bool { get_type(square) == piece_type }
|
||||||
|
/// Return true if the piece on this square is the same as `piece`.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_piece(square: u8, piece: u8) -> bool { has_flag(square, piece) }
|
pub fn is_piece(square: u8, piece: u8) -> bool { has_flag(square, piece) }
|
||||||
|
/// Return true if the piece on this square has this color.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_color(square: u8, color: u8) -> bool { has_flag(square, color) }
|
pub fn is_color(square: u8, color: u8) -> bool { has_flag(square, color) }
|
||||||
|
/// Return true if this square has a white piece.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_white(square: u8) -> bool { is_color(square, SQ_WH) }
|
pub fn is_white(square: u8) -> bool { is_color(square, SQ_WH) }
|
||||||
|
/// Return true if this square has a black piece.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_black(square: u8) -> bool { is_color(square, SQ_BL) }
|
pub fn is_black(square: u8) -> bool { is_color(square, SQ_BL) }
|
||||||
|
/// Return the color of the piece on this square.
|
||||||
/// Get piece color.
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_color(square: u8) -> u8 { square & SQ_COLOR_MASK }
|
pub fn get_color(square: u8) -> u8 { square & SQ_COLOR_MASK }
|
||||||
|
|
||||||
/// Get opposite color.
|
/// Get opposite color.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn opposite(color: u8) -> u8 { color ^ SQ_COLOR_MASK }
|
pub fn opposite(color: u8) -> u8 { color ^ SQ_COLOR_MASK }
|
||||||
|
@ -155,7 +166,15 @@ pub fn is_empty(board: &Board, coords: &Pos) -> bool {
|
||||||
get_square(board, coords) == SQ_E
|
get_square(board, coords) == SQ_E
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Count number of pieces on board
|
pub fn get_piece_iterator<'a>(board: &'a Board) -> Box<dyn Iterator<Item = (u8, Pos)> + 'a> {
|
||||||
|
Box::new(
|
||||||
|
board.iter().enumerate()
|
||||||
|
.filter(|(_, s)| **s != SQ_E)
|
||||||
|
.map(|(i, s)| (*s, ((i / 8) as i8, (i % 8) as i8)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count number of pieces on board. Used for debugging.
|
||||||
pub fn num_pieces(board: &Board) -> u8 {
|
pub fn num_pieces(board: &Board) -> u8 {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
for i in board.iter() {
|
for i in board.iter() {
|
||||||
|
@ -180,22 +199,7 @@ pub fn find_king(board: &Board, color: u8) -> Pos {
|
||||||
(0, 0)
|
(0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A movement, with before/after positions and optional promotion.
|
/// Write a text view of the board. Used for debugging.
|
||||||
pub type Move = (Pos, Pos, Option<u8>);
|
|
||||||
|
|
||||||
/// Apply a move `m` to a copy of `board`.
|
|
||||||
pub fn apply(board: &Board, m: &Move) -> Board {
|
|
||||||
let mut new_board = board.clone();
|
|
||||||
apply_into(&mut new_board, m);
|
|
||||||
new_board
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply a move `m` into `board`.
|
|
||||||
pub fn apply_into(board: &mut Board, m: &Move) {
|
|
||||||
set_square(board, &m.1, get_square(board, &m.0));
|
|
||||||
clear_square(board, &m.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
||||||
let mut rank = String::with_capacity(8);
|
let mut rank = String::with_capacity(8);
|
||||||
|
@ -217,6 +221,160 @@ pub fn draw(board: &Board, f: &mut dyn std::io::Write) {
|
||||||
writeln!(f, " abcdefgh").unwrap();
|
writeln!(f, " abcdefgh").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A movement, with before/after positions and optional promotion.
|
||||||
|
pub type Move = (Pos, Pos, Option<u8>);
|
||||||
|
|
||||||
|
/// Apply a move `m` to a copy of `board`.
|
||||||
|
pub fn apply(board: &Board, m: &Move) -> Board {
|
||||||
|
let mut new_board = board.clone();
|
||||||
|
apply_into(&mut new_board, m);
|
||||||
|
new_board
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a move `m` into `board`.
|
||||||
|
pub fn apply_into(board: &mut Board, m: &Move) {
|
||||||
|
set_square(board, &m.1, get_square(board, &m.0));
|
||||||
|
clear_square(board, &m.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Storage for precomputed board pieces stats.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct BoardStats {
|
||||||
|
pub num_pawns: i8,
|
||||||
|
pub num_bishops: i8,
|
||||||
|
pub num_knights: i8,
|
||||||
|
pub num_rooks: i8,
|
||||||
|
pub num_queens: i8,
|
||||||
|
pub num_kings: i8,
|
||||||
|
pub num_doubled_pawns: i8, // Pawns that are on the same file as a friend.
|
||||||
|
pub num_backward_pawns: i8, // Pawns behind all other pawns on adjacent files.
|
||||||
|
pub num_isolated_pawns: i8, // Pawns that have no friend pawns on adjacent files.
|
||||||
|
pub mobility: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoardStats {
|
||||||
|
pub fn new() -> BoardStats {
|
||||||
|
BoardStats {
|
||||||
|
num_pawns: 0, num_bishops: 0, num_knights: 0, num_rooks: 0, num_queens: 0,
|
||||||
|
num_kings: 0, num_doubled_pawns: 0, num_backward_pawns: 0, num_isolated_pawns: 0,
|
||||||
|
mobility: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.num_pawns = 0;
|
||||||
|
self.num_bishops = 0;
|
||||||
|
self.num_knights = 0;
|
||||||
|
self.num_rooks = 0;
|
||||||
|
self.num_queens = 0;
|
||||||
|
self.num_kings = 0;
|
||||||
|
self.num_doubled_pawns = 0;
|
||||||
|
self.num_backward_pawns = 0;
|
||||||
|
self.num_isolated_pawns = 0;
|
||||||
|
self.mobility = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create two new BoardStats objects from the board, for white and black.
|
||||||
|
///
|
||||||
|
/// See `compute_stats_into` for details.
|
||||||
|
pub fn compute_stats(board: &Board) -> (BoardStats, BoardStats) {
|
||||||
|
let mut stats = (BoardStats::new(), BoardStats::new());
|
||||||
|
compute_stats_into(board, &mut stats);
|
||||||
|
stats
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_stats_into(board: &Board, stats: &mut (BoardStats, BoardStats)) {
|
||||||
|
compute_color_stats_into(board, &mut stats.0, SQ_WH);
|
||||||
|
compute_color_stats_into(board, &mut stats.1, SQ_BL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update `stats` for `color` from given `board`
|
||||||
|
///
|
||||||
|
/// Refresh all stats *except* `mobility`.
|
||||||
|
pub fn compute_color_stats_into(board: &Board, stats: &mut BoardStats, color: u8) {
|
||||||
|
stats.reset();
|
||||||
|
for (piece, (pos_f, pos_r)) in get_piece_iterator(board) {
|
||||||
|
if piece == SQ_E || !is_color(piece, color) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
match get_type(piece) {
|
||||||
|
SQ_P => {
|
||||||
|
stats.num_pawns += 1;
|
||||||
|
let mut doubled = false;
|
||||||
|
let mut isolated = true;
|
||||||
|
let mut backward = true;
|
||||||
|
for r in 0..8 {
|
||||||
|
// Check for doubled pawns.
|
||||||
|
if
|
||||||
|
!doubled &&
|
||||||
|
is_piece(get_square(board, &(pos_f, r)), color|SQ_P) && r != pos_r
|
||||||
|
{
|
||||||
|
doubled = true;
|
||||||
|
}
|
||||||
|
// Check for isolated pawns.
|
||||||
|
if
|
||||||
|
isolated &&
|
||||||
|
(
|
||||||
|
// Check on the left file if not on a-file...
|
||||||
|
(
|
||||||
|
pos_f > POS_MIN &&
|
||||||
|
is_piece(get_square(board, &(pos_f - 1, r)), color|SQ_P)
|
||||||
|
) ||
|
||||||
|
// Check on the right file if not on h-file...
|
||||||
|
(
|
||||||
|
pos_f < POS_MAX &&
|
||||||
|
is_piece(get_square(board, &(pos_f + 1, r)), color|SQ_P)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
isolated = false;
|
||||||
|
}
|
||||||
|
// Check for backward pawns.
|
||||||
|
if backward {
|
||||||
|
if color == SQ_WH && r <= pos_r {
|
||||||
|
if (
|
||||||
|
pos_f > POS_MIN &&
|
||||||
|
is_type(get_square(board, &(pos_f - 1, r)), SQ_P)
|
||||||
|
) || (
|
||||||
|
pos_f < POS_MAX &&
|
||||||
|
is_type(get_square(board, &(pos_f + 1, r)), SQ_P)
|
||||||
|
) {
|
||||||
|
backward = false;
|
||||||
|
}
|
||||||
|
} else if color == SQ_BL && r >= pos_r {
|
||||||
|
if (
|
||||||
|
pos_f > POS_MIN &&
|
||||||
|
is_type(get_square(board, &(pos_f - 1, r)), SQ_P)
|
||||||
|
) || (
|
||||||
|
pos_f < POS_MAX &&
|
||||||
|
is_type(get_square(board, &(pos_f + 1, r)), SQ_P)
|
||||||
|
) {
|
||||||
|
backward = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if doubled {
|
||||||
|
stats.num_doubled_pawns += 1;
|
||||||
|
}
|
||||||
|
if isolated {
|
||||||
|
stats.num_isolated_pawns += 1;
|
||||||
|
}
|
||||||
|
if backward {
|
||||||
|
stats.num_backward_pawns += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SQ_R => stats.num_rooks += 1,
|
||||||
|
SQ_N => stats.num_knights += 1,
|
||||||
|
SQ_B => stats.num_bishops += 1,
|
||||||
|
SQ_Q => stats.num_queens += 1,
|
||||||
|
SQ_K => stats.num_kings += 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -316,4 +474,73 @@ mod tests {
|
||||||
assert_eq!(get_square(&b, &pos("e6")), SQ_BL_N);
|
assert_eq!(get_square(&b, &pos("e6")), SQ_BL_N);
|
||||||
assert_eq!(num_pieces(&b), 1);
|
assert_eq!(num_pieces(&b), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compute_stats() {
|
||||||
|
// Check that initial stats are correct.
|
||||||
|
let b = new();
|
||||||
|
let initial_stats = BoardStats {
|
||||||
|
num_pawns: 8,
|
||||||
|
num_bishops: 2,
|
||||||
|
num_knights: 2,
|
||||||
|
num_rooks: 2,
|
||||||
|
num_queens: 1,
|
||||||
|
num_kings: 1,
|
||||||
|
num_doubled_pawns: 0,
|
||||||
|
num_backward_pawns: 0,
|
||||||
|
num_isolated_pawns: 0,
|
||||||
|
mobility: 0,
|
||||||
|
};
|
||||||
|
let mut stats = compute_stats(&b);
|
||||||
|
assert!(stats.0 == stats.1);
|
||||||
|
assert!(stats.0 == initial_stats);
|
||||||
|
|
||||||
|
// Check that doubled pawns are correctly counted.
|
||||||
|
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, &mut stats.0, SQ_WH);
|
||||||
|
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, &mut stats.0, SQ_WH);
|
||||||
|
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, &mut stats.0, SQ_WH);
|
||||||
|
assert_eq!(stats.0.num_doubled_pawns, 3);
|
||||||
|
|
||||||
|
// Check that isolated and backward pawns are correctly counted.
|
||||||
|
assert_eq!(stats.0.num_isolated_pawns, 0);
|
||||||
|
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, &mut stats.0, SQ_WH);
|
||||||
|
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, &mut stats.0, SQ_WH);
|
||||||
|
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, &mut stats.0, SQ_WH);
|
||||||
|
assert_eq!(stats.0.num_doubled_pawns, 5);
|
||||||
|
assert_eq!(stats.0.num_isolated_pawns, 1);
|
||||||
|
assert_eq!(stats.0.num_backward_pawns, 1);
|
||||||
|
|
||||||
|
// Check for pawns that are backward but not isolated.
|
||||||
|
let mut b = new_empty();
|
||||||
|
// Here, d4 pawn protects both e5 and e3, but it is backward.
|
||||||
|
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, &mut stats.0, SQ_WH);
|
||||||
|
assert_eq!(stats.0.num_doubled_pawns, 2);
|
||||||
|
assert_eq!(stats.0.num_isolated_pawns, 0);
|
||||||
|
assert_eq!(stats.0.num_backward_pawns, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
113
src/engine.rs
113
src/engine.rs
|
@ -13,6 +13,8 @@ use crate::uci;
|
||||||
|
|
||||||
/// Analysis engine.
|
/// Analysis engine.
|
||||||
pub struct Engine {
|
pub struct Engine {
|
||||||
|
/// Debug mode, log some data.
|
||||||
|
debug: bool,
|
||||||
/// Current game state, starting point of further analysis.
|
/// Current game state, starting point of further analysis.
|
||||||
state: GameState,
|
state: GameState,
|
||||||
/// Communication mode.
|
/// Communication mode.
|
||||||
|
@ -30,6 +32,7 @@ pub struct Engine {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct GameState {
|
struct GameState {
|
||||||
board: board::Board,
|
board: board::Board,
|
||||||
|
stats: (board::BoardStats, board::BoardStats), // white and black pieces stats.
|
||||||
color: u8,
|
color: u8,
|
||||||
castling: u8,
|
castling: u8,
|
||||||
en_passant: Option<board::Pos>,
|
en_passant: Option<board::Pos>,
|
||||||
|
@ -37,6 +40,17 @@ struct GameState {
|
||||||
fullmove: i32,
|
fullmove: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for GameState {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"GameState {{ board: [...], stats: {:?}, color: {:08b}, castling: {:08b}, \
|
||||||
|
en_passant: {:?}, halfmove: {}, fullmove: {} }}",
|
||||||
|
self.stats, self.color, self.castling, self.en_passant, self.halfmove, self.fullmove
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Engine communication mode.
|
/// Engine communication mode.
|
||||||
enum Mode {
|
enum Mode {
|
||||||
/// No mode, sit here and do nothing.
|
/// No mode, sit here and do nothing.
|
||||||
|
@ -59,11 +73,21 @@ pub enum Cmd {
|
||||||
UciGo(Vec<uci::GoArgs>), // UCI "go" command.
|
UciGo(Vec<uci::GoArgs>), // UCI "go" command.
|
||||||
Stop, // Stop working ASAP.
|
Stop, // Stop working ASAP.
|
||||||
TmpBestMove(Option<board::Move>), // Send best move found by analysis worker (TEMPORARY).
|
TmpBestMove(Option<board::Move>), // Send best move found by analysis worker (TEMPORARY).
|
||||||
|
WorkerInfo(Vec<Info>), // Informations from a worker.
|
||||||
|
|
||||||
// Commands that can be sent by the engine.
|
// Commands that can be sent by the engine.
|
||||||
|
/// Ask for a string to be logged or printed.
|
||||||
|
///
|
||||||
|
/// Note that workers can send this command to engine, expecting
|
||||||
|
/// the message to be forwarded to whatever can log.
|
||||||
|
Log(String),
|
||||||
|
/// Report found best move.
|
||||||
BestMove(Option<board::Move>),
|
BestMove(Option<board::Move>),
|
||||||
|
/// Report ongoing analysis information.
|
||||||
|
Info(Vec<Info>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parameters for starting work.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct WorkArgs {
|
struct WorkArgs {
|
||||||
move_time: i32,
|
move_time: i32,
|
||||||
|
@ -73,6 +97,12 @@ struct WorkArgs {
|
||||||
black_inc: i32,
|
black_inc: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Information to be transmitted back to whatever is listening.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Info {
|
||||||
|
CurrentMove(board::Move),
|
||||||
|
}
|
||||||
|
|
||||||
pub const CASTLING_WH_K: u8 = 0b00000001;
|
pub const CASTLING_WH_K: u8 = 0b00000001;
|
||||||
pub const CASTLING_WH_Q: u8 = 0b00000010;
|
pub const CASTLING_WH_Q: u8 = 0b00000010;
|
||||||
pub const CASTLING_BL_K: u8 = 0b00000100;
|
pub const CASTLING_BL_K: u8 = 0b00000100;
|
||||||
|
@ -83,8 +113,10 @@ pub const CASTLING_MASK: u8 = 0b00001111;
|
||||||
impl Engine {
|
impl Engine {
|
||||||
pub fn new() -> Engine {
|
pub fn new() -> Engine {
|
||||||
Engine {
|
Engine {
|
||||||
|
debug: true,
|
||||||
state: GameState {
|
state: GameState {
|
||||||
board: board::new_empty(),
|
board: board::new_empty(),
|
||||||
|
stats: (board::BoardStats::new(), board::BoardStats::new()),
|
||||||
color: board::SQ_WH,
|
color: board::SQ_WH,
|
||||||
castling: CASTLING_MASK,
|
castling: CASTLING_MASK,
|
||||||
en_passant: None,
|
en_passant: None,
|
||||||
|
@ -124,6 +156,8 @@ impl Engine {
|
||||||
Cmd::UciGo(args) => self.uci_go(&args),
|
Cmd::UciGo(args) => self.uci_go(&args),
|
||||||
Cmd::Stop => self.stop(),
|
Cmd::Stop => self.stop(),
|
||||||
// Workers commands.
|
// Workers commands.
|
||||||
|
Cmd::Log(s) => self.reply(Cmd::Log(s.to_string())),
|
||||||
|
Cmd::WorkerInfo(infos) => self.reply(Cmd::Info(infos.to_vec())),
|
||||||
Cmd::TmpBestMove(m) => self.reply(Cmd::BestMove(*m)),
|
Cmd::TmpBestMove(m) => self.reply(Cmd::BestMove(*m)),
|
||||||
_ => eprintln!("Not an engine input command: {:?}", cmd),
|
_ => eprintln!("Not an engine input command: {:?}", cmd),
|
||||||
}
|
}
|
||||||
|
@ -142,7 +176,6 @@ impl Engine {
|
||||||
///
|
///
|
||||||
/// For speed purposes, it assumes values are always valid.
|
/// For speed purposes, it assumes values are always valid.
|
||||||
fn apply_fen(&mut self, fen: ¬ation::Fen) {
|
fn apply_fen(&mut self, fen: ¬ation::Fen) {
|
||||||
eprintln!("Applying FEN {:?}", fen);
|
|
||||||
self.set_fen_placement(&fen.placement);
|
self.set_fen_placement(&fen.placement);
|
||||||
self.set_fen_color(&fen.color);
|
self.set_fen_color(&fen.color);
|
||||||
self.set_fen_castling(&fen.castling);
|
self.set_fen_castling(&fen.castling);
|
||||||
|
@ -202,13 +235,18 @@ impl Engine {
|
||||||
///
|
///
|
||||||
/// Stop working after `movetime` ms, or go on forever if it's -1.
|
/// Stop working after `movetime` ms, or go on forever if it's -1.
|
||||||
fn work(&mut self, args: &WorkArgs) {
|
fn work(&mut self, args: &WorkArgs) {
|
||||||
|
if self.debug {
|
||||||
|
self.reply(Cmd::Log(format!("Current evaluation: {}", evaluate(&self.state.stats))));
|
||||||
|
}
|
||||||
|
|
||||||
self.working.store(true, atomic::Ordering::Relaxed);
|
self.working.store(true, atomic::Ordering::Relaxed);
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
let args = args.clone();
|
let args = args.clone();
|
||||||
let working = self.working.clone();
|
let working = self.working.clone();
|
||||||
let tx = match &self.mode { Mode::Uci(_, _, tx) => tx.clone(), _ => return };
|
let tx = match &self.mode { Mode::Uci(_, _, tx) => tx.clone(), _ => return };
|
||||||
|
let debug = self.debug;
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
analyze(&state, &args, working, tx);
|
analyze(&state, &args, working, tx, debug);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,6 +287,7 @@ impl Engine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
board::compute_stats_into(&self.state.board, &mut self.state.stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start working using parameters passed with a "go" command.
|
/// Start working using parameters passed with a "go" command.
|
||||||
|
@ -280,6 +319,7 @@ fn analyze(
|
||||||
_args: &WorkArgs,
|
_args: &WorkArgs,
|
||||||
wip: Arc<atomic::AtomicBool>,
|
wip: Arc<atomic::AtomicBool>,
|
||||||
tx: mpsc::Sender<Cmd>,
|
tx: mpsc::Sender<Cmd>,
|
||||||
|
debug: bool,
|
||||||
) {
|
) {
|
||||||
if !wip.load(atomic::Ordering::Relaxed) {
|
if !wip.load(atomic::Ordering::Relaxed) {
|
||||||
return;
|
return;
|
||||||
|
@ -287,9 +327,38 @@ fn analyze(
|
||||||
|
|
||||||
// Stupid engine! Return a random move.
|
// Stupid engine! Return a random move.
|
||||||
let moves = rules::get_player_legal_moves(&state.board, state.color);
|
let moves = rules::get_player_legal_moves(&state.board, state.color);
|
||||||
let mut rng = rand::thread_rng();
|
if debug {
|
||||||
let best_move = moves.iter().choose(&mut rng).and_then(|m| Some(*m));
|
let state_str = format!("Current state: {:?}", state);
|
||||||
thread::sleep(time::Duration::from_millis(300u64));
|
tx.send(Cmd::Log(state_str)).unwrap();
|
||||||
|
let mut s = vec!();
|
||||||
|
board::draw(&state.board, &mut s);
|
||||||
|
let draw_str = String::from_utf8_lossy(&s).to_string();
|
||||||
|
tx.send(Cmd::Log(draw_str)).unwrap();
|
||||||
|
let moves_str = format!("Legal moves: {}", notation::move_list_to_string(&moves));
|
||||||
|
tx.send(Cmd::Log(moves_str)).unwrap();
|
||||||
|
}
|
||||||
|
// let mut rng = rand::thread_rng();
|
||||||
|
// let best_move = moves.iter().choose(&mut rng).and_then(|m| Some(*m));
|
||||||
|
|
||||||
|
let mut best_e = if board::is_white(state.color) { -999.0 } else { 999.0 };
|
||||||
|
let mut best_move = None;
|
||||||
|
for m in moves {
|
||||||
|
// tx.send(Cmd::WorkerInfo(vec!(Info::CurrentMove(m)))).unwrap();
|
||||||
|
let mut board = state.board.clone();
|
||||||
|
board::apply_into(&mut board, &m);
|
||||||
|
let stats = board::compute_stats(&board);
|
||||||
|
let e = evaluate(&stats);
|
||||||
|
tx.send(Cmd::Log(format!("Move {} eval {}", notation::move_to_string(&m), e))).unwrap();
|
||||||
|
|
||||||
|
if
|
||||||
|
(board::is_white(state.color) && e > best_e) ||
|
||||||
|
(board::is_black(state.color) && e < best_e)
|
||||||
|
{
|
||||||
|
best_e = e;
|
||||||
|
best_move = Some(m.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thread::sleep(time::Duration::from_millis(500u64));
|
||||||
tx.send(Cmd::TmpBestMove(best_move)).unwrap();
|
tx.send(Cmd::TmpBestMove(best_move)).unwrap();
|
||||||
|
|
||||||
// thread::sleep(time::Duration::from_secs(1));
|
// thread::sleep(time::Duration::from_secs(1));
|
||||||
|
@ -302,3 +371,37 @@ fn analyze(
|
||||||
// }
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn evaluate(stats: &(board::BoardStats, board::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 * ( // FIXME
|
||||||
|
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::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_evaluate() {
|
||||||
|
let mut b = board::new();
|
||||||
|
let stats = board::compute_stats(&b);
|
||||||
|
assert_eq!(evaluate(&stats), 0.0);
|
||||||
|
|
||||||
|
board::apply_into(&mut b, &(notation::parse_move("d2d4")));
|
||||||
|
let stats = board::compute_stats(&b);
|
||||||
|
assert_eq!(evaluate(&stats), 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ use crate::board;
|
||||||
|
|
||||||
pub const NULL_MOVE: &str = "0000";
|
pub const NULL_MOVE: &str = "0000";
|
||||||
|
|
||||||
|
/// Create a string containing the UCI algebraic notation of this move.
|
||||||
pub fn move_to_string(m: &board::Move) -> String {
|
pub fn move_to_string(m: &board::Move) -> String {
|
||||||
let mut move_string = String::new();
|
let mut move_string = String::new();
|
||||||
move_string.push_str(&board::pos_string(&m.0));
|
move_string.push_str(&board::pos_string(&m.0));
|
||||||
|
@ -20,6 +21,7 @@ pub fn move_to_string(m: &board::Move) -> String {
|
||||||
move_string
|
move_string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse an UCI move algebraic notation string to a board::Move.
|
||||||
pub fn parse_move(m_str: &str) -> board::Move {
|
pub fn parse_move(m_str: &str) -> board::Move {
|
||||||
let prom = if m_str.len() == 5 {
|
let prom = if m_str.len() == 5 {
|
||||||
Some(match m_str.as_bytes()[4] {
|
Some(match m_str.as_bytes()[4] {
|
||||||
|
@ -35,6 +37,11 @@ pub fn parse_move(m_str: &str) -> board::Move {
|
||||||
(board::pos(&m_str[0..2]), board::pos(&m_str[2..4]), prom)
|
(board::pos(&m_str[0..2]), board::pos(&m_str[2..4]), prom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a space-separated string of moves. Used for debugging.
|
||||||
|
pub fn move_list_to_string(moves: &Vec<board::Move>) -> String {
|
||||||
|
moves.iter().map(|m| move_to_string(m)).collect::<Vec<_>>().join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
pub const FEN_START: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
pub const FEN_START: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
||||||
|
|
||||||
/// FEN notation for positions, split into fields.
|
/// FEN notation for positions, split into fields.
|
||||||
|
|
21
src/uci.rs
21
src/uci.rs
|
@ -195,11 +195,17 @@ impl Uci {
|
||||||
|
|
||||||
/// Handle an engine command.
|
/// Handle an engine command.
|
||||||
fn handle_engine_command(&mut self, cmd: &engine::Cmd) {
|
fn handle_engine_command(&mut self, cmd: &engine::Cmd) {
|
||||||
self.log(format!("ENG >>> {:?}", cmd));
|
|
||||||
match cmd {
|
match cmd {
|
||||||
engine::Cmd::UciChannel(s) => {
|
engine::Cmd::UciChannel(s) => {
|
||||||
|
self.log("ENG >>> Channel opened.".to_string());
|
||||||
self.engine_in = Some(s.to_owned());
|
self.engine_in = Some(s.to_owned());
|
||||||
}
|
}
|
||||||
|
engine::Cmd::Log(s) => {
|
||||||
|
self.log(s.to_string());
|
||||||
|
}
|
||||||
|
engine::Cmd::Info(infos) => {
|
||||||
|
self.send_infos(infos);
|
||||||
|
}
|
||||||
engine::Cmd::BestMove(m) => {
|
engine::Cmd::BestMove(m) => {
|
||||||
self.state = State::Ready;
|
self.state = State::Ready;
|
||||||
self.send_bestmove(m);
|
self.send_bestmove(m);
|
||||||
|
@ -229,6 +235,19 @@ impl Uci {
|
||||||
self.send("readyok");
|
self.send("readyok");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send engine analysis information.
|
||||||
|
fn send_infos(&mut self, infos: &Vec<engine::Info>) {
|
||||||
|
let mut s = "info".to_string();
|
||||||
|
for i in infos {
|
||||||
|
match i {
|
||||||
|
engine::Info::CurrentMove(m) => {
|
||||||
|
s.push_str(&format!(" currmove {}", notation::move_to_string(m)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.send(&s);
|
||||||
|
}
|
||||||
|
|
||||||
/// Send best move.
|
/// Send best move.
|
||||||
fn send_bestmove(&mut self, m: &Option<board::Move>) {
|
fn send_bestmove(&mut self, m: &Option<board::Move>) {
|
||||||
let move_str = match m {
|
let move_str = match m {
|
||||||
|
|
Reference in a new issue