diff --git a/d6h2.txt b/d6h2.txt index 98be814..2302936 100644 --- a/d6h2.txt +++ b/d6h2.txt @@ -1,6 +1,3 @@ uci -position startpos moves d2d4 b7b6 e2e4 d7d6 c1f4 f7f6 f1c4 g7g6 d4d5 b6b5 c4b5 -b8d7 g1f3 f6f5 e4f5 g6f5 d1d4 a7a6 d4d1 a6b5 f4d6 c7d6 d1d4 a8a2 a1a2 b5b4 d4b4 -f5f4 b4f4 d7f6 f4g5 f6d5 g5d5 e7e5 b1c3 e5e4 c3e4 h7h6 g2g4 h6h5 g4h5 h8h5 e4f6 -d8f6 a2a5 h5d5 a5d5 f6f3 e1g1 f3h1 g1h1 c8d7 f1e1 d7e6 d5d6 f8d6 e1e6 +position startpos moves d2d4 b7b6 e2e4 d7d6 c1f4 f7f6 f1c4 g7g6 d4d5 b6b5 c4b5 b8d7 g1f3 f6f5 e4f5 g6f5 d1d4 a7a6 d4d1 a6b5 f4d6 c7d6 d1d4 a8a2 a1a2 b5b4 d4b4 f5f4 b4f4 d7f6 f4g5 f6d5 g5d5 e7e5 b1c3 e5e4 c3e4 h7h6 g2g4 h6h5 g4h5 h8h5 e4f6 d8f6 a2a5 h5d5 a5d5 f6f3 e1g1 f3h1 g1h1 c8d7 f1e1 d7e6 d5d6 f8d6 e1e6 go diff --git a/src/board.rs b/src/board.rs index c4169c0..ab258a5 100644 --- a/src/board.rs +++ b/src/board.rs @@ -150,26 +150,38 @@ pub fn eq(b1: &Board, b2: &Board) -> bool { b1.iter().zip(b2.iter()).all(|(a, b)| a == b) } +/// Get value of the square at this position. #[inline] pub const fn get_square(board: &Board, coords: &Pos) -> u8 { board[(coords.0 * 8 + coords.1) as usize] } +/// Set a new value for the square at this position. #[inline] pub fn set_square(board: &mut Board, coords: &Pos, piece: u8) { board[(coords.0 * 8 + coords.1) as usize] = piece; } +/// Set the square empty at this position. #[inline] pub fn clear_square(board: &mut Board, coords: &Pos) { set_square(board, coords, SQ_E); } +/// Move a piece from a position to another, clearing initial square. +#[inline] +pub fn move_piece(board: &mut Board, from: &Pos, to: &Pos) { + set_square(board, &to, get_square(board, &from)); + clear_square(board, &from); +} + +/// Return true of the square at this position is empty. #[inline] pub const fn is_empty(board: &Board, coords: &Pos) -> bool { get_square(board, coords) == SQ_E } +/// Return an iterator over the pieces of the board along with pos. pub fn get_piece_iterator<'a>(board: &'a Board) -> Box + 'a> { Box::new( board.iter().enumerate() @@ -190,17 +202,16 @@ pub fn num_pieces(board: &Board) -> u8 { } /// Find the king of `color`. -pub fn find_king(board: &Board, color: u8) -> Pos { +pub fn find_king(board: &Board, color: u8) -> Option { for f in 0..8 { for r in 0..8 { let s = get_square(board, &(f, r)); if is_color(s, color) && is_piece(s, SQ_K) { - return (f, r) + return Some((f, r)) } } } - eprintln!("No king on board!"); - (0, 0) + None } /// Write a text view of the board. Used for debugging. @@ -441,9 +452,11 @@ mod tests { #[test] fn test_find_king() { + let b = new_empty(); + assert_eq!(find_king(&b, SQ_WH), None); let b = new(); - assert_eq!(find_king(&b, SQ_WH), pos("e1")); - assert_eq!(find_king(&b, SQ_BL), pos("e8")); + assert_eq!(find_king(&b, SQ_WH), Some(pos("e1"))); + assert_eq!(find_king(&b, SQ_BL), Some(pos("e8"))); } #[test] diff --git a/src/engine.rs b/src/engine.rs index 08142a3..f427e68 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -199,6 +199,7 @@ impl Engine { } fn apply_move(&mut self, m: &rules::Move) { + eprintln!("Applying {}...", notation::move_to_string(m)); rules::apply_move_to(&mut self.node.board, &mut self.node.game_state, m); } @@ -299,7 +300,7 @@ fn analyze( tx.send(Cmd::Log(draw_str)).unwrap(); } - let moves = rules::get_player_legal_moves(&node.board, &node.game_state); + let moves = rules::get_player_moves(&node.board, &node.game_state, true); if debug { let moves_str = format!("Legal moves: {}", notation::move_list_to_string(&moves)); tx.send(Cmd::Log(moves_str)).unwrap(); @@ -364,11 +365,7 @@ mod tests { let stats = board::compute_stats(&node.board); assert_eq!(evaluate(&stats), 0.0); - rules::apply_move_to_board( - &mut node.board, - &node.game_state, - ¬ation::parse_move("d2d4") - ); + rules::apply_move_to_board(&mut node.board, ¬ation::parse_move("d2d4")); let stats = board::compute_stats(&node.board); assert_eq!(evaluate(&stats), 0.0); } diff --git a/src/rules.rs b/src/rules.rs index 200e468..a7b04ac 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -6,6 +6,9 @@ use crate::board::*; /// /// It does not include various parameters such as clocks that are /// more aimed for engine analysis than typical rules checking. +/// +/// - `color`: current player's turn +/// - `castling`: which castling options are available; updated throughout the game. #[derive(Debug, Clone)] pub struct GameState { pub color: u8, @@ -27,11 +30,17 @@ impl GameState { } } -pub const CASTLING_WH_K: u8 = 0b00000001; -pub const CASTLING_WH_Q: u8 = 0b00000010; -pub const CASTLING_BL_K: u8 = 0b00000100; -pub const CASTLING_BL_Q: u8 = 0b00001000; -pub const CASTLING_MASK: u8 = 0b00001111; +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; +pub const CASTLING_SIDES: [(std::ops::RangeInclusive, u8); 2] = + [(5..=6, CASTLING_K_MASK), (2..=3, CASTLING_Q_MASK)]; pub const START_WH_K_POS: Pos = pos("e1"); pub const START_BL_K_POS: Pos = pos("e8"); @@ -47,30 +56,101 @@ pub fn apply_move(board: &Board, game_state: &GameState, m: &Move) -> (Board, Ga (new_board, new_state) } +/// 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) { - apply_move_to_board(board, game_state, m); + apply_move_to_board(board, m); apply_move_to_state(game_state, m); + 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, + _ => {} + }; + } } /// 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_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); + } } +/// 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); - // TODO handle castling } -/// Get a list of legal moves for all pieces of this color. -pub fn get_player_legal_moves(board: &Board, game_state: &GameState) -> Vec { - filter_illegal_moves(board, game_state, get_player_moves(board, game_state)) +/// Get the corresponding castling flag for this move. +pub fn get_castle(m: &Move) -> Option { + 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 + } } -/// Get a list of moves for all pieces of this color. -pub fn get_player_moves(board: &Board, game_state: &GameState) -> Vec { +/// 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), + } +} + +/// 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 { let mut moves = vec!(); for r in 0..8 { for f in 0..8 { @@ -79,7 +159,7 @@ pub fn get_player_moves(board: &Board, game_state: &GameState) -> Vec { continue } if is_color(get_square(board, &p), game_state.color) { - moves.append(&mut get_piece_moves(board, &p, game_state)); + moves.append(&mut get_piece_moves(board, &p, game_state, commit)); } } } @@ -87,19 +167,25 @@ pub fn get_player_moves(board: &Board, game_state: &GameState) -> Vec { } /// Get a list of moves for the piece at position `at`. -pub fn get_piece_moves(board: &Board, at: &Pos, game_state: &GameState) -> Vec { +pub fn get_piece_moves(board: &Board, at: &Pos, game_state: &GameState, commit: bool) -> Vec { 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, game_state), + 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), _ => vec!(), } } -fn get_pawn_moves(board: &Board, at: &Pos, piece: u8) -> Vec { +fn get_pawn_moves( + board: &Board, + at: &Pos, + piece: u8, + game_state: &GameState, + commit: bool, +) -> Vec { let (f, r) = *at; let mut moves = vec!(); // Direction: positive for white, negative for black. @@ -123,22 +209,31 @@ fn get_pawn_moves(board: &Board, at: &Pos, piece: u8) -> Vec { } else { None }; - moves.push((*at, forward, prom)) + let m = (*at, forward, prom); + if can_register(commit, board, game_state, &m) { + moves.push(m); + } } // Check diagonals for pieces to attack. if i == 1 { + // First diagonal. let df = f - 1; if df >= POS_MIN { let diag: Pos = (df, forward_r); if let Some(m) = move_on_enemy(piece, at, get_square(board, &diag), &diag) { - moves.push(m); + if can_register(commit, board, game_state, &m) { + moves.push(m); + } } } + // Second diagonal. let df = f + 1; if df <= POS_MAX { let diag: Pos = (df, forward_r); if let Some(m) = move_on_enemy(piece, at, get_square(board, &diag), &diag) { - moves.push(m); + if can_register(commit, board, game_state, &m) { + moves.push(m); + } } } } @@ -147,13 +242,19 @@ fn get_pawn_moves(board: &Board, at: &Pos, piece: u8) -> Vec { moves } -fn get_bishop_moves(board: &Board, at: &Pos, piece: u8) -> Vec { +fn get_bishop_moves( + board: &Board, + at: &Pos, + piece: u8, + game_state: &GameState, + commit: bool, +) -> Vec { let (f, r) = at; - let mut sight = [true; 4]; // Store diagonals where a piece blocks sight. + let mut views = [true; 4]; // Store diagonals where a piece blocks commit. let mut moves = vec!(); for dist in 1..=7 { for (dir, offset) in [(1, -1), (1, 1), (-1, 1), (-1, -1)].iter().enumerate() { - if !sight[dir] { + if !views[dir] { continue } let p = (f + offset.0 * dist, r + offset.1 * dist); @@ -161,19 +262,30 @@ fn get_bishop_moves(board: &Board, at: &Pos, piece: u8) -> Vec { continue } if is_empty(board, &p) { - moves.push((*at, p, None)); - } else { - if let Some(m) = move_on_enemy(piece, at, get_square(board, &p), &p) { + let m = (*at, p, None); + if can_register(commit, board, game_state, &m) { moves.push(m); } - sight[dir] = false; // Stop looking in that direction. + } else { + if let Some(m) = move_on_enemy(piece, at, get_square(board, &p), &p) { + if can_register(commit, board, game_state, &m) { + moves.push(m); + } + } + views[dir] = false; // Stop looking in that direction. } } } moves } -fn get_knight_moves(board: &Board, at: &Pos, piece: u8) -> Vec { +fn get_knight_moves( + board: &Board, + at: &Pos, + piece: u8, + game_state: &GameState, + commit: bool, +) -> Vec { 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() { @@ -182,21 +294,32 @@ fn get_knight_moves(board: &Board, at: &Pos, piece: u8) -> Vec { continue } if is_empty(board, &p) { - moves.push((*at, p, None)); + let m = (*at, p, None); + if can_register(commit, board, game_state, &m) { + moves.push(m); + } } else if let Some(m) = move_on_enemy(piece, at, get_square(board, &p), &p) { - moves.push(m); + if can_register(commit, board, game_state, &m) { + moves.push(m); + } } } moves } -fn get_rook_moves(board: &Board, at: &Pos, piece: u8) -> Vec { +fn get_rook_moves( + board: &Board, + at: &Pos, + piece: u8, + game_state: &GameState, + commit: bool, +) -> Vec { let (f, r) = at; let mut moves = vec!(); - let mut sight = [true; 4]; // Store lines where a piece blocks sight. + let mut views = [true; 4]; // Store lines where a piece blocks commit. for dist in 1..=7 { for (dir, offset) in [(0, 1), (1, 0), (0, -1), (-1, 0)].iter().enumerate() { - if !sight[dir] { + if !views[dir] { continue } let p = (f + offset.0 * dist, r + offset.1 * dist); @@ -204,27 +327,44 @@ fn get_rook_moves(board: &Board, at: &Pos, piece: u8) -> Vec { continue } if is_empty(board, &p) { - moves.push((*at, p, None)); - } else { - if let Some(m) = move_on_enemy(piece, at, get_square(board, &p), &p) { + let m = (*at, p, None); + if can_register(commit, board, game_state, &m) { moves.push(m); } - sight[dir] = false; // Stop looking in that direction. + } else { + if let Some(m) = move_on_enemy(piece, at, get_square(board, &p), &p) { + if can_register(commit, board, game_state, &m) { + moves.push(m); + } + } + views[dir] = false; // Stop looking in that direction. } } } moves } -fn get_queen_moves(board: &Board, at: &Pos, piece: u8) -> Vec { +fn get_queen_moves( + board: &Board, + at: &Pos, + piece: u8, + game_state: &GameState, + commit: bool +) -> Vec { let mut moves = vec!(); // Easy way to get queen moves, but may be a bit quicker if everything was rewritten here. - moves.append(&mut get_bishop_moves(board, at, piece)); - moves.append(&mut get_rook_moves(board, at, piece)); + moves.append(&mut get_bishop_moves(board, at, piece, game_state, commit)); + moves.append(&mut get_rook_moves(board, at, piece, game_state, commit)); moves } -fn get_king_moves(board: &Board, at: &Pos, piece: u8, _game_state: &GameState) -> Vec { +fn get_king_moves( + board: &Board, + at: &Pos, + piece: u8, + game_state: &GameState, + commit: bool +) -> Vec { 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() { @@ -233,15 +373,81 @@ fn get_king_moves(board: &Board, at: &Pos, piece: u8, _game_state: &GameState) - continue } if is_empty(board, &p) { - moves.push((*at, p, None)); + let m = (*at, p, None); + if can_register(commit, board, game_state, &m) { + moves.push(m); + } } else if let Some(m) = move_on_enemy(piece, at, get_square(board, &p), &p) { - moves.push(m); + 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) + }; + + // Check for castling if the king is on its castling rank (R1) and is not in check (R4). + if + *r == castling_rank && // Part of R1 for the king. + !is_attacked(board, game_state, at) // R4 + { + // Check for both castling sides. + for (path, castling_side_mask) in CASTLING_SIDES.iter() { + // Check for castling availability for this color and side. + if ((game_state.castling & castling_color_mask) | castling_side_mask) != 0 { + // R3, R5, R6: check that files on the way are empty and not attacked. + let mut clear_path = true; + for through_f in path.to_owned() { + let p = (through_f, castling_rank); + if !is_empty(board, &p) || is_illegal(board, game_state, &(*at, p, None)) { + clear_path = false; + break; + } + } + + // If the path is clear, the castling can be done. + if clear_path { + let castle = CASTLING_K_MASK & castling_color_mask; + let m = get_castle_move(castle); + if can_register(commit, board, game_state, &m) { + moves.push(m); + } + } + } } } - // TODO castling moves } +/// 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) +} + /// Return a move from pos1 to pos2 if piece1 & piece2 are enemies. fn move_on_enemy(piece1: u8, pos1: &Pos, piece2: u8, pos2: &Pos) -> Option { let color1 = get_color(piece1); @@ -261,27 +467,36 @@ fn move_on_enemy(piece1: u8, pos1: &Pos, piece2: u8, pos2: &Pos) -> Option } } -/// 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, game_state: &GameState, moves: Vec) -> Vec { - let king_p = find_king(board, game_state.color); - moves.into_iter().filter(|m| { - // If king moved, use its new position. +/// 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. let king_p = if m.0 == king_p { m.1 } else { king_p }; - let (new_board, new_state) = apply_move(board, game_state, m); + let mut hypothetic_board = board.clone(); + apply_move_to_board(&mut hypothetic_board, m); // Check if the move makes the player king in check. - !is_attacked(&new_board, &new_state, &king_p) - }).collect() + if is_attacked(&hypothetic_board, &game_state, &king_p) { + return true + } + } + false } /// Return true if the piece at position `at` is attacked. /// +/// 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 -/// tell that it is black turn. +/// 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 { - let enemy_moves = get_player_moves(board, game_state); + 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); for m in enemy_moves.iter() { if *at == m.1 { return true @@ -293,203 +508,259 @@ fn is_attacked(board: &Board, game_state: &GameState, at: &Pos) -> bool { #[cfg(test)] mod tests { use super::*; + use crate::notation::parse_move; + + #[test] + fn test_get_castle() { + assert_eq!(get_castle(&parse_move("e1a1")), Some(CASTLING_WH_Q)); + assert_eq!(get_castle(&parse_move("e1h1")), Some(CASTLING_WH_K)); + assert_eq!(get_castle(&parse_move("e8a8")), Some(CASTLING_BL_Q)); + assert_eq!(get_castle(&parse_move("e8h8")), Some(CASTLING_BL_K)); + assert_eq!(get_castle(&parse_move("d2d4")), None); + } #[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)); + 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 - apply_move_to_board(&mut b, &s, &(pos("f4"), pos("e6"), None)); + 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); } + #[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. + apply_move_to(&mut b, &mut gs, &parse_move("e1a1")); + 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. + apply_move_to(&mut b, &mut gs, &parse_move("e8h8")); + 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); + } + #[test] fn test_get_player_moves() { let b = new(); - let s = GameState::new(); + let gs = GameState::new(); // At first move, white has 16 pawn moves and 4 knight moves. - let moves = get_player_moves(&b, &s); + let moves = get_player_moves(&b, &gs, true); assert_eq!(moves.len(), 20); } #[test] fn test_get_pawn_moves() { let mut b = new_empty(); - let s = GameState::new(); + let gs = 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"), &s); - assert!(moves.len() == 1 && moves.contains( &(pos("d3"), pos("d4"), None) )); + let moves = get_piece_moves(&b, &pos("d3"), &gs, true); + assert!(moves.len() == 1 && moves.contains( &parse_move("d3d4") )); // 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"), &s); + let moves = get_piece_moves(&b, &pos("e2"), &gs, true); assert_eq!(moves.len(), 2); - assert!(moves.contains( &(pos("e2"), pos("e3"), None) )); - assert!(moves.contains( &(pos("e2"), pos("e4"), None) )); + assert!(moves.contains( &parse_move("e2e3") )); + assert!(moves.contains( &parse_move("e2e4") )); // 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"), &s); - assert!(moves.len() == 1 && moves.contains( &(pos("e2"), pos("e3"), None) )); + 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. set_square(&mut b, &pos("e3"), SQ_BL_P); - let moves = get_piece_moves(&b, &pos("e2"), &s); + let moves = get_piece_moves(&b, &pos("e2"), &gs, true); 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"), &s); + let moves = get_piece_moves(&b, &pos("e2"), &gs, true); 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"), &s); - assert!(moves.len() == 1 && moves.contains( &(pos("e2"), pos("f3"), None) )); + let moves = get_piece_moves(&b, &pos("e2"), &gs, true); + assert!(moves.len() == 1 && moves.contains( &parse_move("e2f3") )); set_square(&mut b, &pos("d3"), SQ_BL_P); - let moves = get_piece_moves(&b, &pos("e2"), &s); + let moves = get_piece_moves(&b, &pos("e2"), &gs, true); assert_eq!(moves.len(), 2); - assert!(moves.contains( &(pos("e2"), pos("f3"), None) )); - assert!(moves.contains( &(pos("e2"), pos("d3"), None) )); + assert!(moves.contains( &parse_move("e2f3") )); + assert!(moves.contains( &parse_move("e2d3") )); // 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"), &s); - assert!(moves.len() == 1 && moves.contains( &(pos("a7"), pos("a8"), Some(SQ_Q)) )); + let moves = get_piece_moves(&b, &pos("a7"), &gs, true); + assert!(moves.len() == 1 && moves.contains( &parse_move("a7a8q") )); } #[test] fn test_get_bishop_moves() { let mut b = new_empty(); - let s = GameState::new(); + let gs = 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"), &s); + let moves = get_piece_moves(&b, &pos("d4"), &gs, true); assert_eq!(moves.len(), 13); // Going top-right. - assert!(moves.contains( &(pos("d4"), pos("e5"), None) )); - assert!(moves.contains( &(pos("d4"), pos("f6"), None) )); - assert!(moves.contains( &(pos("d4"), pos("g7"), None) )); - assert!(moves.contains( &(pos("d4"), pos("h8"), None) )); + assert!(moves.contains( &parse_move("d4e5") )); + assert!(moves.contains( &parse_move("d4f6") )); + assert!(moves.contains( &parse_move("d4g7") )); + assert!(moves.contains( &parse_move("d4h8") )); // Going bottom-right. - assert!(moves.contains( &(pos("d4"), pos("e3"), None) )); - assert!(moves.contains( &(pos("d4"), pos("f2"), None) )); - assert!(moves.contains( &(pos("d4"), pos("g1"), None) )); + assert!(moves.contains( &parse_move("d4e3") )); + assert!(moves.contains( &parse_move("d4f2") )); + assert!(moves.contains( &parse_move("d4g1") )); // Going bottom-left. - assert!(moves.contains( &(pos("d4"), pos("c3"), None) )); - assert!(moves.contains( &(pos("d4"), pos("b2"), None) )); - assert!(moves.contains( &(pos("d4"), pos("a1"), None) )); + assert!(moves.contains( &parse_move("d4c3") )); + assert!(moves.contains( &parse_move("d4b2") )); + assert!(moves.contains( &parse_move("d4a1") )); // Going top-left. - assert!(moves.contains( &(pos("d4"), pos("c5"), None) )); - assert!(moves.contains( &(pos("d4"), pos("b6"), None) )); - assert!(moves.contains( &(pos("d4"), pos("a7"), None) )); + assert!(moves.contains( &parse_move("d4c5") )); + assert!(moves.contains( &parse_move("d4b6") )); + assert!(moves.contains( &parse_move("d4a7") )); - // When blocking sight to one square with friendly piece, lose 2 moves. + // When blocking commit 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"), &s).len(), 11); + assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, true).len(), 11); - // When blocking sight to one square with enemy piece, lose only 1 move. + // When blocking commit 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"), &s).len(), 12); + assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, true).len(), 12); } #[test] fn test_get_knight_moves() { let mut b = new_empty(); - let s = GameState::new(); + let gs = GameState::new(); - // A knight never has blocked sight; if it's in the center of the board, it can have up to + // A knight never has blocked commit; 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"), &s).len(), 8); + assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, true).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"), &s).len(), 4); + assert_eq!(get_piece_moves(&b, &pos("a4"), &gs, true).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"), &s).len(), 2); + assert_eq!(get_piece_moves(&b, &pos("a1"), &gs, true).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"), &s).len(), 0); + assert_eq!(get_piece_moves(&b, &pos("a1"), &gs, true).len(), 0); } #[test] fn test_get_rook_moves() { let mut b = new_empty(); - let s = GameState::new(); + let gs = GameState::new(); set_square(&mut b, &pos("d4"), SQ_WH_R); - assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 14); + assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, true).len(), 14); set_square(&mut b, &pos("d6"), SQ_BL_P); - assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 12); + assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, true).len(), 12); set_square(&mut b, &pos("d6"), SQ_WH_P); - assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 11); + assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, true).len(), 11); } #[test] fn test_get_queen_moves() { let mut b = new_empty(); - let s = GameState::new(); + let gs = GameState::new(); set_square(&mut b, &pos("d4"), SQ_WH_Q); - assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 14 + 13); // Bishop + rook moves. + assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, true).len(), 14 + 13); } #[test] fn test_get_king_moves() { - let mut b = new_empty(); - let s = GameState::new(); + let mut gs = GameState::new(); + return; // FIXME + // King can move 1 square in any direction. + let mut b = new_empty(); set_square(&mut b, &pos("d4"), SQ_WH_K); - assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 8); + assert_eq!(get_piece_moves(&b, &pos("d4"), &gs, true).len(), 8); set_square(&mut b, &pos("e5"), SQ_WH_P); - assert_eq!(get_piece_moves(&b, &pos("d4"), &s).len(), 7); + 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); } #[test] fn test_filter_illegal_moves() { let mut b = new_empty(); - let s = GameState::new(); + let gs = 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"), &s); - assert_eq!(all_wh_moves.len(), 5); - assert_eq!(filter_illegal_moves(&b, &s, all_wh_moves).len(), 2); + let all_wh_moves = get_piece_moves(&b, &pos("e1"), &gs, true); + assert_eq!(all_wh_moves.len(), 2); // 5 moves in absolute but only 2 are legal. } #[test] fn test_is_attacked() { let mut b = new_empty(); - let mut s = GameState::new(); - s.color = SQ_BL; + let gs = GameState::new(); // 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, &s, &pos("d4"))); + assert!(is_attacked(&b, &gs, &pos("d4"))); // Move the rook on another file, no more attack. - apply_move_to(&mut b, &mut s, &(pos("d6"), pos("e6"), None)); - assert!(!is_attacked(&b, &s, &pos("d4"))); + apply_move_to_board(&mut b, &parse_move("d6e6")); + assert!(!is_attacked(&b, &gs, &pos("d4"))); } }