engine: create Node type for easier analysis
This commit is contained in:
parent
362e23e254
commit
21aae1d9ba
67
src/board.rs
67
src/board.rs
|
@ -30,43 +30,47 @@ pub const SQ_BL_Q: u8 = SQ_BL|SQ_Q;
|
|||
pub const SQ_BL_K: u8 = SQ_BL|SQ_K;
|
||||
|
||||
#[inline]
|
||||
pub fn has_flag(i: u8, flag: u8) -> bool { i & flag == flag }
|
||||
pub const fn has_flag(i: u8, flag: u8) -> bool { i & flag == flag }
|
||||
|
||||
// Wrappers for clearer naming.
|
||||
/// Get type of piece on square, without color.
|
||||
#[inline]
|
||||
pub fn get_type(square: u8) -> u8 { square & SQ_TYPE_MASK }
|
||||
pub const 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 }
|
||||
pub const 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]
|
||||
pub fn is_piece(square: u8, piece: u8) -> bool { has_flag(square, piece) }
|
||||
pub const fn is_piece(square: u8, piece: u8) -> bool { has_flag(square, piece) }
|
||||
/// Return true if the piece on this square has this color.
|
||||
#[inline]
|
||||
pub fn is_color(square: u8, color: u8) -> bool { has_flag(square, color) }
|
||||
pub const fn is_color(square: u8, color: u8) -> bool { has_flag(square, color) }
|
||||
/// Return true if this square has a white piece.
|
||||
#[inline]
|
||||
pub fn is_white(square: u8) -> bool { is_color(square, SQ_WH) }
|
||||
pub const fn is_white(square: u8) -> bool { is_color(square, SQ_WH) }
|
||||
/// Return true if this square has a black piece.
|
||||
#[inline]
|
||||
pub fn is_black(square: u8) -> bool { is_color(square, SQ_BL) }
|
||||
pub const fn is_black(square: u8) -> bool { is_color(square, SQ_BL) }
|
||||
/// Return the color of the piece on this square.
|
||||
#[inline]
|
||||
pub fn get_color(square: u8) -> u8 { square & SQ_COLOR_MASK }
|
||||
pub const fn get_color(square: u8) -> u8 { square & SQ_COLOR_MASK }
|
||||
|
||||
/// Get opposite color.
|
||||
#[inline]
|
||||
pub fn opposite(color: u8) -> u8 { color ^ SQ_COLOR_MASK }
|
||||
pub const fn opposite(color: u8) -> u8 { color ^ SQ_COLOR_MASK }
|
||||
|
||||
/// Minimum allowed value for stored Pos components.
|
||||
pub const POS_MIN: i8 = 0;
|
||||
/// Maximum allowed value for stored Pos components.
|
||||
pub const POS_MAX: i8 = 7;
|
||||
/// Coords (file, rank) of a square on a board, both components are in [0, 7].
|
||||
pub type Pos = (i8, i8);
|
||||
|
||||
/// Check if a Pos component is in the [0, 7] range.
|
||||
#[inline]
|
||||
pub fn is_valid_pos_c(component: i8) -> bool { component >= 0 && component <= 7 }
|
||||
|
||||
/// Check if both `pos` components are valid.
|
||||
#[inline]
|
||||
pub fn is_valid_pos(pos: Pos) -> bool { is_valid_pos_c(pos.0) && is_valid_pos_c(pos.1) }
|
||||
|
||||
|
@ -75,7 +79,7 @@ pub fn is_valid_pos(pos: Pos) -> bool { is_valid_pos_c(pos.0) && is_valid_pos_c(
|
|||
/// `s` has to be valid UTF8, or the very least ASCII because chars
|
||||
/// are interpreted as raw bytes, and lowercase.
|
||||
#[inline]
|
||||
pub fn pos(s: &str) -> Pos {
|
||||
pub const fn pos(s: &str) -> Pos {
|
||||
let chars = s.as_bytes();
|
||||
((chars[0] - 0x61) as i8, (chars[1] - 0x31) as i8)
|
||||
}
|
||||
|
@ -95,7 +99,7 @@ pub fn pos_string(p: &Pos) -> String {
|
|||
pub type Board = [u8; 64];
|
||||
|
||||
/// Generate the board of a new game.
|
||||
pub fn new() -> Board {
|
||||
pub const fn new() -> Board {
|
||||
[
|
||||
/* 1 2 3 4 5 6 7 8 */
|
||||
/* A */ SQ_WH_R, SQ_WH_P, SQ_E, SQ_E, SQ_E, SQ_E, SQ_BL_P, SQ_BL_R,
|
||||
|
@ -110,7 +114,7 @@ pub fn new() -> Board {
|
|||
}
|
||||
|
||||
/// Generate an empty board.
|
||||
pub fn new_empty() -> Board {
|
||||
pub const fn new_empty() -> Board {
|
||||
[SQ_E; 64]
|
||||
}
|
||||
|
||||
|
@ -147,7 +151,7 @@ pub fn eq(b1: &Board, b2: &Board) -> bool {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_square(board: &Board, coords: &Pos) -> u8 {
|
||||
pub const fn get_square(board: &Board, coords: &Pos) -> u8 {
|
||||
board[(coords.0 * 8 + coords.1) as usize]
|
||||
}
|
||||
|
||||
|
@ -162,7 +166,7 @@ pub fn clear_square(board: &mut Board, coords: &Pos) {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_empty(board: &Board, coords: &Pos) -> bool {
|
||||
pub const fn is_empty(board: &Board, coords: &Pos) -> bool {
|
||||
get_square(board, coords) == SQ_E
|
||||
}
|
||||
|
||||
|
@ -221,22 +225,6 @@ pub fn draw(board: &Board, f: &mut dyn std::io::Write) {
|
|||
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 {
|
||||
|
@ -253,7 +241,7 @@ pub struct BoardStats {
|
|||
}
|
||||
|
||||
impl BoardStats {
|
||||
pub fn new() -> BoardStats {
|
||||
pub const 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,
|
||||
|
@ -458,23 +446,6 @@ mod tests {
|
|||
assert_eq!(find_king(&b, SQ_BL), pos("e8"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_into() {
|
||||
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_into(&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_into(&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_compute_stats() {
|
||||
// Check that initial stats are correct.
|
||||
|
|
195
src/engine.rs
195
src/engine.rs
|
@ -14,7 +14,7 @@ pub struct Engine {
|
|||
/// Debug mode, log some data.
|
||||
debug: bool,
|
||||
/// Current game state, starting point of further analysis.
|
||||
state: GameState,
|
||||
node: Node,
|
||||
/// Communication mode.
|
||||
mode: Mode,
|
||||
/// If true, the engine is currently listening to incoming cmds.
|
||||
|
@ -23,28 +23,33 @@ pub struct Engine {
|
|||
working: Arc<atomic::AtomicBool>,
|
||||
}
|
||||
|
||||
/// Representation of a game state that can cloned to analysis workers.
|
||||
///
|
||||
/// It does not include various parameters such as clocks so that they
|
||||
/// can be passed separately using `WorkArgs`.
|
||||
/// Analysis node: a board along with the game state and some stats.
|
||||
#[derive(Clone)]
|
||||
struct GameState {
|
||||
struct Node {
|
||||
/// Board for this node.
|
||||
board: board::Board,
|
||||
stats: (board::BoardStats, board::BoardStats), // white and black pieces stats.
|
||||
color: u8,
|
||||
castling: u8,
|
||||
en_passant: Option<board::Pos>,
|
||||
halfmove: i32,
|
||||
fullmove: i32,
|
||||
/// Game state.
|
||||
game_state: rules::GameState,
|
||||
/// White and black pieces stats; have to be recomputed if board changes.
|
||||
stats: (board::BoardStats, board::BoardStats), // white and black pieces stats
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for GameState {
|
||||
impl Node {
|
||||
fn new() -> Node {
|
||||
Node {
|
||||
board: board::new_empty(),
|
||||
game_state: rules::GameState::new(),
|
||||
stats: (board::BoardStats::new(), board::BoardStats::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Node {
|
||||
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
|
||||
"Node {{ board: [...], game_state: {:?}, stats: {:?} }}",
|
||||
self.game_state, self.stats
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +75,7 @@ pub enum Cmd {
|
|||
UciPosition(Vec<uci::PositionArgs>), // UCI "position" command.
|
||||
UciGo(Vec<uci::GoArgs>), // UCI "go" command.
|
||||
Stop, // Stop working ASAP.
|
||||
TmpBestMove(Option<board::Move>), // Send best move found by analysis worker (TEMPORARY).
|
||||
TmpBestMove(Option<rules::Move>), // Send best move found by analysis worker (TEMPORARY).
|
||||
WorkerInfo(Vec<Info>), // Informations from a worker.
|
||||
|
||||
// Commands that can be sent by the engine.
|
||||
|
@ -80,7 +85,7 @@ pub enum Cmd {
|
|||
/// the message to be forwarded to whatever can log.
|
||||
Log(String),
|
||||
/// Report found best move.
|
||||
BestMove(Option<board::Move>),
|
||||
BestMove(Option<rules::Move>),
|
||||
/// Report ongoing analysis information.
|
||||
Info(Vec<Info>),
|
||||
}
|
||||
|
@ -98,7 +103,7 @@ struct WorkArgs {
|
|||
/// Information to be transmitted back to whatever is listening.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Info {
|
||||
CurrentMove(board::Move),
|
||||
CurrentMove(rules::Move),
|
||||
}
|
||||
|
||||
/// General engine implementation.
|
||||
|
@ -106,15 +111,7 @@ impl Engine {
|
|||
pub fn new() -> Engine {
|
||||
Engine {
|
||||
debug: true,
|
||||
state: GameState {
|
||||
board: board::new_empty(),
|
||||
stats: (board::BoardStats::new(), board::BoardStats::new()),
|
||||
color: board::SQ_WH,
|
||||
castling: rules::CASTLING_MASK,
|
||||
en_passant: None,
|
||||
halfmove: 0,
|
||||
fullmove: 1,
|
||||
},
|
||||
node: Node::new(),
|
||||
mode: Mode::No,
|
||||
listening: false,
|
||||
working: Arc::new(atomic::AtomicBool::new(false)),
|
||||
|
@ -168,59 +165,41 @@ impl Engine {
|
|||
///
|
||||
/// For speed purposes, it assumes values are always valid.
|
||||
fn apply_fen(&mut self, fen: ¬ation::Fen) {
|
||||
self.set_fen_placement(&fen.placement);
|
||||
self.set_fen_color(&fen.color);
|
||||
self.set_fen_castling(&fen.castling);
|
||||
self.set_fen_en_passant(&fen.en_passant);
|
||||
self.set_fen_halfmove(&fen.halfmove);
|
||||
self.set_fen_fullmove(&fen.fullmove);
|
||||
}
|
||||
|
||||
fn set_fen_placement(&mut self, placement: &str) {
|
||||
self.state.board = board::new_from_fen(placement);
|
||||
}
|
||||
|
||||
fn set_fen_color(&mut self, color: &str) {
|
||||
match color.chars().next().unwrap() {
|
||||
'w' => self.state.color = board::SQ_WH,
|
||||
'b' => self.state.color = board::SQ_BL,
|
||||
// Placement.
|
||||
self.node.board = board::new_from_fen(&fen.placement);
|
||||
// Color.
|
||||
match fen.color.chars().next().unwrap() {
|
||||
'w' => self.node.game_state.color = board::SQ_WH,
|
||||
'b' => self.node.game_state.color = board::SQ_BL,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_fen_castling(&mut self, castling: &str) {
|
||||
for c in castling.chars() {
|
||||
};
|
||||
// Castling.
|
||||
for c in fen.castling.chars() {
|
||||
match c {
|
||||
'K' => self.state.castling |= rules::CASTLING_WH_K,
|
||||
'Q' => self.state.castling |= rules::CASTLING_WH_Q,
|
||||
'k' => self.state.castling |= rules::CASTLING_BL_K,
|
||||
'q' => self.state.castling |= rules::CASTLING_BL_Q,
|
||||
'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,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_fen_en_passant(&mut self, en_passant: &str) {
|
||||
self.state.en_passant = match en_passant {
|
||||
// En passant.
|
||||
self.node.game_state.en_passant = match fen.en_passant.as_ref() {
|
||||
"-" => None,
|
||||
p => Some(board::pos(p)),
|
||||
};
|
||||
// Half moves.
|
||||
self.node.game_state.halfmove = fen.halfmove.parse::<i32>().ok().unwrap();
|
||||
// Full moves.
|
||||
self.node.game_state.fullmove = fen.fullmove.parse::<i32>().ok().unwrap();
|
||||
}
|
||||
|
||||
fn set_fen_halfmove(&mut self, halfmove: &str) {
|
||||
self.state.halfmove = halfmove.parse::<i32>().ok().unwrap();
|
||||
}
|
||||
|
||||
fn set_fen_fullmove(&mut self, fullmove: &str) {
|
||||
self.state.fullmove = fullmove.parse::<i32>().ok().unwrap();
|
||||
}
|
||||
|
||||
fn apply_moves(&mut self, moves: &Vec<board::Move>) {
|
||||
fn apply_moves(&mut self, moves: &Vec<rules::Move>) {
|
||||
moves.iter().for_each(|m| self.apply_move(m));
|
||||
}
|
||||
|
||||
fn apply_move(&mut self, m: &board::Move) {
|
||||
board::apply_into(&mut self.state.board, m);
|
||||
fn apply_move(&mut self, m: &rules::Move) {
|
||||
rules::apply_move_to(&mut self.node.board, &mut self.node.game_state, m);
|
||||
}
|
||||
|
||||
/// Start working on board, returning the best move found.
|
||||
|
@ -228,17 +207,17 @@ impl Engine {
|
|||
/// Stop working after `movetime` ms, or go on forever if it's -1.
|
||||
fn work(&mut self, args: &WorkArgs) {
|
||||
if self.debug {
|
||||
self.reply(Cmd::Log(format!("Current evaluation: {}", evaluate(&self.state.stats))));
|
||||
self.reply(Cmd::Log(format!("Current evaluation: {}", evaluate(&self.node.stats))));
|
||||
}
|
||||
|
||||
self.working.store(true, atomic::Ordering::Relaxed);
|
||||
let state = self.state.clone();
|
||||
let node = self.node.clone();
|
||||
let args = args.clone();
|
||||
let working = self.working.clone();
|
||||
let tx = match &self.mode { Mode::Uci(_, _, tx) => tx.clone(), _ => return };
|
||||
let debug = self.debug;
|
||||
thread::spawn(move || {
|
||||
analyze(&state, &args, working, tx, debug);
|
||||
analyze(&node, &args, working, tx, debug);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -271,15 +250,10 @@ impl Engine {
|
|||
},
|
||||
uci::PositionArgs::Moves(moves) => {
|
||||
self.apply_moves(&moves);
|
||||
self.state.color = if moves.len() % 2 == 0 {
|
||||
board::SQ_WH
|
||||
} else {
|
||||
board::SQ_BL
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
board::compute_stats_into(&self.state.board, &mut self.state.stats);
|
||||
board::compute_stats_into(&self.node.board, &mut self.node.stats);
|
||||
}
|
||||
|
||||
/// Start working using parameters passed with a "go" command.
|
||||
|
@ -307,41 +281,43 @@ impl Engine {
|
|||
}
|
||||
|
||||
fn analyze(
|
||||
state: &GameState,
|
||||
node: &Node,
|
||||
_args: &WorkArgs,
|
||||
wip: Arc<atomic::AtomicBool>,
|
||||
working: Arc<atomic::AtomicBool>,
|
||||
tx: mpsc::Sender<Cmd>,
|
||||
debug: bool,
|
||||
) {
|
||||
if !wip.load(atomic::Ordering::Relaxed) {
|
||||
if !working.load(atomic::Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let moves = rules::get_player_legal_moves(&state.board, state.color);
|
||||
if debug {
|
||||
let state_str = format!("Current state: {:?}", state);
|
||||
let state_str = format!("Analysing node: {:?}", node);
|
||||
tx.send(Cmd::Log(state_str)).unwrap();
|
||||
let mut s = vec!();
|
||||
board::draw(&state.board, &mut s);
|
||||
board::draw(&node.board, &mut s);
|
||||
let draw_str = String::from_utf8_lossy(&s).to_string();
|
||||
tx.send(Cmd::Log(draw_str)).unwrap();
|
||||
}
|
||||
|
||||
let moves = rules::get_player_legal_moves(&node.board, &node.game_state);
|
||||
if debug {
|
||||
let moves_str = format!("Legal moves: {}", notation::move_list_to_string(&moves));
|
||||
tx.send(Cmd::Log(moves_str)).unwrap();
|
||||
}
|
||||
|
||||
let mut best_e = if board::is_white(state.color) { -999.0 } else { 999.0 };
|
||||
let mut best_e = if board::is_white(node.game_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 mut board = node.board.clone();
|
||||
let mut game_state = node.game_state.clone();
|
||||
rules::apply_move_to(&mut board, &mut game_state, &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)
|
||||
(board::is_white(game_state.color) && e > best_e) ||
|
||||
(board::is_black(game_state.color) && e < best_e)
|
||||
{
|
||||
best_e = e;
|
||||
best_move = Some(m.clone());
|
||||
|
@ -363,20 +339,19 @@ 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
|
||||
)
|
||||
|
||||
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)]
|
||||
|
@ -385,12 +360,16 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_evaluate() {
|
||||
let mut b = board::new();
|
||||
let stats = board::compute_stats(&b);
|
||||
let mut node = Node::new();
|
||||
let stats = board::compute_stats(&node.board);
|
||||
assert_eq!(evaluate(&stats), 0.0);
|
||||
|
||||
board::apply_into(&mut b, &(notation::parse_move("d2d4")));
|
||||
let stats = board::compute_stats(&b);
|
||||
rules::apply_move_to_board(
|
||||
&mut node.board,
|
||||
&node.game_state,
|
||||
¬ation::parse_move("d2d4")
|
||||
);
|
||||
let stats = board::compute_stats(&node.board);
|
||||
assert_eq!(evaluate(&stats), 0.0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
//! Functions using various notations.
|
||||
|
||||
use crate::board;
|
||||
use crate::rules;
|
||||
|
||||
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: &rules::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));
|
||||
|
@ -21,8 +22,8 @@ pub fn move_to_string(m: &board::Move) -> String {
|
|||
move_string
|
||||
}
|
||||
|
||||
/// Parse an UCI move algebraic notation string to a board::Move.
|
||||
pub fn parse_move(m_str: &str) -> board::Move {
|
||||
/// Parse an UCI move algebraic notation string to a rules::Move.
|
||||
pub fn parse_move(m_str: &str) -> rules::Move {
|
||||
let prom = if m_str.len() == 5 {
|
||||
Some(match m_str.as_bytes()[4] {
|
||||
b'b' => board::SQ_B,
|
||||
|
@ -38,7 +39,7 @@ pub fn parse_move(m_str: &str) -> board::Move {
|
|||
}
|
||||
|
||||
/// Create a space-separated string of moves. Used for debugging.
|
||||
pub fn move_list_to_string(moves: &Vec<board::Move>) -> String {
|
||||
pub fn move_list_to_string(moves: &Vec<rules::Move>) -> String {
|
||||
moves.iter().map(|m| move_to_string(m)).collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
|
|
173
src/rules.rs
173
src/rules.rs
|
@ -2,6 +2,31 @@
|
|||
|
||||
use crate::board::*;
|
||||
|
||||
/// Characteristics of the state of a game.
|
||||
///
|
||||
/// It does not include various parameters such as clocks that are
|
||||
/// more aimed for engine analysis than typical rules checking.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GameState {
|
||||
pub color: u8,
|
||||
pub castling: u8,
|
||||
pub en_passant: Option<Pos>,
|
||||
pub halfmove: i32,
|
||||
pub fullmove: i32,
|
||||
}
|
||||
|
||||
impl GameState {
|
||||
pub const fn new() -> GameState {
|
||||
GameState {
|
||||
color: SQ_WH,
|
||||
castling: CASTLING_MASK,
|
||||
en_passant: None,
|
||||
halfmove: 0,
|
||||
fullmove: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const CASTLING_WH_K: u8 = 0b00000001;
|
||||
pub const CASTLING_WH_Q: u8 = 0b00000010;
|
||||
pub const CASTLING_BL_K: u8 = 0b00000100;
|
||||
|
@ -11,13 +36,41 @@ pub const CASTLING_MASK: u8 = 0b00001111;
|
|||
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)
|
||||
}
|
||||
|
||||
pub fn apply_move_to(board: &mut Board, game_state: &mut GameState, m: &Move) {
|
||||
apply_move_to_board(board, game_state, m);
|
||||
apply_move_to_state(game_state, m);
|
||||
}
|
||||
|
||||
/// Apply a move `m` into `board`.
|
||||
pub fn apply_move_to_board(board: &mut Board, _game_state: &GameState, m: &Move) {
|
||||
// TODO handle castling
|
||||
set_square(board, &m.1, get_square(board, &m.0));
|
||||
clear_square(board, &m.0)
|
||||
}
|
||||
|
||||
pub fn apply_move_to_state(game_state: &mut GameState, _m: &Move) {
|
||||
game_state.color = opposite(game_state.color);
|
||||
// TODO handle castling
|
||||
}
|
||||
|
||||
/// Get a list of legal moves for all pieces of this color.
|
||||
pub fn get_player_legal_moves(board: &Board, color: u8, castling: u8) -> Vec<Move> {
|
||||
filter_illegal_moves(board, color, get_player_moves(board, color, castling))
|
||||
pub fn get_player_legal_moves(board: &Board, game_state: &GameState) -> Vec<Move> {
|
||||
filter_illegal_moves(board, game_state, get_player_moves(board, game_state))
|
||||
}
|
||||
|
||||
/// Get a list of moves for all pieces of this color.
|
||||
pub fn get_player_moves(board: &Board, color: u8, castling: u8) -> Vec<Move> {
|
||||
pub fn get_player_moves(board: &Board, game_state: &GameState) -> Vec<Move> {
|
||||
let mut moves = vec!();
|
||||
for r in 0..8 {
|
||||
for f in 0..8 {
|
||||
|
@ -25,8 +78,8 @@ pub fn get_player_moves(board: &Board, color: u8, castling: u8) -> Vec<Move> {
|
|||
if is_empty(board, &p) {
|
||||
continue
|
||||
}
|
||||
if is_color(get_square(board, &p), color) {
|
||||
moves.append(&mut get_piece_moves(board, &p, castling));
|
||||
if is_color(get_square(board, &p), game_state.color) {
|
||||
moves.append(&mut get_piece_moves(board, &p, game_state));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,14 +87,14 @@ pub fn get_player_moves(board: &Board, color: u8, castling: u8) -> Vec<Move> {
|
|||
}
|
||||
|
||||
/// Get a list of moves for the piece at position `at`.
|
||||
pub fn get_piece_moves(board: &Board, at: &Pos, castling: u8) -> Vec<Move> {
|
||||
pub fn get_piece_moves(board: &Board, at: &Pos, game_state: &GameState) -> Vec<Move> {
|
||||
match get_square(board, at) {
|
||||
p if is_piece(p, SQ_P) => get_pawn_moves(board, at, p),
|
||||
p if is_piece(p, SQ_B) => get_bishop_moves(board, at, p),
|
||||
p if is_piece(p, SQ_N) => get_knight_moves(board, at, p),
|
||||
p if is_piece(p, SQ_R) => get_rook_moves(board, at, p),
|
||||
p if is_piece(p, SQ_Q) => get_queen_moves(board, at, p),
|
||||
p if is_piece(p, SQ_K) => get_king_moves(board, at, p, castling),
|
||||
p if is_piece(p, SQ_K) => get_king_moves(board, at, p, game_state),
|
||||
_ => vec!(),
|
||||
}
|
||||
}
|
||||
|
@ -171,7 +224,7 @@ fn get_queen_moves(board: &Board, at: &Pos, piece: u8) -> Vec<Move> {
|
|||
moves
|
||||
}
|
||||
|
||||
fn get_king_moves(board: &Board, at: &Pos, piece: u8, castling: u8) -> Vec<Move> {
|
||||
fn get_king_moves(board: &Board, at: &Pos, piece: u8, _game_state: &GameState) -> Vec<Move> {
|
||||
let (f, r) = at;
|
||||
let mut moves = vec!();
|
||||
for offset in [(-1, 1), (0, 1), (1, 1), (-1, 0), (1, 0), (-1, -1), (0, -1), (1, -1)].iter() {
|
||||
|
@ -211,24 +264,24 @@ fn move_on_enemy(piece1: u8, pos1: &Pos, piece2: u8, pos2: &Pos) -> Option<Move>
|
|||
/// Return an iterator filtering out illegal moves from given list.
|
||||
///
|
||||
/// Pass color of moving player to avoid checking it for every move.
|
||||
fn filter_illegal_moves(board: &Board, color: u8, castling: u8, moves: Vec<Move>) -> Vec<Move> {
|
||||
let king_p = find_king(board, color);
|
||||
fn filter_illegal_moves(board: &Board, game_state: &GameState, moves: Vec<Move>) -> Vec<Move> {
|
||||
let king_p = find_king(board, game_state.color);
|
||||
moves.into_iter().filter(|m| {
|
||||
// If king moved, use its new position.
|
||||
let king_p = if m.0 == king_p { m.1 } else { king_p };
|
||||
let new_board = apply(board, m);
|
||||
let (new_board, new_state) = apply_move(board, game_state, m);
|
||||
// Check if the move makes the player king in check.
|
||||
if is_attacked(&new_board, &king_p, castling) {
|
||||
return false
|
||||
}
|
||||
true
|
||||
!is_attacked(&new_board, &new_state, &king_p)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// Return true if the piece at position `at` is attacked.
|
||||
fn is_attacked(board: &Board, at: &Pos, castling: u8) -> bool {
|
||||
let color = get_color(get_square(board, at));
|
||||
let enemy_moves = get_player_moves(board, opposite(color), castling);
|
||||
///
|
||||
/// Beware that the game state must be coherent with the analysed
|
||||
/// square, i.e. if the piece at `at` is white, the game state should
|
||||
/// tell that it is black turn.
|
||||
fn is_attacked(board: &Board, game_state: &GameState, at: &Pos) -> bool {
|
||||
let enemy_moves = get_player_moves(board, game_state);
|
||||
for m in enemy_moves.iter() {
|
||||
if *at == m.1 {
|
||||
return true
|
||||
|
@ -241,26 +294,48 @@ fn is_attacked(board: &Board, at: &Pos, castling: u8) -> bool {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_apply_move_to_board() {
|
||||
let mut b = new_empty();
|
||||
let s = GameState::new();
|
||||
|
||||
// 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, &s, &(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, &s, &(pos("f4"), pos("e6"), None));
|
||||
assert_eq!(get_square(&b, &pos("e6")), SQ_BL_N);
|
||||
assert_eq!(num_pieces(&b), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_player_moves() {
|
||||
let b = new();
|
||||
let s = GameState::new();
|
||||
|
||||
// At first move, white has 16 pawn moves and 4 knight moves.
|
||||
let moves = get_player_moves(&b, SQ_WH, CASTLING_MASK);
|
||||
let moves = get_player_moves(&b, &s);
|
||||
assert_eq!(moves.len(), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_pawn_moves() {
|
||||
let mut b = new_empty();
|
||||
let s = GameState::new();
|
||||
|
||||
// Check that a pawn (here white queen's pawn) can move forward if the road is free.
|
||||
set_square(&mut b, &pos("d3"), SQ_WH_P);
|
||||
let moves = get_piece_moves(&b, &pos("d3"));
|
||||
let moves = get_piece_moves(&b, &pos("d3"), &s);
|
||||
assert!(moves.len() == 1 && moves.contains( &(pos("d3"), pos("d4"), None) ));
|
||||
|
||||
// Check that a pawn (here white king's pawn) can move 2 square forward on first move.
|
||||
set_square(&mut b, &pos("e2"), SQ_WH_P);
|
||||
let moves = get_piece_moves(&b, &pos("e2"));
|
||||
let moves = get_piece_moves(&b, &pos("e2"), &s);
|
||||
assert_eq!(moves.len(), 2);
|
||||
assert!(moves.contains( &(pos("e2"), pos("e3"), None) ));
|
||||
assert!(moves.contains( &(pos("e2"), pos("e4"), None) ));
|
||||
|
@ -268,23 +343,23 @@ mod tests {
|
|||
// Check that a pawn cannot move forward if a piece is blocking its path.
|
||||
// 1. black pawn 2 square forward; only 1 square forward available from start pos.
|
||||
set_square(&mut b, &pos("e4"), SQ_BL_P);
|
||||
let moves = get_piece_moves(&b, &pos("e2"));
|
||||
let moves = get_piece_moves(&b, &pos("e2"), &s);
|
||||
assert!(moves.len() == 1 && moves.contains( &(pos("e2"), pos("e3"), None) ));
|
||||
// 2. black pawn 1 square forward; no square available.
|
||||
set_square(&mut b, &pos("e3"), SQ_BL_P);
|
||||
let moves = get_piece_moves(&b, &pos("e2"));
|
||||
let moves = get_piece_moves(&b, &pos("e2"), &s);
|
||||
assert_eq!(moves.len(), 0);
|
||||
// 3. remove the e4 black pawn; the white pawn should not be able to jump above e3 pawn.
|
||||
clear_square(&mut b, &pos("e4"));
|
||||
let moves = get_piece_moves(&b, &pos("e2"));
|
||||
let moves = get_piece_moves(&b, &pos("e2"), &s);
|
||||
assert_eq!(moves.len(), 0);
|
||||
|
||||
// Check that a pawn can take a piece diagonally.
|
||||
set_square(&mut b, &pos("f3"), SQ_BL_P);
|
||||
let moves = get_piece_moves(&b, &pos("e2"));
|
||||
let moves = get_piece_moves(&b, &pos("e2"), &s);
|
||||
assert!(moves.len() == 1 && moves.contains( &(pos("e2"), pos("f3"), None) ));
|
||||
set_square(&mut b, &pos("d3"), SQ_BL_P);
|
||||
let moves = get_piece_moves(&b, &pos("e2"));
|
||||
let moves = get_piece_moves(&b, &pos("e2"), &s);
|
||||
assert_eq!(moves.len(), 2);
|
||||
assert!(moves.contains( &(pos("e2"), pos("f3"), None) ));
|
||||
assert!(moves.contains( &(pos("e2"), pos("d3"), None) ));
|
||||
|
@ -292,17 +367,18 @@ mod tests {
|
|||
// Check that a pawn moving to the last rank leads to queen promotion.
|
||||
// 1. by simply moving forward.
|
||||
set_square(&mut b, &pos("a7"), SQ_WH_P);
|
||||
let moves = get_piece_moves(&b, &pos("a7"));
|
||||
let moves = get_piece_moves(&b, &pos("a7"), &s);
|
||||
assert!(moves.len() == 1 && moves.contains( &(pos("a7"), pos("a8"), Some(SQ_Q)) ));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_bishop_moves() {
|
||||
let mut b = new_empty();
|
||||
let s = GameState::new();
|
||||
|
||||
// A bishop has maximum range when it's in a center square.
|
||||
set_square(&mut b, &pos("d4"), SQ_WH_B);
|
||||
let moves = get_piece_moves(&b, &pos("d4"));
|
||||
let moves = get_piece_moves(&b, &pos("d4"), &s);
|
||||
assert_eq!(moves.len(), 13);
|
||||
// Going top-right.
|
||||
assert!(moves.contains( &(pos("d4"), pos("e5"), None) ));
|
||||
|
@ -324,89 +400,96 @@ mod tests {
|
|||
|
||||
// When blocking sight to one square with friendly piece, lose 2 moves.
|
||||
set_square(&mut b, &pos("b2"), SQ_WH_P);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 11);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 11);
|
||||
|
||||
// When blocking sight to one square with enemy piece, lose only 1 move.
|
||||
set_square(&mut b, &pos("b2"), SQ_BL_P);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 12);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_knight_moves() {
|
||||
let mut b = new_empty();
|
||||
let s = GameState::new();
|
||||
|
||||
// A knight never has blocked sight; if it's in the center of the board, it can have up to
|
||||
// 8 moves.
|
||||
set_square(&mut b, &pos("d4"), SQ_WH_N);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 8);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 8);
|
||||
|
||||
// If on a side if has only 4 moves.
|
||||
set_square(&mut b, &pos("a4"), SQ_WH_N);
|
||||
assert_eq!(get_piece_moves(&b, &pos("a4")).len(), 4);
|
||||
assert_eq!(get_piece_moves(&b, &pos("a4"), &s).len(), 4);
|
||||
|
||||
// And in a corner, only 2 moves.
|
||||
set_square(&mut b, &pos("a1"), SQ_WH_N);
|
||||
assert_eq!(get_piece_moves(&b, &pos("a1")).len(), 2);
|
||||
assert_eq!(get_piece_moves(&b, &pos("a1"), &s).len(), 2);
|
||||
|
||||
// Add 2 friendly pieces and it is totally blocked.
|
||||
set_square(&mut b, &pos("b3"), SQ_WH_P);
|
||||
set_square(&mut b, &pos("c2"), SQ_WH_P);
|
||||
assert_eq!(get_piece_moves(&b, &pos("a1")).len(), 0);
|
||||
assert_eq!(get_piece_moves(&b, &pos("a1"), &s).len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_rook_moves() {
|
||||
let mut b = new_empty();
|
||||
let s = GameState::new();
|
||||
|
||||
set_square(&mut b, &pos("d4"), SQ_WH_R);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 14);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 14);
|
||||
set_square(&mut b, &pos("d6"), SQ_BL_P);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 12);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 12);
|
||||
set_square(&mut b, &pos("d6"), SQ_WH_P);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 11);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_queen_moves() {
|
||||
let mut b = new_empty();
|
||||
let s = GameState::new();
|
||||
|
||||
set_square(&mut b, &pos("d4"), SQ_WH_Q);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 14 + 13); // Bishop + rook moves.
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 14 + 13); // Bishop + rook moves.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_king_moves() {
|
||||
let mut b = new_empty();
|
||||
let s = GameState::new();
|
||||
|
||||
set_square(&mut b, &pos("d4"), SQ_WH_K);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 8);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 8);
|
||||
set_square(&mut b, &pos("e5"), SQ_WH_P);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4")).len(), 7);
|
||||
assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filter_illegal_moves() {
|
||||
let mut b = new_empty();
|
||||
let s = GameState::new();
|
||||
|
||||
// Place white's king on first rank.
|
||||
set_square(&mut b, &pos("e1"), SQ_WH_K);
|
||||
// Place black rook in second rank: king can only move left or right.
|
||||
set_square(&mut b, &pos("h2"), SQ_BL_R);
|
||||
let all_wh_moves = get_piece_moves(&b, &pos("e1"));
|
||||
let all_wh_moves = get_piece_moves(&b, &pos("e1"), &s);
|
||||
assert_eq!(all_wh_moves.len(), 5);
|
||||
assert_eq!(filter_illegal_moves(&b, SQ_WH, all_wh_moves).len(), 2);
|
||||
assert_eq!(filter_illegal_moves(&b, &s, all_wh_moves).len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_attacked() {
|
||||
let mut b = new_empty();
|
||||
let mut s = GameState::new();
|
||||
s.color = SQ_BL;
|
||||
|
||||
// Place a black rook in white pawn's file.
|
||||
set_square(&mut b, &pos("d4"), SQ_WH_P);
|
||||
set_square(&mut b, &pos("d6"), SQ_BL_R);
|
||||
assert!(is_attacked(&b, &pos("d4")));
|
||||
assert!(is_attacked(&b, &s, &pos("d4")));
|
||||
// Move the rook on another file, no more attack.
|
||||
apply_into(&mut b, &(pos("d6"), pos("e6"), None));
|
||||
assert!(!is_attacked(&b, &pos("d4")));
|
||||
apply_move_to(&mut b, &mut s, &(pos("d6"), pos("e6"), None));
|
||||
assert!(!is_attacked(&b, &s, &pos("d4")));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ use std::io::{self, Write};
|
|||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
use crate::board;
|
||||
use crate::engine;
|
||||
use crate::notation;
|
||||
use crate::rules;
|
||||
|
||||
const VATU_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
const VATU_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
||||
|
@ -57,13 +57,13 @@ pub enum UciCmd {
|
|||
pub enum PositionArgs {
|
||||
Startpos,
|
||||
Fen(notation::Fen),
|
||||
Moves(Vec<board::Move>),
|
||||
Moves(Vec<rules::Move>),
|
||||
}
|
||||
|
||||
/// Arguments for the go remote commands.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GoArgs {
|
||||
SearchMoves(Vec<board::Move>),
|
||||
SearchMoves(Vec<rules::Move>),
|
||||
Ponder,
|
||||
WTime(i32),
|
||||
BTime(i32),
|
||||
|
@ -249,7 +249,7 @@ impl Uci {
|
|||
}
|
||||
|
||||
/// Send best move.
|
||||
fn send_bestmove(&mut self, m: &Option<board::Move>) {
|
||||
fn send_bestmove(&mut self, m: &Option<rules::Move>) {
|
||||
let move_str = match m {
|
||||
Some(m) => notation::move_to_string(m),
|
||||
None => notation::NULL_MOVE.to_string(),
|
||||
|
|
Reference in a new issue