This repository has been archived on 2023-03-04. You can view files and clone it, but cannot push or open issues or pull requests.
Vatu/src/rules.rs

841 lines
30 KiB
Rust
Raw Normal View History

2020-05-31 02:41:02 +02:00
//! Functions to determine legal moves.
use crate::board::*;
2020-06-11 19:33:18 +02:00
use crate::notation;
2020-05-31 02:41:02 +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.
#[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,
}
}
}
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: {}",
self.color, self.castling, notation::en_passant_to_string(self.en_passant),
self.halfmove, self.fullmove
)
}
}
2020-06-06 00:47:07 +02:00
pub const CASTLING_WH_K: u8 = 0b00000001;
pub const CASTLING_WH_Q: u8 = 0b00000010;
pub const CASTLING_WH_MASK: u8 = 0b00000011;
pub const CASTLING_BL_K: u8 = 0b00000100;
pub const CASTLING_BL_Q: u8 = 0b00001000;
pub const CASTLING_BL_MASK: u8 = 0b00001100;
pub const CASTLING_K_MASK: u8 = 0b00000101;
pub const CASTLING_Q_MASK: u8 = 0b00001010;
pub const CASTLING_MASK: u8 = 0b00001111;
2020-06-11 19:32:47 +02:00
/// Castling sides parameters.
///
/// For both sides, the 3-uple contains files that should be empty
/// and not attacked, an optional file that should be empty for
/// queen-side, and the castling side-mask.
pub const CASTLING_SIDES: [([i8; 2], Option<i8>, u8); 2] =
[([5i8, 6i8], None, CASTLING_K_MASK), ([3i8, 2i8], Some(1i8), CASTLING_Q_MASK)];
2020-06-05 09:21:33 +02:00
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)
}
2020-06-06 00:47:07 +02:00
/// Update `board` and `game_state` to reflect the move `m`.
///
/// The board is updated with correct piece placement.
///
/// The game state is updated with the new player turn and the new
/// castling options.
pub fn apply_move_to(board: &mut Board, game_state: &mut GameState, m: &Move) {
2020-06-06 00:47:07 +02:00
apply_move_to_board(board, m);
apply_move_to_state(game_state, m);
// If the move is a castle, remove it from castling options.
2020-06-06 00:47:07 +02:00
if let Some(castle) = get_castle(m) {
match castle {
CASTLING_WH_K | CASTLING_WH_Q => game_state.castling &= !CASTLING_WH_MASK,
CASTLING_BL_K | CASTLING_BL_Q => game_state.castling &= !CASTLING_BL_MASK,
_ => {}
};
}
// Else, check if it's either a rook or the king that moved.
else {
let piece = get_square(board, &m.1);
if is_white(piece) && game_state.castling & CASTLING_WH_MASK != 0 {
match get_type(piece) {
SQ_K => {
if m.0 == pos("e1") {
game_state.castling &= !CASTLING_WH_MASK;
}
}
SQ_R => {
if m.0 == pos("a1") {
game_state.castling &= !CASTLING_WH_Q;
} else if m.0 == pos("h1") {
game_state.castling &= !CASTLING_WH_K;
}
}
_ => {}
}
} else if is_black(piece) && game_state.castling & CASTLING_BL_MASK != 0 {
match get_type(piece) {
SQ_K => {
if m.0 == pos("e8") {
game_state.castling &= !CASTLING_BL_MASK;
}
}
SQ_R => {
if m.0 == pos("a8") {
game_state.castling &= !CASTLING_BL_Q;
} else if m.0 == pos("h8") {
game_state.castling &= !CASTLING_BL_K;
}
}
_ => {}
}
}
}
}
/// Apply a move `m` into `board`.
2020-06-06 00:47:07 +02:00
pub fn apply_move_to_board(board: &mut Board, m: &Move) {
if let Some(castle) = get_castle(m) {
match castle {
CASTLING_WH_K => {
move_piece(board, &START_WH_K_POS, &pos("g1"));
move_piece(board, &pos("h1"), &pos("f1"));
}
CASTLING_WH_Q => {
move_piece(board, &START_WH_K_POS, &pos("c1"));
move_piece(board, &pos("a1"), &pos("d1"));
}
CASTLING_BL_K => {
move_piece(board, &START_BL_K_POS, &pos("g8"));
move_piece(board, &pos("h8"), &pos("f8"));
}
CASTLING_BL_Q => {
move_piece(board, &START_BL_K_POS, &pos("c8"));
move_piece(board, &pos("a8"), &pos("d8"));
}
_ => {}
}
} else {
move_piece(board, &m.0, &m.1);
if let Some(prom_type) = m.2 {
let color = get_color(get_square(board, &m.1));
set_square(board, &m.1, color|prom_type);
}
2020-06-06 00:47:07 +02:00
}
}
2020-06-06 00:47:07 +02:00
/// Update `game_state` with the move `m`.
///
/// This only updates the player turn. Castling should be updated in a
/// context where the corresponding board is available.
pub fn apply_move_to_state(game_state: &mut GameState, _m: &Move) {
game_state.color = opposite(game_state.color);
}
2020-06-06 00:47:07 +02:00
/// Get the corresponding castling flag for this move.
pub fn get_castle(m: &Move) -> Option<u8> {
if m.0 == pos("e1") {
if m.1 == pos("c1") {
Some(CASTLING_WH_Q)
} else if m.1 == pos("g1") {
Some(CASTLING_WH_K)
} else {
None
}
} else if m.0 == pos("e8") {
if m.1 == pos("c8") {
Some(CASTLING_BL_Q)
} else if m.1 == pos("g8") {
Some(CASTLING_BL_K)
} else {
None
}
} else {
None
}
2020-06-01 00:43:05 +02:00
}
2020-06-06 00:47:07 +02:00
/// Get the move for this castle.
pub fn get_castle_move(castle: u8) -> Move {
match castle {
CASTLING_WH_Q => (pos("e1"), pos("c1"), None),
CASTLING_WH_K => (pos("e1"), pos("g1"), None),
CASTLING_BL_Q => (pos("e8"), pos("c8"), None),
CASTLING_BL_K => (pos("e8"), pos("g8"), None),
_ => panic!("Illegal castling requested: {:08b}", castle),
}
}
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.
pub fn get_player_moves(board: &Board, game_state: &GameState, commit: bool) -> Vec<Move> {
2020-05-31 02:41:02 +02:00
let mut moves = vec!();
for r in 0..8 {
for f in 0..8 {
2020-05-31 19:00:56 +02:00
let p = (f, r);
if is_empty(board, &p) {
continue
}
if is_color(get_square(board, &p), game_state.color) {
2020-06-06 00:47:07 +02:00
moves.append(&mut get_piece_moves(board, &p, game_state, commit));
2020-05-31 02:41:02 +02:00
}
}
}
moves
}
2020-05-31 19:00:56 +02:00
/// Get a list of moves for the piece at position `at`.
2020-06-06 00:47:07 +02:00
pub fn get_piece_moves(board: &Board, at: &Pos, game_state: &GameState, commit: bool) -> Vec<Move> {
2020-05-31 02:41:02 +02:00
match get_square(board, at) {
2020-06-06 00:47:07 +02:00
p if is_piece(p, SQ_P) => get_pawn_moves(board, at, p, game_state, commit),
p if is_piece(p, SQ_B) => get_bishop_moves(board, at, p, game_state, commit),
p if is_piece(p, SQ_N) => get_knight_moves(board, at, p, game_state, commit),
p if is_piece(p, SQ_R) => get_rook_moves(board, at, p, game_state, commit),
p if is_piece(p, SQ_Q) => get_queen_moves(board, at, p, game_state, commit),
p if is_piece(p, SQ_K) => get_king_moves(board, at, p, game_state, commit),
2020-05-31 02:41:02 +02:00
_ => vec!(),
}
}
2020-06-06 00:47:07 +02:00
fn get_pawn_moves(
board: &Board,
at: &Pos,
piece: u8,
game_state: &GameState,
commit: bool,
) -> Vec<Move> {
2020-05-31 19:00:56 +02:00
let (f, r) = *at;
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.
let dir: i8 = if is_white(piece) { 1 } else { -1 };
2020-05-31 02:41:02 +02:00
// Check 1 or 2 square forward.
let move_len = if (is_white(piece) && r == 1) || (is_black(piece) && r == 6) { 2 } else { 1 };
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
}
let forward: Pos = (f, forward_r);
// If forward square is empty (and we are not jumping over an occupied square), add it.
2020-06-04 19:19:24 +02:00
if is_empty(board, &forward) && (i == 1 || is_empty(board, &(f, forward_r - dir))) {
// Pawns that get to the opposite rank automatically promote as queens.
let prom = if (dir > 0 && forward_r == POS_MAX) || (dir < 0 && forward_r == POS_MIN) {
Some(SQ_Q)
} else {
None
};
2020-06-06 00:47:07 +02:00
let m = (*at, forward, prom);
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-05-31 02:41:02 +02:00
let df = f - 1;
if df >= POS_MIN {
let diag: Pos = (df, forward_r);
2020-05-31 19:00:56 +02:00
if let Some(m) = move_on_enemy(piece, at, get_square(board, &diag), &diag) {
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
}
}
2020-06-06 00:47:07 +02:00
// Second diagonal.
2020-05-31 02:41:02 +02:00
let df = f + 1;
if df <= POS_MAX {
let diag: Pos = (df, forward_r);
2020-05-31 19:00:56 +02:00
if let Some(m) = move_on_enemy(piece, at, get_square(board, &diag), &diag) {
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
}
}
}
// TODO en passant
}
moves
}
2020-06-06 00:47:07 +02:00
fn get_bishop_moves(
board: &Board,
at: &Pos,
piece: u8,
game_state: &GameState,
commit: bool,
) -> Vec<Move> {
2020-05-31 02:41:02 +02:00
let (f, r) = at;
2020-06-06 00:47:07 +02:00
let mut views = [true; 4]; // Store diagonals where a piece blocks commit.
2020-05-31 02:41:02 +02:00
let mut moves = vec!();
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
}
let p = (f + offset.0 * dist, r + offset.1 * dist);
if !is_valid_pos(p) {
continue
}
2020-05-31 19:00:56 +02:00
if is_empty(board, &p) {
2020-06-06 00:47:07 +02:00
let m = (*at, p, None);
if can_register(commit, board, game_state, &m) {
moves.push(m);
}
2020-05-31 02:41:02 +02:00
} else {
2020-05-31 19:00:56 +02:00
if let Some(m) = move_on_enemy(piece, at, get_square(board, &p), &p) {
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
}
2020-06-06 00:47:07 +02:00
views[dir] = false; // Stop looking in that direction.
2020-05-31 02:41:02 +02:00
}
}
}
moves
}
2020-06-06 00:47:07 +02:00
fn get_knight_moves(
board: &Board,
at: &Pos,
piece: u8,
game_state: &GameState,
commit: bool,
) -> Vec<Move> {
2020-05-31 02:41:02 +02:00
let (f, r) = at;
let mut moves = vec!();
for offset in [(1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2)].iter() {
let p = (f + offset.0, r + offset.1);
if !is_valid_pos(p) {
continue
}
2020-05-31 19:00:56 +02:00
if is_empty(board, &p) {
2020-06-06 00:47:07 +02:00
let m = (*at, p, None);
if can_register(commit, board, game_state, &m) {
moves.push(m);
}
2020-05-31 19:00:56 +02:00
} else if let Some(m) = move_on_enemy(piece, at, get_square(board, &p), &p) {
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
}
}
moves
}
2020-06-06 00:47:07 +02:00
fn get_rook_moves(
board: &Board,
at: &Pos,
piece: u8,
game_state: &GameState,
commit: bool,
) -> Vec<Move> {
2020-05-31 02:41:02 +02:00
let (f, r) = at;
let mut moves = vec!();
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
}
let p = (f + offset.0 * dist, r + offset.1 * dist);
if !is_valid_pos(p) {
continue
}
2020-05-31 19:00:56 +02:00
if is_empty(board, &p) {
2020-06-06 00:47:07 +02:00
let m = (*at, p, None);
if can_register(commit, board, game_state, &m) {
moves.push(m);
}
2020-05-31 02:41:02 +02:00
} else {
2020-05-31 19:00:56 +02:00
if let Some(m) = move_on_enemy(piece, at, get_square(board, &p), &p) {
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
}
2020-06-06 00:47:07 +02:00
views[dir] = false; // Stop looking in that direction.
2020-05-31 02:41:02 +02:00
}
}
}
moves
}
2020-06-06 00:47:07 +02:00
fn get_queen_moves(
board: &Board,
at: &Pos,
piece: u8,
game_state: &GameState,
commit: bool
) -> Vec<Move> {
2020-05-31 02:41:02 +02:00
let mut moves = vec!();
// Easy way to get queen moves, but may be a bit quicker if everything was rewritten here.
2020-06-06 00:47:07 +02:00
moves.append(&mut get_bishop_moves(board, at, piece, game_state, commit));
moves.append(&mut get_rook_moves(board, at, piece, game_state, commit));
2020-05-31 02:41:02 +02:00
moves
}
2020-06-06 00:47:07 +02:00
fn get_king_moves(
board: &Board,
at: &Pos,
piece: u8,
game_state: &GameState,
commit: bool
) -> Vec<Move> {
2020-05-31 02:41:02 +02:00
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() {
let p = (f + offset.0, r + offset.1);
if !is_valid_pos(p) {
continue
}
2020-05-31 19:00:56 +02:00
if is_empty(board, &p) {
2020-06-06 00:47:07 +02:00
let m = (*at, p, None);
if can_register(commit, board, game_state, &m) {
moves.push(m);
}
2020-05-31 19:00:56 +02:00
} else if let Some(m) = move_on_enemy(piece, at, get_square(board, &p), &p) {
2020-06-06 00:47:07 +02:00
if can_register(commit, board, game_state, &m) {
moves.push(m);
}
}
}
// 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.
let (castling_rank, castling_color_mask) = if is_white(game_state.color) {
(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-06 01:27:19 +02:00
*r == castling_rank &&
!is_attacked(board, game_state, at)
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 {
let p = (*path_f, castling_rank);
if !is_empty(board, &p) || is_illegal(board, game_state, &(*at, p, None)) {
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 {
let p = (*rook_path_f, castling_rank);
if !is_empty(board, &p) {
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;
let m = get_castle_move(castle);
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-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-05-31 02:41:02 +02:00
/// Return a move from pos1 to pos2 if piece1 & piece2 are enemies.
2020-05-31 19:00:56 +02:00
fn move_on_enemy(piece1: u8, pos1: &Pos, piece2: u8, pos2: &Pos) -> Option<Move> {
let color1 = get_color(piece1);
if is_color(piece2, opposite(color1)) {
2020-06-04 19:19:24 +02:00
// Automatic queen promotion for pawns moving to the opposite rank.
let prom = if
is_piece(piece1, SQ_P) &&
((is_white(piece1) && pos2.1 == POS_MAX) || (is_black(piece1) && pos2.1 == POS_MIN))
{
Some(SQ_Q)
} else {
None
};
Some((*pos1, *pos2, prom))
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 {
if let Some(king_p) = find_king(board, game_state.color) {
// Rule 1: a move is illegal if the king ends up in check.
// If king moves, use its new position.
2020-05-31 19:00:56 +02:00
let king_p = if m.0 == king_p { m.1 } else { king_p };
2020-06-06 00:47:07 +02:00
let mut hypothetic_board = board.clone();
apply_move_to_board(&mut hypothetic_board, m);
2020-05-31 19:00:56 +02:00
// Check if the move makes the player king in check.
2020-06-06 00:47:07 +02:00
if is_attacked(&hypothetic_board, &game_state, &king_p) {
return true
}
}
false
2020-05-31 19:00:56 +02:00
}
/// Return true if the piece at position `at` is attacked.
///
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.
///
/// 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
2020-06-06 00:47:07 +02:00
/// tell that it is white turn. If the square at `at` is empty, simply
/// check if it is getting attacked by the opposite player.
fn is_attacked(board: &Board, game_state: &GameState, at: &Pos) -> 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() {
if *at == m.1 {
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
use crate::notation::parse_move;
#[test]
fn test_get_castle() {
2020-06-06 01:27:19 +02:00
assert_eq!(get_castle(&parse_move("e1c1")), Some(CASTLING_WH_Q));
assert_eq!(get_castle(&parse_move("e1g1")), Some(CASTLING_WH_K));
assert_eq!(get_castle(&parse_move("e8c8")), Some(CASTLING_BL_Q));
assert_eq!(get_castle(&parse_move("e8g8")), Some(CASTLING_BL_K));
2020-06-06 00:47:07 +02:00
assert_eq!(get_castle(&parse_move("d2d4")), None);
}
2020-05-31 02:41:02 +02:00
#[test]
fn test_apply_move_to_board() {
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.
2020-06-06 00:47:07 +02:00
apply_move_to_board(&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
2020-06-06 00:47:07 +02:00
apply_move_to_board(&mut b, &(pos("f4"), pos("e6"), None));
assert_eq!(get_square(&b, &pos("e6")), SQ_BL_N);
assert_eq!(num_pieces(&b), 1);
}
2020-06-06 00:47:07 +02:00
#[test]
fn test_apply_move_to_castling() {
let mut b = new();
let mut gs = GameState::new();
assert_eq!(gs.castling, CASTLING_MASK);
// On a starting board, start by making place for all castles.
clear_square(&mut b, &pos("b1"));
clear_square(&mut b, &pos("c1"));
clear_square(&mut b, &pos("d1"));
clear_square(&mut b, &pos("f1"));
clear_square(&mut b, &pos("g1"));
clear_square(&mut b, &pos("b8"));
clear_square(&mut b, &pos("c8"));
clear_square(&mut b, &pos("d8"));
clear_square(&mut b, &pos("f8"));
clear_square(&mut b, &pos("g8"));
// White queen-side castling.
2020-06-06 01:27:19 +02:00
apply_move_to(&mut b, &mut gs, &parse_move("e1c1"));
2020-06-06 00:47:07 +02:00
assert!(is_piece(get_square(&b, &pos("c1")), SQ_WH_K));
assert!(is_piece(get_square(&b, &pos("d1")), SQ_WH_R));
assert!(is_empty(&b, &pos("a1")));
assert!(is_empty(&b, &pos("e1")));
assert_eq!(gs.castling, CASTLING_BL_MASK);
// Black king-side castling.
2020-06-06 01:27:19 +02:00
apply_move_to(&mut b, &mut gs, &parse_move("e8g8"));
2020-06-06 00:47:07 +02:00
assert!(is_piece(get_square(&b, &pos("g8")), SQ_BL_K));
assert!(is_piece(get_square(&b, &pos("f8")), SQ_BL_R));
assert!(is_empty(&b, &pos("h8")));
assert!(is_empty(&b, &pos("e8")));
assert_eq!(gs.castling, 0);
}
2020-05-31 02:41:02 +02:00
#[test]
2020-05-31 19:00:56 +02:00
fn test_get_player_moves() {
2020-05-31 02:41:02 +02:00
let b = new();
2020-06-06 00:47:07 +02:00
let gs = GameState::new();
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-05-31 02:41:02 +02:00
let mut b = 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-05-31 19:00:56 +02:00
set_square(&mut b, &pos("d3"), SQ_WH_P);
2020-06-06 00:47:07 +02:00
let moves = get_piece_moves(&b, &pos("d3"), &gs, true);
assert!(moves.len() == 1 && moves.contains( &parse_move("d3d4") ));
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-05-31 19:00:56 +02:00
set_square(&mut b, &pos("e2"), SQ_WH_P);
2020-06-06 00:47:07 +02:00
let moves = get_piece_moves(&b, &pos("e2"), &gs, true);
2020-05-31 02:41:02 +02:00
assert_eq!(moves.len(), 2);
2020-06-06 00:47:07 +02:00
assert!(moves.contains( &parse_move("e2e3") ));
assert!(moves.contains( &parse_move("e2e4") ));
2020-05-31 02:41:02 +02:00
// 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.
2020-05-31 19:00:56 +02:00
set_square(&mut b, &pos("e4"), SQ_BL_P);
2020-06-06 00:47:07 +02:00
let moves = get_piece_moves(&b, &pos("e2"), &gs, true);
assert!(moves.len() == 1 && moves.contains( &parse_move("e2e3") ));
// 2. black pawn 1 square forward; no square available.
2020-05-31 19:00:56 +02:00
set_square(&mut b, &pos("e3"), SQ_BL_P);
2020-06-06 00:47:07 +02:00
let moves = get_piece_moves(&b, &pos("e2"), &gs, true);
2020-05-31 02:41:02 +02:00
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"));
2020-06-06 00:47:07 +02:00
let moves = get_piece_moves(&b, &pos("e2"), &gs, true);
assert_eq!(moves.len(), 0);
2020-05-31 02:41:02 +02:00
// Check that a pawn can take a piece diagonally.
2020-05-31 19:00:56 +02:00
set_square(&mut b, &pos("f3"), SQ_BL_P);
2020-06-06 00:47:07 +02:00
let moves = get_piece_moves(&b, &pos("e2"), &gs, true);
assert!(moves.len() == 1 && moves.contains( &parse_move("e2f3") ));
2020-05-31 19:00:56 +02:00
set_square(&mut b, &pos("d3"), SQ_BL_P);
2020-06-06 00:47:07 +02:00
let moves = get_piece_moves(&b, &pos("e2"), &gs, true);
2020-05-31 02:41:02 +02:00
assert_eq!(moves.len(), 2);
2020-06-06 00:47:07 +02:00
assert!(moves.contains( &parse_move("e2f3") ));
assert!(moves.contains( &parse_move("e2d3") ));
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.
set_square(&mut b, &pos("a7"), SQ_WH_P);
2020-06-06 00:47:07 +02:00
let moves = get_piece_moves(&b, &pos("a7"), &gs, true);
assert!(moves.len() == 1 && moves.contains( &parse_move("a7a8q") ));
2020-05-31 02:41:02 +02:00
}
#[test]
2020-05-31 19:00:56 +02:00
fn test_get_bishop_moves() {
2020-05-31 02:41:02 +02:00
let mut b = 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-05-31 19:00:56 +02:00
set_square(&mut b, &pos("d4"), SQ_WH_B);
2020-06-06 00:47:07 +02:00
let moves = get_piece_moves(&b, &pos("d4"), &gs, true);
2020-05-31 02:41:02 +02:00
assert_eq!(moves.len(), 13);
// Going top-right.
2020-06-06 00:47:07 +02:00
assert!(moves.contains( &parse_move("d4e5") ));
assert!(moves.contains( &parse_move("d4f6") ));
assert!(moves.contains( &parse_move("d4g7") ));
assert!(moves.contains( &parse_move("d4h8") ));
2020-05-31 02:41:02 +02:00
// Going bottom-right.
2020-06-06 00:47:07 +02:00
assert!(moves.contains( &parse_move("d4e3") ));
assert!(moves.contains( &parse_move("d4f2") ));
assert!(moves.contains( &parse_move("d4g1") ));
2020-05-31 02:41:02 +02:00
// Going bottom-left.
2020-06-06 00:47:07 +02:00
assert!(moves.contains( &parse_move("d4c3") ));
assert!(moves.contains( &parse_move("d4b2") ));
assert!(moves.contains( &parse_move("d4a1") ));
2020-05-31 02:41:02 +02:00
// Going top-left.
2020-06-06 00:47:07 +02:00
assert!(moves.contains( &parse_move("d4c5") ));
assert!(moves.contains( &parse_move("d4b6") ));
assert!(moves.contains( &parse_move("d4a7") ));
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-05-31 19:00:56 +02:00
set_square(&mut b, &pos("b2"), SQ_WH_P);
2020-06-06 00:47:07 +02:00
assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, 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-05-31 19:00:56 +02:00
set_square(&mut b, &pos("b2"), SQ_BL_P);
2020-06-06 00:47:07 +02:00
assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, 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-05-31 02:41:02 +02:00
let mut b = 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-05-31 19:00:56 +02:00
set_square(&mut b, &pos("d4"), SQ_WH_N);
2020-06-06 00:47:07 +02:00
assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, true).len(), 8);
2020-05-31 02:41:02 +02:00
// If on a side if has only 4 moves.
2020-05-31 19:00:56 +02:00
set_square(&mut b, &pos("a4"), SQ_WH_N);
2020-06-06 00:47:07 +02:00
assert_eq!(get_piece_moves(&b, &pos("a4"), &gs, true).len(), 4);
2020-05-31 02:41:02 +02:00
// And in a corner, only 2 moves.
2020-05-31 19:00:56 +02:00
set_square(&mut b, &pos("a1"), SQ_WH_N);
2020-06-06 00:47:07 +02:00
assert_eq!(get_piece_moves(&b, &pos("a1"), &gs, true).len(), 2);
2020-05-31 02:41:02 +02:00
// Add 2 friendly pieces and it is totally blocked.
2020-05-31 19:00:56 +02:00
set_square(&mut b, &pos("b3"), SQ_WH_P);
set_square(&mut b, &pos("c2"), SQ_WH_P);
2020-06-06 00:47:07 +02:00
assert_eq!(get_piece_moves(&b, &pos("a1"), &gs, true).len(), 0);
2020-05-31 19:00:56 +02:00
}
#[test]
fn test_get_rook_moves() {
let mut b = new_empty();
2020-06-06 00:47:07 +02:00
let gs = GameState::new();
2020-05-31 19:00:56 +02:00
set_square(&mut b, &pos("d4"), SQ_WH_R);
2020-06-06 00:47:07 +02:00
assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, true).len(), 14);
2020-05-31 19:00:56 +02:00
set_square(&mut b, &pos("d6"), SQ_BL_P);
2020-06-06 00:47:07 +02:00
assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, true).len(), 12);
2020-05-31 19:00:56 +02:00
set_square(&mut b, &pos("d6"), SQ_WH_P);
2020-06-06 00:47:07 +02:00
assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, true).len(), 11);
2020-05-31 19:00:56 +02:00
}
#[test]
fn test_get_queen_moves() {
let mut b = new_empty();
2020-06-06 00:47:07 +02:00
let gs = GameState::new();
2020-05-31 19:00:56 +02:00
set_square(&mut b, &pos("d4"), SQ_WH_Q);
2020-06-06 00:47:07 +02:00
assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, 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.
let mut b = new_empty();
2020-05-31 19:00:56 +02:00
set_square(&mut b, &pos("d4"), SQ_WH_K);
2020-06-06 00:47:07 +02:00
assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, true).len(), 8);
2020-05-31 19:00:56 +02:00
set_square(&mut b, &pos("e5"), SQ_WH_P);
2020-06-06 00:47:07 +02:00
assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, true).len(), 7);
// If castling is available, other moves are possible: 5 moves + 2 castles.
let mut b = new_empty();
set_square(&mut b, &pos("e1"), SQ_WH_K);
set_square(&mut b, &pos("a1"), SQ_WH_R);
set_square(&mut b, &pos("h1"), SQ_WH_R);
assert_eq!(get_piece_moves(&b, &pos("e1"), &gs, true).len(), 5 + 2);
// Castling works as well for black.
gs.color = SQ_BL;
set_square(&mut b, &pos("e8"), SQ_BL_K);
set_square(&mut b, &pos("a8"), SQ_BL_R);
set_square(&mut b, &pos("h8"), SQ_BL_R);
assert_eq!(get_piece_moves(&b, &pos("e8"), &gs, 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-05-31 02:41:02 +02:00
let mut b = 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.
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);
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-06 00:47:07 +02:00
let all_wh_moves = get_piece_moves(&b, &pos("e1"), &gs, 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-05-31 02:41:02 +02:00
let mut b = 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.
set_square(&mut b, &pos("d4"), SQ_WH_P);
set_square(&mut b, &pos("d6"), SQ_BL_R);
2020-06-06 00:47:07 +02:00
assert!(is_attacked(&b, &gs, &pos("d4")));
2020-05-31 19:00:56 +02:00
// Move the rook on another file, no more attack.
2020-06-06 00:47:07 +02:00
apply_move_to_board(&mut b, &parse_move("d6e6"));
assert!(!is_attacked(&b, &gs, &pos("d4")));
2020-05-31 02:41:02 +02:00
}
}