rules: rework illegal moves detection (WIP)

This commit is contained in:
dece 2020-06-23 00:15:25 +02:00
parent 97444db39c
commit 3d5bbb8d2c
7 changed files with 108 additions and 62 deletions

View file

@ -103,7 +103,7 @@ impl Analyzer {
if self.debug {
self.log(format!("Analyzing node:\n{}", &self.node));
let moves = self.node.get_player_legal_moves();
let moves = self.node.get_player_moves();
self.log(format!("Legal moves: {}", Move::list_to_uci_string(&moves)));
self.log(format!("Move time: {}", self.time_limit));
}
@ -119,7 +119,7 @@ impl Analyzer {
} else {
// If no best move could be found, checkmate is unavoidable; send the first legal move.
self.log("Checkmate is unavoidable.".to_string());
let moves = rules::get_player_moves(&self.node.board, &self.node.game_state, false);
let moves = rules::get_player_moves(&self.node.board, &self.node.game_state);
let m = if moves.len() > 0 { Some(moves[0].clone()) } else { None };
self.report_best_move(m);
}
@ -185,7 +185,7 @@ impl Analyzer {
}
// Get negamax for playable moves.
let moves = node.get_player_legal_moves();
let moves = node.get_player_moves();
let mut alpha = alpha;
let mut best_score = MIN_F32;
let mut best_move = None;

View file

@ -163,6 +163,21 @@ pub struct Board {
pub pieces: [Bitboard; 6],
}
/// A direction to move (file and rank).
pub type Direction = (i8, i8);
/// Direction in which bishops moves.
pub const BISHOP_DIRS: [Direction; 4] = [
(1, 1), (1, -1), (-1, -1), (-1, 1)
];
/// Direction in which rooks moves.
pub const ROOK_DIRS: [Direction; 4] = [
(1, 0), (0, 1), (-1, 0), (0, -1)
];
/// Direction in which queens moves.
pub const QUEEN_DIRS: [Direction; 8] = [
(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)
];
// Factories.
impl Board {
/// Generate the board of a new game.
@ -311,14 +326,15 @@ impl Board {
None
}
/// Get capture rays for all pieces of `color`.
/// Get all rays for all pieces of `color`.
///
/// This function is used to find illegal moves for opposite color.
///
/// This add move rays of all piece types, pawns being a special
/// case: their diagonal capture are all added even though no enemy
/// piece is on the target square.
pub fn get_rays(&self, color: Color) -> Bitboard {
/// piece is on the target square. Rays include simple moves,
/// captures and friendly pieces being protected.
pub fn get_full_rays(&self, color: Color) -> Bitboard {
let mut ray_bb = 0;
let color_bb = self.by_color(color);
for square in 0..NUM_SQUARES {
@ -327,11 +343,11 @@ impl Board {
}
ray_bb |= match self.get_piece_on(square) {
PAWN => self.get_pawn_protections(square, color),
BISHOP => self.get_bishop_rays(square, color),
KNIGHT => self.get_knight_rays(square, color),
ROOK => self.get_rook_rays(square, color),
QUEEN => self.get_queen_rays(square, color),
KING => self.get_king_rays(square, color),
BISHOP => self.get_bishop_full_rays(square, color),
KNIGHT => self.get_knight_full_rays(square),
ROOK => self.get_rook_full_rays(square, color),
QUEEN => self.get_queen_full_rays(square, color),
KING => self.get_king_full_rays(square),
_ => { panic!("No piece on square {} but color {} bit is set.", square, color) }
};
}
@ -373,19 +389,32 @@ impl Board {
/// Get bishop rays: moves and captures bitboard.
pub fn get_bishop_rays(&self, square: Square, color: Color) -> Bitboard {
self.get_blockable_rays(square, color, &[(1, 1), (1, -1), (-1, -1), (-1, 1)])
self.get_blockable_rays(square, color, &BISHOP_DIRS, false)
}
/// Get all bishop rays: moves, captures and protections bitboard.
pub fn get_bishop_full_rays(&self, square: Square, color: Color) -> Bitboard {
self.get_blockable_rays(square, color, &BISHOP_DIRS, true)
}
/// Get rook rays: moves and captures bitboard.
pub fn get_rook_rays(&self, square: Square, color: Color) -> Bitboard {
self.get_blockable_rays(square, color, &[(1, 0), (0, 1), (-1, 0), (0, -1)])
self.get_blockable_rays(square, color, &ROOK_DIRS, false)
}
/// Get all rook rays: moves, captures and protections bitboard.
pub fn get_rook_full_rays(&self, square: Square, color: Color) -> Bitboard {
self.get_blockable_rays(square, color, &ROOK_DIRS, true)
}
/// Get queen rays: moves and captures bitboard.
pub fn get_queen_rays(&self, square: Square, color: Color) -> Bitboard {
self.get_blockable_rays(
square, color, &[(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)]
)
self.get_blockable_rays(square, color, &QUEEN_DIRS, false)
}
/// Get all queen rays: moves, captures and protections bitboard.
pub fn get_queen_full_rays(&self, square: Square, color: Color) -> Bitboard {
self.get_blockable_rays(square, color, &QUEEN_DIRS, true)
}
/// Get rays for piece that can move how far they want.
@ -393,14 +422,18 @@ impl Board {
/// Used for bishops, rooks and queens. A ray bitboard is the
/// combination of squares either empty or occupied by an enemy
/// piece they can reach.
fn get_blockable_rays(&self,
///
/// If `protection` is true, consider friend pieces in rays as well.
fn get_blockable_rays(
&self,
square: Square,
color: Color,
directions: &[(i8, i8)]
directions: &[(i8, i8)],
include_protections: bool
) -> Bitboard {
let mut rays_bb: Bitboard = 0;
let color_bb = self.by_color(color);
let enemy_bb = self.by_color(opposite(color));
let combined_bb = self.combined();
for dir in directions {
let mut ray_f = sq_file(square);
let mut ray_r = sq_rank(square);
@ -411,11 +444,11 @@ impl Board {
break
}
let bp = bit_pos(sq(ray_f, ray_r));
if color_bb & bp != 0 {
if !include_protections && color_bb & bp != 0 {
break
}
rays_bb |= bp;
if enemy_bb & bp != 0 {
if combined_bb & bp != 0 {
break
}
}
@ -428,11 +461,21 @@ impl Board {
KNIGHT_RAYS[square as usize] & !self.by_color(color)
}
/// Get all knight rays: moves, captures and protections bitboard.
pub fn get_knight_full_rays(&self, square: Square) -> Bitboard {
KNIGHT_RAYS[square as usize]
}
/// Get king rays: moves and captures bitboard.
pub fn get_king_rays(&self, square: Square, color: Color) -> Bitboard {
KING_RAYS[square as usize] & !self.by_color(color)
}
/// Get all king rays: moves, captures and protections bitboard.
pub fn get_king_full_rays(&self, square: Square) -> Bitboard {
KING_RAYS[square as usize]
}
/// Debug only: write a text view of the board to stderr.
#[allow(dead_code)] // For tests only.
pub(crate) fn draw(&self) {
@ -589,10 +632,11 @@ mod tests {
}
#[test]
fn test_get_rays() {
fn test_get_full_rays() {
let b = Board::new();
assert_eq!(count_bits(b.get_rays(WHITE)), 8);
assert_eq!(count_bits(b.get_rays(BLACK)), 8);
// Third ranks protected, all pieces protected except rooks = 22 squares.
assert_eq!(count_bits(b.get_full_rays(WHITE)), 22);
assert_eq!(count_bits(b.get_full_rays(BLACK)), 22);
}
#[test]

View file

@ -130,7 +130,7 @@ impl Engine {
Cmd::DrawBoard => {
let mut s = vec!();
self.node.board.draw_to(&mut s);
let s = format!("Current board:\n{}", String::from_utf8_lossy(&s));
let s = format!("{}", String::from_utf8_lossy(&s));
self.reply(Cmd::Log(s));
}
_ => eprintln!("Not an engine input command: {:?}", cmd),
@ -162,10 +162,10 @@ impl Engine {
// Castling.
for c in fen.castling.chars() {
match c {
'K' => self.node.game_state.castling |= castling::CASTLING_WH_K,
'Q' => self.node.game_state.castling |= castling::CASTLING_WH_Q,
'k' => self.node.game_state.castling |= castling::CASTLING_BL_K,
'q' => self.node.game_state.castling |= castling::CASTLING_BL_Q,
'K' => self.node.game_state.castling |= castling::CASTLE_WH_K,
'Q' => self.node.game_state.castling |= castling::CASTLE_WH_Q,
'k' => self.node.game_state.castling |= castling::CASTLE_BL_K,
'q' => self.node.game_state.castling |= castling::CASTLE_BL_Q,
_ => {}
}
}

View file

@ -37,13 +37,13 @@ impl Move {
/// Apply this move to `board` and `game_state`.
pub fn apply_to(&self, board: &mut Board, game_state: &mut GameState) {
// If a king moves, remove it from castling options.
if self.source == E1 { game_state.castling &= !CASTLING_WH_MASK; }
else if self.source == E8 { game_state.castling &= !CASTLING_BL_MASK; }
if self.source == E1 { game_state.castling &= !CASTLE_WH_MASK; }
else if self.source == E8 { game_state.castling &= !CASTLE_BL_MASK; }
// Same for rooks.
if self.source == A1 || self.dest == A1 { game_state.castling &= !CASTLING_WH_Q; }
else if self.source == H1 || self.dest == H1 { game_state.castling &= !CASTLING_WH_K; }
else if self.source == A8 || self.dest == A8 { game_state.castling &= !CASTLING_BL_Q; }
else if self.source == H8 || self.dest == H8 { game_state.castling &= !CASTLING_BL_K; }
if self.source == A1 || self.dest == A1 { game_state.castling &= !CASTLE_WH_Q; }
else if self.source == H1 || self.dest == H1 { game_state.castling &= !CASTLE_WH_K; }
else if self.source == A8 || self.dest == A8 { game_state.castling &= !CASTLE_BL_Q; }
else if self.source == H8 || self.dest == H8 { game_state.castling &= !CASTLE_BL_K; }
// Update board and game state.
self.apply_to_board(board);
game_state.color = opposite(game_state.color);
@ -56,10 +56,10 @@ impl Move {
if piece == KING {
if let Some(castle) = self.get_castle() {
match castle {
CASTLING_WH_K => { board.move_square(E1, G1); board.move_square(H1, F1); }
CASTLING_WH_Q => { board.move_square(E1, C1); board.move_square(A1, D1); }
CASTLING_BL_K => { board.move_square(E8, G8); board.move_square(H8, F8); }
CASTLING_BL_Q => { board.move_square(E8, C8); board.move_square(A8, D8); }
CASTLE_WH_K => { board.move_square(E1, G1); board.move_square(H1, F1); }
CASTLE_WH_Q => { board.move_square(E1, C1); board.move_square(A1, D1); }
CASTLE_BL_K => { board.move_square(E8, G8); board.move_square(H8, F8); }
CASTLE_BL_Q => { board.move_square(E8, C8); board.move_square(A8, D8); }
_ => { panic!("Invalid castle.") }
}
return
@ -75,10 +75,10 @@ impl Move {
/// Get the corresponding castling flag for this move.
pub fn get_castle(&self) -> Option<Castle> {
match (self.source, self.dest) {
(E1, C1) => Some(CASTLING_WH_Q),
(E1, G1) => Some(CASTLING_WH_K),
(E8, C8) => Some(CASTLING_BL_Q),
(E8, G8) => Some(CASTLING_BL_K),
(E1, C1) => Some(CASTLE_WH_Q),
(E1, G1) => Some(CASTLE_WH_K),
(E8, C8) => Some(CASTLE_BL_Q),
(E8, G8) => Some(CASTLE_BL_K),
_ => None,
}
}
@ -86,10 +86,10 @@ impl Move {
/// Get the move for this castle.
pub fn get_castle_move(castle: u8) -> Move {
match castle {
CASTLING_WH_Q => Move::new(E1, C1),
CASTLING_WH_K => Move::new(E1, G1),
CASTLING_BL_Q => Move::new(E8, C8),
CASTLING_BL_K => Move::new(E8, G8),
CASTLE_WH_Q => Move::new(E1, C1),
CASTLE_WH_K => Move::new(E1, G1),
CASTLE_BL_Q => Move::new(E8, C8),
CASTLE_BL_K => Move::new(E8, G8),
_ => panic!("Illegal castling requested: {:08b}", castle),
}
}
@ -164,7 +164,7 @@ mod tests {
fn test_apply_to_castling() {
let mut b = Board::new();
let mut gs = GameState::new();
assert_eq!(gs.castling, CASTLING_MASK);
assert_eq!(gs.castling, CASTLE_MASK);
// On a starting board, start by making place for all castles.
b.clear_square(B1, WHITE, KNIGHT);
@ -185,7 +185,7 @@ mod tests {
assert_eq!(b.get_piece_on(D1), ROOK);
assert!(b.is_empty(A1));
assert!(b.is_empty(E1));
assert_eq!(gs.castling, CASTLING_BL_MASK);
assert_eq!(gs.castling, CASTLE_BL_MASK);
// Black king-side castling.
Move::new(E8, G8).apply_to(&mut b, &mut gs);
assert_eq!(b.get_color_on(G8), BLACK);
@ -200,10 +200,10 @@ mod tests {
#[test]
fn test_get_castle() {
assert_eq!(Move::new(E1, C1).get_castle(), Some(CASTLING_WH_Q));
assert_eq!(Move::new(E1, G1).get_castle(), Some(CASTLING_WH_K));
assert_eq!(Move::new(E8, C8).get_castle(), Some(CASTLING_BL_Q));
assert_eq!(Move::new(E8, G8).get_castle(), Some(CASTLING_BL_K));
assert_eq!(Move::new(E1, C1).get_castle(), Some(CASTLE_WH_Q));
assert_eq!(Move::new(E1, G1).get_castle(), Some(CASTLE_WH_K));
assert_eq!(Move::new(E8, C8).get_castle(), Some(CASTLE_BL_Q));
assert_eq!(Move::new(E8, G8).get_castle(), Some(CASTLE_BL_K));
assert_eq!(Move::new(D2, D4).get_castle(), None);
}

View file

@ -29,8 +29,8 @@ impl Node {
}
/// Return player moves from this node.
pub fn get_player_legal_moves(&self) -> Vec<Move> {
rules::get_player_moves(&self.board, &self.game_state, false)
pub fn get_player_moves(&self) -> Vec<Move> {
rules::get_player_moves(&self.board, &self.game_state)
}
/// Compute stats for both players for this node.
@ -54,6 +54,6 @@ impl fmt::Display for Node {
let mut s = vec!();
self.board.draw_to(&mut s);
let board_drawing = String::from_utf8_lossy(&s).to_string();
write!(f, "* Board:\n{}\nGame state: {}", board_drawing, self.game_state)
write!(f, "{}{}", board_drawing, self.game_state)
}
}

View file

@ -60,7 +60,7 @@ pub fn get_player_moves(
board: &Board,
game_state: &GameState,
) -> Vec<Move> {
let attacked_bb = board.get_rays(opposite(game_state.color));
let attacked_bb = board.get_full_rays(opposite(game_state.color)); // FIXME remove, doesn't w
let mut moves = Vec::with_capacity(32);
for r in 0..8 {
for f in 0..8 {
@ -190,6 +190,8 @@ fn is_illegal(
} else {
if let Some(king) = board.find_king(game_state.color) { king } else { return false }
};
// FIXME implement unmake move
let attacked_bb = board.get_full_rays(opposite(game_state.color));
attacked_bb & bit_pos(king_square) != 0
}
@ -255,7 +257,7 @@ mod tests {
square: Square,
color: Color
) -> Vec<Move> {
let attacked_bb = board.get_rays(opposite(game_state.color));
let attacked_bb = board.get_full_rays(opposite(game_state.color));
get_piece_moves(board, game_state, square, color, attacked_bb)
}

View file

@ -1,7 +1,7 @@
//! Board statistics used for heuristics.
use crate::board::*;
use crate::rules::{GameState, get_player_moves, POS_MIN, POS_MAX};
use crate::rules::{GameState, get_player_moves};
/// Storage for board pieces stats.
#[derive(Debug, Clone, PartialEq)]
@ -62,7 +62,7 @@ impl BoardStats {
self.reset();
let color = game_state.color;
// Compute mobility for all pieces.
self.mobility = get_player_moves(board, game_state, false).len() as i32;
self.mobility = get_player_moves(board, game_state).len() as i32;
// Compute amount of each piece.
for file in 0..8 {
for rank in 0..8 {
@ -87,12 +87,12 @@ impl BoardStats {
}
// Check for isolated and backward pawns.
let (iso_on_prev_file, bw_on_prev_file) = if file > POS_MIN {
let (iso_on_prev_file, bw_on_prev_file) = if file > FILE_A {
self.find_isolated_and_backward(pawn_bb, square, color, file - 1)
} else {
(true, true)
};
let (iso_on_next_file, bw_on_next_file) = if file < POS_MAX {
let (iso_on_next_file, bw_on_next_file) = if file < FILE_H {
self.find_isolated_and_backward(pawn_bb, square, color, file + 1)
} else {
(true, true)