2020-05-31 02:41:02 +02:00
|
|
|
//! Functions to determine legal moves.
|
|
|
|
|
|
|
|
use crate::board::*;
|
2020-06-14 20:32:40 +02:00
|
|
|
use crate::castling::*;
|
2020-06-19 19:29:10 +02:00
|
|
|
use crate::fen;
|
|
|
|
use crate::movement::Move;
|
|
|
|
|
2020-06-20 03:42:20 +02:00
|
|
|
pub const POS_MIN: i8 = 0;
|
|
|
|
pub const POS_MAX: i8 = 7;
|
2020-05-31 02:41:02 +02:00
|
|
|
|
2020-06-05 18:46:08 +02:00
|
|
|
/// 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.
|
2020-06-06 00:47:07 +02:00
|
|
|
///
|
|
|
|
/// - `color`: current player's turn
|
|
|
|
/// - `castling`: which castling options are available; updated throughout the game.
|
2020-06-14 01:33:11 +02:00
|
|
|
/// - `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)]
|
2020-06-05 18:46:08 +02:00
|
|
|
pub struct GameState {
|
2020-06-19 02:44:33 +02:00
|
|
|
pub color: Color,
|
2020-06-05 18:46:08 +02:00
|
|
|
pub castling: u8,
|
2020-06-19 02:44:33 +02:00
|
|
|
pub en_passant: Option<Square>,
|
2020-06-05 18:46:08 +02:00
|
|
|
pub halfmove: i32,
|
|
|
|
pub fullmove: i32,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl GameState {
|
|
|
|
pub const fn new() -> GameState {
|
|
|
|
GameState {
|
2020-06-19 02:44:33 +02:00
|
|
|
color: WHITE,
|
2020-06-05 18:46:08 +02:00
|
|
|
castling: CASTLING_MASK,
|
|
|
|
en_passant: None,
|
|
|
|
halfmove: 0,
|
|
|
|
fullmove: 1,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-11 19:33:18 +02:00
|
|
|
impl std::fmt::Display for GameState {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
|
|
write!(
|
|
|
|
f,
|
|
|
|
"- color: {}\n\
|
|
|
|
- castling: {:04b}\n\
|
|
|
|
- en_passant: {}\n\
|
|
|
|
- halfmove: {}\n\
|
|
|
|
- fullmove: {}",
|
2020-06-11 20:40:08 +02:00
|
|
|
color_to_string(self.color), self.castling,
|
2020-06-19 19:29:10 +02:00
|
|
|
fen::en_passant_to_string(self.en_passant),
|
2020-06-11 19:33:18 +02:00
|
|
|
self.halfmove, self.fullmove
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-06 00:47:07 +02:00
|
|
|
/// Get a list of moves for all pieces of the playing color.
|
|
|
|
///
|
|
|
|
/// If `commit` is false, do not check for illegal moves. This is used
|
|
|
|
/// to avoid endless recursion when checking if a P move is illegal,
|
|
|
|
/// as it needs to check all possible following enemy moves, e.g. to
|
|
|
|
/// see if P's king can be taken. Consider a call with true `commit` as
|
|
|
|
/// a collection of attacked squares instead of legal move collection.
|
2020-06-14 20:32:40 +02:00
|
|
|
pub fn get_player_moves(
|
|
|
|
board: &Board,
|
|
|
|
game_state: &GameState,
|
|
|
|
commit: bool,
|
|
|
|
) -> Vec<Move> {
|
2020-06-14 13:59:28 +02:00
|
|
|
let mut moves = Vec::with_capacity(256);
|
2020-05-31 02:41:02 +02:00
|
|
|
for r in 0..8 {
|
|
|
|
for f in 0..8 {
|
2020-06-19 19:29:10 +02:00
|
|
|
let square = sq(f, r);
|
|
|
|
if board.is_empty(square) {
|
2020-05-31 19:00:56 +02:00
|
|
|
continue
|
|
|
|
}
|
2020-06-20 03:42:20 +02:00
|
|
|
if board.get_color_on(square) == game_state.color {
|
2020-06-19 19:29:10 +02:00
|
|
|
moves.append(
|
|
|
|
&mut get_piece_moves(board, game_state, square, game_state.color, commit)
|
|
|
|
);
|
2020-05-31 02:41:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
moves
|
|
|
|
}
|
|
|
|
|
2020-06-19 19:29:10 +02:00
|
|
|
/// Get a list of moves for the piece of `color` on `square`.
|
|
|
|
///
|
|
|
|
/// Use `board` and `game_state` to get the moves. `color` is the color
|
|
|
|
/// of the piece on `square`; it could technically be found from the
|
|
|
|
/// board but that would require an additional lookup and this function
|
|
|
|
/// is always called in a context where the piece color is known.
|
2020-06-14 20:32:40 +02:00
|
|
|
pub fn get_piece_moves(
|
|
|
|
board: &Board,
|
|
|
|
game_state: &GameState,
|
2020-06-19 19:29:10 +02:00
|
|
|
square: Square,
|
|
|
|
color: Color,
|
2020-06-14 20:32:40 +02:00
|
|
|
commit: bool,
|
|
|
|
) -> Vec<Move> {
|
2020-06-20 03:42:20 +02:00
|
|
|
match board.get_piece_on(square) {
|
2020-06-19 19:29:10 +02:00
|
|
|
PAWN => get_pawn_moves(board, game_state, square, color, commit),
|
|
|
|
BISHOP => get_bishop_moves(board, game_state, square, color, commit),
|
|
|
|
KNIGHT => get_knight_moves(board, game_state, square, color, commit),
|
|
|
|
ROOK => get_rook_moves(board, game_state, square, color, commit),
|
|
|
|
QUEEN => get_queen_moves(board, game_state, square, color, commit),
|
|
|
|
KING => get_king_moves(board, game_state, square, color, commit),
|
2020-05-31 02:41:02 +02:00
|
|
|
_ => vec!(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-06 00:47:07 +02:00
|
|
|
fn get_pawn_moves(
|
|
|
|
board: &Board,
|
|
|
|
game_state: &GameState,
|
2020-06-19 19:29:10 +02:00
|
|
|
square: Square,
|
|
|
|
color: Color,
|
2020-06-06 00:47:07 +02:00
|
|
|
commit: bool,
|
|
|
|
) -> Vec<Move> {
|
2020-06-19 19:29:10 +02:00
|
|
|
let (f, r) = (sq_file(square), sq_rank(square));
|
2020-05-31 02:41:02 +02:00
|
|
|
let mut moves = vec!();
|
2020-06-04 19:19:24 +02:00
|
|
|
// Direction: positive for white, negative for black.
|
2020-06-19 19:29:10 +02:00
|
|
|
let dir = if color == WHITE { 1 } else { -1 };
|
2020-05-31 02:41:02 +02:00
|
|
|
// Check 1 or 2 square forward.
|
2020-06-19 19:29:10 +02:00
|
|
|
let move_len = if (color == WHITE && r == 1) || (color == BLACK && r == 6) { 2 } else { 1 };
|
2020-05-31 02:41:02 +02:00
|
|
|
for i in 1..=move_len {
|
2020-06-04 19:19:24 +02:00
|
|
|
let forward_r = r + dir * i;
|
|
|
|
if dir > 0 && forward_r > POS_MAX {
|
2020-05-31 02:41:02 +02:00
|
|
|
return moves
|
|
|
|
}
|
2020-06-04 19:19:24 +02:00
|
|
|
if dir < 0 && forward_r < POS_MIN {
|
2020-05-31 02:41:02 +02:00
|
|
|
return moves
|
|
|
|
}
|
2020-06-19 19:29:10 +02:00
|
|
|
let forward: Square = sq(f, forward_r);
|
2020-06-04 18:58:00 +02:00
|
|
|
// If forward square is empty (and we are not jumping over an occupied square), add it.
|
2020-06-19 19:29:10 +02:00
|
|
|
if board.is_empty(forward) && (i == 1 || board.is_empty(sq(f, forward_r - dir))) {
|
|
|
|
let mut m = Move::new(square, forward);
|
2020-06-04 19:19:24 +02:00
|
|
|
// Pawns that get to the opposite rank automatically promote as queens.
|
2020-06-19 19:29:10 +02:00
|
|
|
if (dir > 0 && forward_r == POS_MAX) || (dir < 0 && forward_r == POS_MIN) {
|
|
|
|
m.promotion = Some(QUEEN)
|
|
|
|
}
|
2020-06-06 00:47:07 +02:00
|
|
|
if can_register(commit, board, game_state, &m) {
|
|
|
|
moves.push(m);
|
|
|
|
}
|
2020-05-31 02:41:02 +02:00
|
|
|
}
|
|
|
|
// Check diagonals for pieces to attack.
|
|
|
|
if i == 1 {
|
2020-06-06 00:47:07 +02:00
|
|
|
// First diagonal.
|
2020-06-14 01:33:11 +02:00
|
|
|
if f - 1 >= POS_MIN {
|
2020-06-19 19:29:10 +02:00
|
|
|
let diag = sq(f - 1, forward_r);
|
|
|
|
if !board.is_empty(diag) {
|
2020-06-20 03:42:20 +02:00
|
|
|
let diag_color = board.get_color_on(diag);
|
2020-06-20 19:56:00 +02:00
|
|
|
if let Some(m) = get_capture_move(color, square, diag_color, diag, true) {
|
2020-06-19 19:29:10 +02:00
|
|
|
if can_register(commit, board, game_state, &m) {
|
|
|
|
moves.push(m);
|
|
|
|
}
|
2020-06-06 00:47:07 +02:00
|
|
|
}
|
2020-06-19 19:29:10 +02:00
|
|
|
|
2020-05-31 02:41:02 +02:00
|
|
|
}
|
|
|
|
}
|
2020-06-06 00:47:07 +02:00
|
|
|
// Second diagonal.
|
2020-06-14 01:33:11 +02:00
|
|
|
if f + 1 <= POS_MAX {
|
2020-06-19 19:29:10 +02:00
|
|
|
let diag = sq(f + 1, forward_r);
|
|
|
|
if !board.is_empty(diag) {
|
2020-06-20 03:42:20 +02:00
|
|
|
let diag_color = board.get_color_on(diag);
|
2020-06-20 19:56:00 +02:00
|
|
|
if let Some(m) = get_capture_move(color, square, diag_color, diag, true) {
|
2020-06-19 19:29:10 +02:00
|
|
|
if can_register(commit, board, game_state, &m) {
|
|
|
|
moves.push(m);
|
|
|
|
}
|
2020-06-06 00:47:07 +02:00
|
|
|
}
|
2020-05-31 02:41:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// TODO en passant
|
|
|
|
}
|
|
|
|
moves
|
|
|
|
}
|
|
|
|
|
2020-06-06 00:47:07 +02:00
|
|
|
fn get_bishop_moves(
|
|
|
|
board: &Board,
|
|
|
|
game_state: &GameState,
|
2020-06-19 19:29:10 +02:00
|
|
|
square: Square,
|
|
|
|
color: Color,
|
2020-06-06 00:47:07 +02:00
|
|
|
commit: bool,
|
|
|
|
) -> Vec<Move> {
|
2020-06-19 19:29:10 +02:00
|
|
|
let (f, r) = (sq_file(square), sq_rank(square));
|
2020-06-06 00:47:07 +02:00
|
|
|
let mut views = [true; 4]; // Store diagonals where a piece blocks commit.
|
2020-06-14 01:33:11 +02:00
|
|
|
let mut moves = Vec::with_capacity(8);
|
2020-05-31 02:41:02 +02:00
|
|
|
for dist in 1..=7 {
|
|
|
|
for (dir, offset) in [(1, -1), (1, 1), (-1, 1), (-1, -1)].iter().enumerate() {
|
2020-06-06 00:47:07 +02:00
|
|
|
if !views[dir] {
|
2020-05-31 02:41:02 +02:00
|
|
|
continue
|
|
|
|
}
|
2020-06-19 19:29:10 +02:00
|
|
|
|
2020-06-14 01:33:11 +02:00
|
|
|
// If this position is out of the board, stop looking in that direction.
|
2020-06-19 19:29:10 +02:00
|
|
|
let ray_f = f + offset.0 * dist;
|
2020-06-20 03:42:20 +02:00
|
|
|
if ray_f < POS_MIN || ray_f > POS_MAX {
|
2020-06-14 01:33:11 +02:00
|
|
|
views[dir] = false;
|
2020-05-31 02:41:02 +02:00
|
|
|
continue
|
|
|
|
}
|
2020-06-19 19:29:10 +02:00
|
|
|
let ray_r = r + offset.1 * dist;
|
2020-06-20 03:42:20 +02:00
|
|
|
if ray_r < POS_MIN || ray_r > POS_MAX {
|
2020-06-19 19:29:10 +02:00
|
|
|
views[dir] = false;
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
let ray_square = sq(ray_f, ray_r);
|
|
|
|
|
2020-06-20 19:56:00 +02:00
|
|
|
match get_move_type(board, game_state, square, ray_square, color, commit) {
|
|
|
|
MoveType::Simple(m) => { moves.push(m) }
|
|
|
|
MoveType::Capture(m) => { moves.push(m); views[dir] = false; }
|
|
|
|
MoveType::CantTakeFriend => { views[dir] = false; }
|
|
|
|
_ => {}
|
2020-05-31 02:41:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
moves
|
|
|
|
}
|
|
|
|
|
2020-06-06 00:47:07 +02:00
|
|
|
fn get_knight_moves(
|
|
|
|
board: &Board,
|
|
|
|
game_state: &GameState,
|
2020-06-19 19:29:10 +02:00
|
|
|
square: Square,
|
|
|
|
color: Color,
|
2020-06-06 00:47:07 +02:00
|
|
|
commit: bool,
|
|
|
|
) -> Vec<Move> {
|
2020-06-19 19:29:10 +02:00
|
|
|
let (f, r) = (sq_file(square), sq_rank(square));
|
2020-06-14 01:33:11 +02:00
|
|
|
let mut moves = Vec::with_capacity(8);
|
2020-05-31 02:41:02 +02:00
|
|
|
for offset in [(1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2)].iter() {
|
2020-06-19 19:29:10 +02:00
|
|
|
let ray_f = f + offset.0;
|
2020-06-20 03:42:20 +02:00
|
|
|
if ray_f < POS_MIN || ray_f > POS_MAX {
|
2020-06-19 19:29:10 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
let ray_r = r + offset.1;
|
2020-06-20 03:42:20 +02:00
|
|
|
if ray_r < POS_MIN || ray_r > POS_MAX {
|
2020-05-31 02:41:02 +02:00
|
|
|
continue
|
|
|
|
}
|
2020-06-19 19:29:10 +02:00
|
|
|
let ray_square = sq(ray_f, ray_r);
|
|
|
|
|
2020-06-20 19:56:00 +02:00
|
|
|
match get_move_type(board, game_state, square, ray_square, color, commit) {
|
|
|
|
MoveType::Simple(m) | MoveType::Capture(m) => moves.push(m),
|
|
|
|
_ => {}
|
2020-05-31 02:41:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
moves
|
|
|
|
}
|
|
|
|
|
2020-06-06 00:47:07 +02:00
|
|
|
fn get_rook_moves(
|
|
|
|
board: &Board,
|
|
|
|
game_state: &GameState,
|
2020-06-19 19:29:10 +02:00
|
|
|
square: Square,
|
|
|
|
color: Color,
|
2020-06-06 00:47:07 +02:00
|
|
|
commit: bool,
|
|
|
|
) -> Vec<Move> {
|
2020-06-19 19:29:10 +02:00
|
|
|
let (f, r) = (sq_file(square), sq_rank(square));
|
2020-06-14 01:33:11 +02:00
|
|
|
let mut moves = Vec::with_capacity(8);
|
2020-06-06 00:47:07 +02:00
|
|
|
let mut views = [true; 4]; // Store lines where a piece blocks commit.
|
2020-05-31 02:41:02 +02:00
|
|
|
for dist in 1..=7 {
|
|
|
|
for (dir, offset) in [(0, 1), (1, 0), (0, -1), (-1, 0)].iter().enumerate() {
|
2020-06-06 00:47:07 +02:00
|
|
|
if !views[dir] {
|
2020-05-31 02:41:02 +02:00
|
|
|
continue
|
|
|
|
}
|
2020-06-19 19:29:10 +02:00
|
|
|
|
|
|
|
let ray_f = f + offset.0 * dist;
|
2020-06-20 03:42:20 +02:00
|
|
|
if ray_f < POS_MIN || ray_f > POS_MAX {
|
2020-06-19 19:29:10 +02:00
|
|
|
views[dir] = false;
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
let ray_r = r + offset.1 * dist;
|
2020-06-20 03:42:20 +02:00
|
|
|
if ray_r < POS_MIN || ray_r > POS_MAX {
|
2020-06-14 01:33:11 +02:00
|
|
|
views[dir] = false;
|
2020-05-31 02:41:02 +02:00
|
|
|
continue
|
|
|
|
}
|
2020-06-19 19:29:10 +02:00
|
|
|
let ray_square = sq(ray_f, ray_r);
|
|
|
|
|
2020-06-20 19:56:00 +02:00
|
|
|
match get_move_type(board, game_state, square, ray_square, color, commit) {
|
|
|
|
MoveType::Simple(m) => { moves.push(m) }
|
|
|
|
MoveType::Capture(m) => { moves.push(m); views[dir] = false; }
|
|
|
|
MoveType::CantTakeFriend => { views[dir] = false; }
|
|
|
|
_ => {}
|
2020-05-31 02:41:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
moves
|
|
|
|
}
|
|
|
|
|
2020-06-06 00:47:07 +02:00
|
|
|
fn get_queen_moves(
|
|
|
|
board: &Board,
|
|
|
|
game_state: &GameState,
|
2020-06-19 19:29:10 +02:00
|
|
|
square: Square,
|
|
|
|
color: Color,
|
|
|
|
commit: bool,
|
2020-06-06 00:47:07 +02:00
|
|
|
) -> Vec<Move> {
|
2020-06-19 19:29:10 +02:00
|
|
|
let mut moves = Vec::with_capacity(16);
|
2020-05-31 02:41:02 +02:00
|
|
|
// Easy way to get queen moves, but may be a bit quicker if everything was rewritten here.
|
2020-06-19 19:29:10 +02:00
|
|
|
moves.append(&mut get_bishop_moves(board, game_state, square, color, commit));
|
|
|
|
moves.append(&mut get_rook_moves(board, game_state, square, color, commit));
|
2020-05-31 02:41:02 +02:00
|
|
|
moves
|
|
|
|
}
|
|
|
|
|
2020-06-06 00:47:07 +02:00
|
|
|
fn get_king_moves(
|
|
|
|
board: &Board,
|
|
|
|
game_state: &GameState,
|
2020-06-19 19:29:10 +02:00
|
|
|
square: Square,
|
|
|
|
color: Color,
|
|
|
|
commit: bool,
|
2020-06-06 00:47:07 +02:00
|
|
|
) -> Vec<Move> {
|
2020-06-19 19:29:10 +02:00
|
|
|
let (f, r) = (sq_file(square), sq_rank(square));
|
|
|
|
let mut moves = Vec::with_capacity(8);
|
2020-05-31 02:41:02 +02:00
|
|
|
for offset in [(-1, 1), (0, 1), (1, 1), (-1, 0), (1, 0), (-1, -1), (0, -1), (1, -1)].iter() {
|
2020-06-19 19:29:10 +02:00
|
|
|
let ray_f = f + offset.0;
|
2020-06-20 03:42:20 +02:00
|
|
|
if ray_f < POS_MIN || ray_f > POS_MAX {
|
2020-06-19 19:29:10 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
let ray_r = r + offset.1;
|
2020-06-20 03:42:20 +02:00
|
|
|
if ray_r < POS_MIN || ray_r > POS_MAX {
|
2020-05-31 02:41:02 +02:00
|
|
|
continue
|
|
|
|
}
|
2020-06-19 19:29:10 +02:00
|
|
|
let ray_square = sq(ray_f, ray_r);
|
|
|
|
|
2020-06-20 19:56:00 +02:00
|
|
|
match get_move_type(board, game_state, square, ray_square, color, commit) {
|
|
|
|
MoveType::Simple(m) | MoveType::Capture(m) => moves.push(m),
|
|
|
|
_ => {}
|
2020-06-06 00:47:07 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stop here for uncommitted moves.
|
|
|
|
if !commit {
|
|
|
|
return moves
|
|
|
|
}
|
|
|
|
|
|
|
|
// Castling. Here are the rules that should ALL be respected:
|
|
|
|
// 1. The king and the chosen rook are on the player's first rank.
|
|
|
|
// 2. Neither the king nor the chosen rook has previously moved.
|
|
|
|
// 3. There are no pieces between the king and the chosen rook.
|
|
|
|
// 4. The king is not currently in check.
|
|
|
|
// 5. The king does not pass through a square that is attacked by an enemy piece.
|
|
|
|
// 6. The king does not end up in check.
|
|
|
|
|
|
|
|
// First get the required castling rank and color mask for the player.
|
2020-06-19 19:29:10 +02:00
|
|
|
let (castling_rank, castling_color_mask) = if game_state.color == WHITE {
|
2020-06-06 00:47:07 +02:00
|
|
|
(0, CASTLING_WH_MASK)
|
|
|
|
} else {
|
|
|
|
(7, CASTLING_BL_MASK)
|
|
|
|
};
|
|
|
|
|
2020-06-06 01:27:19 +02:00
|
|
|
// Check for castling if the king is on its castling rank (R1)
|
2020-06-11 19:32:47 +02:00
|
|
|
// and is not in check (R4).
|
2020-06-06 00:47:07 +02:00
|
|
|
if
|
2020-06-19 19:29:10 +02:00
|
|
|
r == castling_rank &&
|
|
|
|
!is_attacked(board, game_state, square)
|
2020-06-06 00:47:07 +02:00
|
|
|
{
|
|
|
|
// Check for both castling sides.
|
2020-06-11 19:32:47 +02:00
|
|
|
for (path_files, opt_empty_file, castling_side_mask) in CASTLING_SIDES.iter() {
|
2020-06-06 00:47:07 +02:00
|
|
|
// Check for castling availability for this color and side.
|
2020-06-06 01:27:19 +02:00
|
|
|
if (game_state.castling & castling_color_mask & castling_side_mask) != 0 {
|
2020-06-11 19:32:47 +02:00
|
|
|
// Check that squares in the king's path are empty and not attacked (R3.1, R5, R6).
|
|
|
|
let mut path_is_clear = true;
|
|
|
|
for path_f in path_files {
|
2020-06-19 19:29:10 +02:00
|
|
|
let path_square = sq(*path_f, castling_rank);
|
|
|
|
if
|
|
|
|
!board.is_empty(path_square)
|
|
|
|
|| is_illegal(board, game_state, &Move::new(square, path_square)) {
|
2020-06-11 19:32:47 +02:00
|
|
|
path_is_clear = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !path_is_clear {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Check that rook jumps over an empty square on queen-side (R3.2).
|
|
|
|
if let Some(rook_path_f) = opt_empty_file {
|
2020-06-19 19:29:10 +02:00
|
|
|
let rook_path_square = sq(*rook_path_f, castling_rank);
|
|
|
|
if !board.is_empty(rook_path_square) {
|
2020-06-11 19:32:47 +02:00
|
|
|
continue;
|
2020-06-06 00:47:07 +02:00
|
|
|
}
|
|
|
|
}
|
2020-06-11 19:32:47 +02:00
|
|
|
let castle = castling_side_mask & castling_color_mask;
|
2020-06-19 19:29:10 +02:00
|
|
|
let m = Move::get_castle_move(castle);
|
2020-06-11 19:32:47 +02:00
|
|
|
if can_register(commit, board, game_state, &m) {
|
|
|
|
moves.push(m);
|
|
|
|
}
|
2020-06-06 00:47:07 +02:00
|
|
|
}
|
2020-05-31 02:41:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
moves
|
|
|
|
}
|
|
|
|
|
2020-06-20 19:56:00 +02:00
|
|
|
/// Accept or ignore a move from `square` to `ray_square`.
|
|
|
|
fn get_move_type(
|
|
|
|
board: &Board,
|
|
|
|
game_state: &GameState,
|
|
|
|
square: Square,
|
|
|
|
ray_square: Square,
|
|
|
|
color: Color,
|
|
|
|
commit: bool
|
|
|
|
) -> MoveType {
|
|
|
|
if board.is_empty(ray_square) {
|
|
|
|
let m = Move::new(square, ray_square);
|
|
|
|
if can_register(commit, board, game_state, &m) {
|
|
|
|
MoveType::Simple(m)
|
|
|
|
} else {
|
|
|
|
MoveType::CantRegister
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let ray_color = board.get_color_on(ray_square);
|
|
|
|
if let Some(m) = get_capture_move(color, square, ray_color, ray_square, false) {
|
|
|
|
if can_register(commit, board, game_state, &m) {
|
|
|
|
MoveType::Capture(m)
|
|
|
|
} else {
|
|
|
|
MoveType::CantRegister
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
MoveType::CantTakeFriend
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum MoveType {
|
|
|
|
/// Move to an empty square.
|
|
|
|
Simple(Move),
|
|
|
|
/// Capture an enemy piece.
|
|
|
|
Capture(Move),
|
|
|
|
/// May be illegal, or should not be committed, e.g. to test rays.
|
|
|
|
CantRegister,
|
|
|
|
/// Can't capture a friend piece.
|
|
|
|
CantTakeFriend,
|
|
|
|
}
|
|
|
|
|
2020-06-06 00:47:07 +02:00
|
|
|
/// Return true if `commit` is false, or the move is not illegal,
|
|
|
|
///
|
|
|
|
/// Committing a move means that it can be safely played afterwards.
|
|
|
|
/// Sometimes it is not what is needed to accept a move in a collection
|
|
|
|
/// of moves, e.g. when simply checking if some moves would make a
|
|
|
|
/// previous move illegal.
|
|
|
|
#[inline]
|
|
|
|
fn can_register(commit: bool, board: &Board, game_state: &GameState, m: &Move) -> bool {
|
|
|
|
!commit || !is_illegal(board, game_state, m)
|
|
|
|
}
|
|
|
|
|
2020-06-19 19:29:10 +02:00
|
|
|
/// Return a move from `square1` to `square2` if colors are opposite.
|
2020-06-20 19:56:00 +02:00
|
|
|
fn get_capture_move(
|
2020-06-19 19:29:10 +02:00
|
|
|
color1: Color,
|
|
|
|
square1: Square,
|
|
|
|
color2: Color,
|
|
|
|
square2: Square,
|
|
|
|
is_pawn: bool,
|
|
|
|
) -> Option<Move> {
|
|
|
|
if color2 == opposite(color1) {
|
2020-06-04 19:19:24 +02:00
|
|
|
// Automatic queen promotion for pawns moving to the opposite rank.
|
2020-06-19 19:29:10 +02:00
|
|
|
Some(if
|
|
|
|
is_pawn
|
|
|
|
&& (color1 == WHITE && sq_rank(square2) == POS_MAX)
|
|
|
|
|| (color1 == BLACK && sq_rank(square2) == POS_MIN)
|
2020-06-04 19:19:24 +02:00
|
|
|
{
|
2020-06-19 19:29:10 +02:00
|
|
|
Move::new_promotion(square1, square2, QUEEN)
|
2020-06-04 19:19:24 +02:00
|
|
|
} else {
|
2020-06-19 19:29:10 +02:00
|
|
|
Move::new(square1, square2)
|
|
|
|
})
|
2020-05-31 02:41:02 +02:00
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-06 00:47:07 +02:00
|
|
|
/// Check if a move is illegal.
|
|
|
|
fn is_illegal(board: &Board, game_state: &GameState, m: &Move) -> bool {
|
2020-06-19 19:29:10 +02:00
|
|
|
if let Some(mut king_square) = board.find_king(game_state.color) {
|
2020-06-20 19:56:00 +02:00
|
|
|
let mut hypothetic_board = board.clone();
|
|
|
|
m.apply_to_board(&mut hypothetic_board);
|
|
|
|
// A move is illegal if the king ends up in check.
|
2020-06-06 00:47:07 +02:00
|
|
|
// If king moves, use its new position.
|
2020-06-19 19:29:10 +02:00
|
|
|
if m.source == king_square {
|
|
|
|
king_square = m.dest
|
|
|
|
}
|
2020-05-31 19:00:56 +02:00
|
|
|
// Check if the move makes the player king in check.
|
2020-06-19 19:29:10 +02:00
|
|
|
if is_attacked(&hypothetic_board, &game_state, king_square) {
|
2020-06-06 00:47:07 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
false
|
2020-05-31 19:00:56 +02:00
|
|
|
}
|
|
|
|
|
2020-06-19 19:29:10 +02:00
|
|
|
/// Return true if the piece on `square` is attacked.
|
2020-06-05 18:46:08 +02:00
|
|
|
///
|
2020-06-06 00:47:07 +02:00
|
|
|
/// Check all possible enemy moves and return true when one of them
|
|
|
|
/// ends up attacking the position.
|
|
|
|
///
|
2020-06-05 18:46:08 +02:00
|
|
|
/// Beware that the game state must be coherent with the analysed
|
2020-06-19 19:29:10 +02:00
|
|
|
/// square, i.e. if the piece on `square` is white, the game state
|
|
|
|
/// should tell that it is white turn. If `square` is empty, simply
|
2020-06-06 00:47:07 +02:00
|
|
|
/// check if it is getting attacked by the opposite player.
|
2020-06-19 19:29:10 +02:00
|
|
|
fn is_attacked(board: &Board, game_state: &GameState, square: Square) -> bool {
|
2020-06-06 00:47:07 +02:00
|
|
|
let mut enemy_game_state = game_state.clone();
|
|
|
|
enemy_game_state.color = opposite(game_state.color);
|
|
|
|
// Do not attempt to commit moves, just check for attacked squares.
|
|
|
|
let enemy_moves = get_player_moves(board, &enemy_game_state, false);
|
2020-05-31 19:00:56 +02:00
|
|
|
for m in enemy_moves.iter() {
|
2020-06-19 19:29:10 +02:00
|
|
|
if square == m.dest {
|
2020-05-31 19:00:56 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
false
|
|
|
|
}
|
|
|
|
|
2020-05-31 02:41:02 +02:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
2020-06-06 00:47:07 +02:00
|
|
|
|
2020-05-31 02:41:02 +02:00
|
|
|
#[test]
|
2020-05-31 19:00:56 +02:00
|
|
|
fn test_get_player_moves() {
|
2020-06-19 19:29:10 +02:00
|
|
|
let b = Board::new();
|
2020-06-06 00:47:07 +02:00
|
|
|
let gs = GameState::new();
|
2020-06-05 18:46:08 +02:00
|
|
|
|
2020-05-31 02:41:02 +02:00
|
|
|
// At first move, white has 16 pawn moves and 4 knight moves.
|
2020-06-06 00:47:07 +02:00
|
|
|
let moves = get_player_moves(&b, &gs, true);
|
2020-05-31 02:41:02 +02:00
|
|
|
assert_eq!(moves.len(), 20);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2020-05-31 19:00:56 +02:00
|
|
|
fn test_get_pawn_moves() {
|
2020-06-19 19:29:10 +02:00
|
|
|
let mut b = Board::new_empty();
|
2020-06-06 00:47:07 +02:00
|
|
|
let gs = GameState::new();
|
2020-05-31 02:41:02 +02:00
|
|
|
|
|
|
|
// Check that a pawn (here white queen's pawn) can move forward if the road is free.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(D3, WHITE, PAWN);
|
|
|
|
let moves = get_piece_moves(&b, &gs, D3, WHITE, true);
|
|
|
|
assert!(moves.len() == 1 && moves.contains(&Move::new(D3, D4)));
|
2020-05-31 02:41:02 +02:00
|
|
|
|
|
|
|
// Check that a pawn (here white king's pawn) can move 2 square forward on first move.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(E2, WHITE, PAWN);
|
|
|
|
let moves = get_piece_moves(&b, &gs, E2, WHITE, true);
|
2020-05-31 02:41:02 +02:00
|
|
|
assert_eq!(moves.len(), 2);
|
2020-06-19 19:29:10 +02:00
|
|
|
assert!(moves.contains(&Move::new(E2, E3)));
|
|
|
|
assert!(moves.contains(&Move::new(E2, E4)));
|
2020-05-31 02:41:02 +02:00
|
|
|
|
|
|
|
// Check that a pawn cannot move forward if a piece is blocking its path.
|
2020-06-04 18:58:00 +02:00
|
|
|
// 1. black pawn 2 square forward; only 1 square forward available from start pos.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(E4, BLACK, PAWN);
|
|
|
|
let moves = get_piece_moves(&b, &gs, E2, WHITE, true);
|
|
|
|
assert!(moves.len() == 1 && moves.contains(&Move::new(E2, E3)));
|
2020-06-04 18:58:00 +02:00
|
|
|
// 2. black pawn 1 square forward; no square available.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(E3, BLACK, PAWN);
|
|
|
|
let moves = get_piece_moves(&b, &gs, E2, WHITE, true);
|
2020-05-31 02:41:02 +02:00
|
|
|
assert_eq!(moves.len(), 0);
|
2020-06-04 18:58:00 +02:00
|
|
|
// 3. remove the e4 black pawn; the white pawn should not be able to jump above e3 pawn.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.clear_square(E4);
|
|
|
|
let moves = get_piece_moves(&b, &gs, E2, WHITE, true);
|
2020-06-04 18:58:00 +02:00
|
|
|
assert_eq!(moves.len(), 0);
|
2020-05-31 02:41:02 +02:00
|
|
|
|
|
|
|
// Check that a pawn can take a piece diagonally.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(F3, BLACK, PAWN);
|
|
|
|
let moves = get_piece_moves(&b, &gs, E2, WHITE, true);
|
|
|
|
assert!(moves.len() == 1 && moves.contains(&Move::new(E2, F3)));
|
|
|
|
b.set_square(D3, BLACK, PAWN);
|
|
|
|
let moves = get_piece_moves(&b, &gs, E2, WHITE, true);
|
2020-05-31 02:41:02 +02:00
|
|
|
assert_eq!(moves.len(), 2);
|
2020-06-19 19:29:10 +02:00
|
|
|
assert!(moves.contains( &Move::new(E2, F3) ));
|
|
|
|
assert!(moves.contains( &Move::new(E2, D3) ));
|
2020-06-04 19:19:24 +02:00
|
|
|
|
|
|
|
// Check that a pawn moving to the last rank leads to queen promotion.
|
|
|
|
// 1. by simply moving forward.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(A7, WHITE, PAWN);
|
|
|
|
let moves = get_piece_moves(&b, &gs, A7, WHITE, true);
|
|
|
|
assert!(moves.len() == 1 && moves.contains(&Move::new_promotion(A7, A8, QUEEN)));
|
2020-05-31 02:41:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2020-05-31 19:00:56 +02:00
|
|
|
fn test_get_bishop_moves() {
|
2020-06-19 19:29:10 +02:00
|
|
|
let mut b = Board::new_empty();
|
2020-06-06 00:47:07 +02:00
|
|
|
let gs = GameState::new();
|
2020-05-31 02:41:02 +02:00
|
|
|
|
|
|
|
// A bishop has maximum range when it's in a center square.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(D4, WHITE, BISHOP);
|
|
|
|
let moves = get_piece_moves(&b, &gs, D4, WHITE, true);
|
2020-05-31 02:41:02 +02:00
|
|
|
assert_eq!(moves.len(), 13);
|
|
|
|
// Going top-right.
|
2020-06-19 19:29:10 +02:00
|
|
|
assert!(moves.contains(&Move::new(D4, E5)));
|
|
|
|
assert!(moves.contains(&Move::new(D4, F6)));
|
|
|
|
assert!(moves.contains(&Move::new(D4, G7)));
|
|
|
|
assert!(moves.contains(&Move::new(D4, H8)));
|
2020-05-31 02:41:02 +02:00
|
|
|
// Going bottom-right.
|
2020-06-19 19:29:10 +02:00
|
|
|
assert!(moves.contains(&Move::new(D4, E3)));
|
|
|
|
assert!(moves.contains(&Move::new(D4, F2)));
|
|
|
|
assert!(moves.contains(&Move::new(D4, G1)));
|
2020-05-31 02:41:02 +02:00
|
|
|
// Going bottom-left.
|
2020-06-19 19:29:10 +02:00
|
|
|
assert!(moves.contains(&Move::new(D4, C3)));
|
|
|
|
assert!(moves.contains(&Move::new(D4, B2)));
|
|
|
|
assert!(moves.contains(&Move::new(D4, A1)));
|
2020-05-31 02:41:02 +02:00
|
|
|
// Going top-left.
|
2020-06-19 19:29:10 +02:00
|
|
|
assert!(moves.contains(&Move::new(D4, C5)));
|
|
|
|
assert!(moves.contains(&Move::new(D4, B6)));
|
|
|
|
assert!(moves.contains(&Move::new(D4, A7)));
|
2020-05-31 02:41:02 +02:00
|
|
|
|
2020-06-06 00:47:07 +02:00
|
|
|
// When blocking commit to one square with friendly piece, lose 2 moves.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(B2, WHITE, PAWN);
|
|
|
|
assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 11);
|
2020-05-31 02:41:02 +02:00
|
|
|
|
2020-06-06 00:47:07 +02:00
|
|
|
// When blocking commit to one square with enemy piece, lose only 1 move.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(B2, BLACK, PAWN);
|
|
|
|
assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 12);
|
2020-05-31 02:41:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2020-05-31 19:00:56 +02:00
|
|
|
fn test_get_knight_moves() {
|
2020-06-19 19:29:10 +02:00
|
|
|
let mut b = Board::new_empty();
|
2020-06-06 00:47:07 +02:00
|
|
|
let gs = GameState::new();
|
2020-05-31 02:41:02 +02:00
|
|
|
|
2020-06-06 00:47:07 +02:00
|
|
|
// A knight never has blocked commit; if it's in the center of the board, it can have up to
|
2020-05-31 02:41:02 +02:00
|
|
|
// 8 moves.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(D4, WHITE, KNIGHT);
|
|
|
|
assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 8);
|
2020-05-31 02:41:02 +02:00
|
|
|
|
|
|
|
// If on a side if has only 4 moves.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(A4, WHITE, KNIGHT);
|
|
|
|
assert_eq!(get_piece_moves(&b, &gs, A4, WHITE, true).len(), 4);
|
2020-05-31 02:41:02 +02:00
|
|
|
|
|
|
|
// And in a corner, only 2 moves.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(A1, WHITE, KNIGHT);
|
|
|
|
assert_eq!(get_piece_moves(&b, &gs, A1, WHITE, true).len(), 2);
|
2020-05-31 02:41:02 +02:00
|
|
|
|
|
|
|
// Add 2 friendly pieces and it is totally blocked.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(B3, WHITE, PAWN);
|
|
|
|
b.set_square(C2, WHITE, PAWN);
|
|
|
|
assert_eq!(get_piece_moves(&b, &gs, A1, WHITE, true).len(), 0);
|
2020-05-31 19:00:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_get_rook_moves() {
|
2020-06-19 19:29:10 +02:00
|
|
|
let mut b = Board::new_empty();
|
2020-06-06 00:47:07 +02:00
|
|
|
let gs = GameState::new();
|
2020-05-31 19:00:56 +02:00
|
|
|
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(D4, WHITE, ROOK);
|
|
|
|
assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 14);
|
|
|
|
b.set_square(D6, BLACK, PAWN);
|
|
|
|
assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 12);
|
|
|
|
b.set_square(D6, WHITE, PAWN);
|
|
|
|
assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 11);
|
2020-05-31 19:00:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_get_queen_moves() {
|
2020-06-19 19:29:10 +02:00
|
|
|
let mut b = Board::new_empty();
|
2020-06-06 00:47:07 +02:00
|
|
|
let gs = GameState::new();
|
2020-05-31 19:00:56 +02:00
|
|
|
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(D4, WHITE, QUEEN);
|
|
|
|
assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 14 + 13);
|
2020-05-31 02:41:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2020-05-31 19:00:56 +02:00
|
|
|
fn test_get_king_moves() {
|
2020-06-06 00:47:07 +02:00
|
|
|
let mut gs = GameState::new();
|
2020-05-31 02:41:02 +02:00
|
|
|
|
2020-06-06 00:47:07 +02:00
|
|
|
// King can move 1 square in any direction.
|
2020-06-19 19:29:10 +02:00
|
|
|
let mut b = Board::new_empty();
|
|
|
|
b.set_square(D4, WHITE, KING);
|
|
|
|
assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 8);
|
|
|
|
b.set_square(E5, WHITE, PAWN);
|
|
|
|
assert_eq!(get_piece_moves(&b, &gs, D4, WHITE, true).len(), 7);
|
2020-06-06 00:47:07 +02:00
|
|
|
|
|
|
|
// If castling is available, other moves are possible: 5 moves + 2 castles.
|
2020-06-19 19:29:10 +02:00
|
|
|
let mut b = Board::new_empty();
|
|
|
|
b.set_square(E1, WHITE, KING);
|
|
|
|
b.set_square(A1, WHITE, ROOK);
|
|
|
|
b.set_square(H1, WHITE, ROOK);
|
|
|
|
assert_eq!(get_piece_moves(&b, &gs, E1, WHITE, true).len(), 5 + 2);
|
2020-06-06 00:47:07 +02:00
|
|
|
|
|
|
|
// Castling works as well for black.
|
2020-06-19 19:29:10 +02:00
|
|
|
gs.color = BLACK;
|
|
|
|
b.set_square(E8, BLACK, KING);
|
|
|
|
b.set_square(A8, BLACK, ROOK);
|
|
|
|
b.set_square(H8, BLACK, ROOK);
|
|
|
|
assert_eq!(get_piece_moves(&b, &gs, E8, BLACK, true).len(), 5 + 2);
|
2020-05-31 02:41:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2020-05-31 19:00:56 +02:00
|
|
|
fn test_filter_illegal_moves() {
|
2020-06-19 19:29:10 +02:00
|
|
|
let mut b = Board::new_empty();
|
2020-06-06 01:27:19 +02:00
|
|
|
let mut gs = GameState::new();
|
2020-05-31 02:41:02 +02:00
|
|
|
|
2020-05-31 19:00:56 +02:00
|
|
|
// Place white's king on first rank.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(E1, WHITE, KING);
|
2020-05-31 19:00:56 +02:00
|
|
|
// Place black rook in second rank: king can only move left or right.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(H2, BLACK, ROOK);
|
2020-06-06 01:27:19 +02:00
|
|
|
// No castling available.
|
|
|
|
gs.castling = 0;
|
|
|
|
// 5 moves in absolute but only 2 are legal.
|
2020-06-19 19:29:10 +02:00
|
|
|
let all_wh_moves = get_piece_moves(&b, &gs, E1, WHITE, true);
|
2020-06-06 01:27:19 +02:00
|
|
|
assert_eq!(all_wh_moves.len(), 2);
|
2020-05-31 02:41:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2020-05-31 19:00:56 +02:00
|
|
|
fn test_is_attacked() {
|
2020-06-19 19:29:10 +02:00
|
|
|
let mut b = Board::new_empty();
|
2020-06-06 00:47:07 +02:00
|
|
|
let gs = GameState::new();
|
2020-05-31 02:41:02 +02:00
|
|
|
|
2020-05-31 19:00:56 +02:00
|
|
|
// Place a black rook in white pawn's file.
|
2020-06-19 19:29:10 +02:00
|
|
|
b.set_square(D4, WHITE, PAWN);
|
|
|
|
b.set_square(D6, BLACK, ROOK);
|
|
|
|
assert!(is_attacked(&b, &gs, D4));
|
2020-05-31 19:00:56 +02:00
|
|
|
// Move the rook on another file, no more attack.
|
2020-06-19 19:29:10 +02:00
|
|
|
Move::new(D6, E6).apply_to_board(&mut b);
|
|
|
|
assert!(!is_attacked(&b, &gs, D4));
|
2020-05-31 02:41:02 +02:00
|
|
|
}
|
|
|
|
}
|