commit ce16748258b0efd87d2a58bfb552801c9ad1b835 Author: dece Date: Sun May 31 02:41:02 2020 +0200 init: board and basic move rules diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7a58565 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "vatu" +version = "0.1.0" + diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c5100cb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "vatu" +version = "0.1.0" +authors = ["dece "] +edition = "2018" + +[dependencies] diff --git a/src/.rules.rs.swp b/src/.rules.rs.swp new file mode 100644 index 0000000..72d6200 Binary files /dev/null and b/src/.rules.rs.swp differ diff --git a/src/board.rs b/src/board.rs new file mode 100644 index 0000000..6d06e48 --- /dev/null +++ b/src/board.rs @@ -0,0 +1,165 @@ +//! Basic type definitions and functions. + +// Piece type flags. +pub const SQ_E: u8 = 0; +pub const SQ_P: u8 = 0b00000001; +pub const SQ_B: u8 = 0b00000010; +pub const SQ_N: u8 = 0b00000100; +pub const SQ_R: u8 = 0b00001000; +pub const SQ_Q: u8 = 0b00010000; +pub const SQ_K: u8 = 0b00100000; + +// Piece color flags. +pub const SQ_WH: u8 = 0b01000000; +pub const SQ_BL: u8 = 0b10000000; + +// Piece flags helpers. +pub const SQ_WH_P: u8 = SQ_WH|SQ_P; +pub const SQ_WH_B: u8 = SQ_WH|SQ_B; +pub const SQ_WH_N: u8 = SQ_WH|SQ_N; +pub const SQ_WH_R: u8 = SQ_WH|SQ_R; +pub const SQ_WH_Q: u8 = SQ_WH|SQ_Q; +pub const SQ_WH_K: u8 = SQ_WH|SQ_K; +pub const SQ_BL_P: u8 = SQ_BL|SQ_P; +pub const SQ_BL_B: u8 = SQ_BL|SQ_B; +pub const SQ_BL_N: u8 = SQ_BL|SQ_N; +pub const SQ_BL_R: u8 = SQ_BL|SQ_R; +pub const SQ_BL_Q: u8 = SQ_BL|SQ_Q; +pub const SQ_BL_K: u8 = SQ_BL|SQ_K; + +#[inline] +pub fn has_flag(i: u8, flag: u8) -> bool { i & flag == flag } + +// Wrappers for clearer naming. +#[inline] +pub fn is_piece(square: u8, piece: u8) -> bool { has_flag(square, piece) } +#[inline] +pub fn is_color(square: u8, color: u8) -> bool { has_flag(square, color) } +#[inline] +pub fn is_white(square: u8) -> bool { is_color(square, SQ_WH) } +#[inline] +pub fn is_black(square: u8) -> bool { is_color(square, SQ_BL) } + +pub const POS_MIN: i8 = 0; +pub const POS_MAX: i8 = 7; +/// Coords (file, rank) of a square on a board, both components are in [0, 7]. +pub type Pos = (i8, i8); + +#[inline] +pub fn is_valid_pos_c(component: i8) -> bool { component >= 0 && component <= 7 } + +#[inline] +pub fn is_valid_pos(pos: Pos) -> bool { is_valid_pos_c(pos.0) && is_valid_pos_c(pos.1) } + +/// Convert string coordinates to Pos. +/// +/// `s` has to be valid UTF8, or the very least ASCII because chars +/// are interpreted as raw bytes. +#[inline] +pub fn pos(s: &str) -> Pos { + let chars = s.as_bytes(); + ((chars[0] - 0x61) as i8, (chars[1] - 0x31) as i8) +} + +/// Bitboard representation of a chess board. +/// +/// 64 squares, from A1, A2 to H7, H8. A square is an u8, with bits +/// defining the state of the square. +pub type Board = [u8; 64]; + +pub fn new() -> Board { + [ + /* 1 2 3 4 5 6 7 8 */ + /* A */ SQ_WH_R, SQ_WH_P, SQ_E, SQ_E, SQ_E, SQ_E, SQ_BL_P, SQ_BL_R, + /* B */ SQ_WH_N, SQ_WH_P, SQ_E, SQ_E, SQ_E, SQ_E, SQ_BL_P, SQ_BL_N, + /* C */ SQ_WH_B, SQ_WH_P, SQ_E, SQ_E, SQ_E, SQ_E, SQ_BL_P, SQ_BL_B, + /* D */ SQ_WH_Q, SQ_WH_P, SQ_E, SQ_E, SQ_E, SQ_E, SQ_BL_P, SQ_BL_Q, + /* E */ SQ_WH_K, SQ_WH_P, SQ_E, SQ_E, SQ_E, SQ_E, SQ_BL_P, SQ_BL_K, + /* F */ SQ_WH_B, SQ_WH_P, SQ_E, SQ_E, SQ_E, SQ_E, SQ_BL_P, SQ_BL_B, + /* G */ SQ_WH_N, SQ_WH_P, SQ_E, SQ_E, SQ_E, SQ_E, SQ_BL_P, SQ_BL_N, + /* H */ SQ_WH_R, SQ_WH_P, SQ_E, SQ_E, SQ_E, SQ_E, SQ_BL_P, SQ_BL_R, + ] +} + +pub fn new_empty() -> Board { + [SQ_E; 64] +} + +#[inline] +pub fn get_square(board: &Board, coords: Pos) -> u8 { + board[(coords.0 * 8 + coords.1) as usize] +} + +#[inline] +pub fn set_square(board: &mut Board, coords: Pos, piece: u8) { + board[(coords.0 * 8 + coords.1) as usize] = piece; +} + +#[inline] +pub fn clear_square(board: &mut Board, coords: Pos) { + set_square(board, coords, SQ_E); +} + +#[inline] +pub fn is_empty(board: &Board, coords: Pos) -> bool { get_square(board, coords) == SQ_E } + +/// A movement, with before/after positions. +pub type Move = (Pos, Pos); + +pub fn draw(board: &Board) { + for r in (0..8).rev() { + let mut rank = String::with_capacity(8); + for f in 0..8 { + let s = get_square(board, (f, r)); + let piece = + if is_piece(s, SQ_P) { 'p' } + else if is_piece(s, SQ_B) { 'b' } + else if is_piece(s, SQ_N) { 'n' } + else if is_piece(s, SQ_R) { 'r' } + else if is_piece(s, SQ_Q) { 'q' } + else if is_piece(s, SQ_K) { 'k' } + else { '.' }; + let piece = if is_color(s, SQ_WH) { piece.to_ascii_uppercase() } else { piece }; + rank.push(piece); + } + println!("{}", rank); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pos() { + assert_eq!(pos("a1"), (0, 0)); + assert_eq!(pos("a2"), (0, 1)); + assert_eq!(pos("a8"), (0, 7)); + assert_eq!(pos("b1"), (1, 0)); + assert_eq!(pos("h8"), (7, 7)); + } + + #[test] + fn test_get_square() { + let b = new(); + assert_eq!(get_square(&b, pos("a1")), SQ_WH_R); + assert_eq!(get_square(&b, pos("a2")), SQ_WH_P); + assert_eq!(get_square(&b, pos("a3")), SQ_E); + + assert_eq!(get_square(&b, pos("a7")), SQ_BL_P); + assert_eq!(get_square(&b, pos("a8")), SQ_BL_R); + + assert_eq!(get_square(&b, pos("d1")), SQ_WH_Q); + assert_eq!(get_square(&b, pos("d8")), SQ_BL_Q); + assert_eq!(get_square(&b, pos("e1")), SQ_WH_K); + assert_eq!(get_square(&b, pos("e8")), SQ_BL_K); + } + + #[test] + fn test_is_empty() { + let b = new(); + assert_eq!(is_empty(&b, pos("a1")), false); + assert_eq!(is_empty(&b, pos("a2")), false); + assert_eq!(is_empty(&b, pos("a3")), true); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5245eac --- /dev/null +++ b/src/main.rs @@ -0,0 +1,7 @@ +pub mod board; +pub mod rules; + +fn main() { + let b = board::new(); + board::draw(&b); +} diff --git a/src/rules.rs b/src/rules.rs new file mode 100644 index 0000000..8aff838 --- /dev/null +++ b/src/rules.rs @@ -0,0 +1,311 @@ +//! Functions to determine legal moves. + +use crate::board::*; + +/// Get a list of legal moves for all pieces of either white or black. +pub fn get_legal_player_moves(board: &Board, color: u8) -> Vec { + let mut moves = vec!(); + for r in 0..8 { + for f in 0..8 { + if is_color(get_square(board, (f, r)), color) { + moves.append(&mut get_legal_piece_moves(board, (f, r))); + } + } + } + moves +} + +/// Get a list of legal moves for the piece at position `at`. +pub fn get_legal_piece_moves(board: &Board, at: Pos) -> Vec { + match get_square(board, at) { + p if is_piece(p, SQ_P) => get_legal_pawn_moves(board, at, p), + p if is_piece(p, SQ_B) => get_legal_bishop_moves(board, at, p), + p if is_piece(p, SQ_N) => get_legal_knight_moves(board, at, p), + p if is_piece(p, SQ_R) => get_legal_rook_moves(board, at, p), + p if is_piece(p, SQ_Q) => get_legal_queen_moves(board, at, p), + p if is_piece(p, SQ_K) => get_legal_king_moves(board, at, p), + _ => vec!(), + } +} + +fn get_legal_pawn_moves(board: &Board, at: Pos, piece: u8) -> Vec { + let (f, r) = at; + let mut moves = vec!(); + let movement: i8 = if is_white(piece) { 1 } else { -1 }; + // 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 { + let forward_r = r + movement * i; + if movement > 0 && forward_r > POS_MAX { + return moves + } + if movement < 0 && forward_r < POS_MIN { + return moves + } + let forward: Pos = (f, forward_r); + if is_empty(board, forward) { + moves.push((at, forward)) + } + // Check diagonals for pieces to attack. + if i == 1 { + 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); + } + } + 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); + } + } + } + // TODO en passant + } + moves +} + +fn get_legal_bishop_moves(board: &Board, at: Pos, piece: u8) -> Vec { + let (f, r) = at; + let mut sight = [true; 4]; // Store diagonals where a piece blocks sight. + 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] { + continue + } + let p = (f + offset.0 * dist, r + offset.1 * dist); + if !is_valid_pos(p) { + continue + } + if is_empty(board, p) { + moves.push((at, p)); + } else { + if let Some(m) = move_on_enemy(piece, at, get_square(board, p), p) { + moves.push(m); + } + sight[dir] = false; // Stop looking in that direction. + } + } + } + moves +} + +fn get_legal_knight_moves(board: &Board, at: Pos, piece: u8) -> 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() { + let p = (f + offset.0, r + offset.1); + if !is_valid_pos(p) { + continue + } + if is_empty(board, p) { + moves.push((at, p)); + } else if let Some(m) = move_on_enemy(piece, at, get_square(board, p), p) { + moves.push(m); + } + } + moves +} + +fn get_legal_rook_moves(board: &Board, at: Pos, piece: u8) -> Vec { + let (f, r) = at; + let mut moves = vec!(); + let mut sight = [true; 4]; // Store lines where a piece blocks sight. + for dist in 1..=7 { + for (dir, offset) in [(0, 1), (1, 0), (0, -1), (-1, 0)].iter().enumerate() { + if !sight[dir] { + continue + } + let p = (f + offset.0 * dist, r + offset.1 * dist); + if !is_valid_pos(p) { + continue + } + if is_empty(board, p) { + moves.push((at, p)); + } else { + if let Some(m) = move_on_enemy(piece, at, get_square(board, p), p) { + moves.push(m); + } + sight[dir] = false; // Stop looking in that direction. + } + } + } + moves +} + +fn get_legal_queen_moves(board: &Board, at: Pos, piece: u8) -> 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_legal_bishop_moves(board, at, piece)); + moves.append(&mut get_legal_rook_moves(board, at, piece)); + moves +} + +fn get_legal_king_moves(board: &Board, at: Pos, piece: u8) -> 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() { + let p = (f + offset.0, r + offset.1); + if !is_valid_pos(p) { + continue + } + if is_empty(board, p) { + moves.push((at, p)); + } else if let Some(m) = move_on_enemy(piece, at, get_square(board, p), p) { + moves.push(m); + } + } + // TODO castling + moves +} + +/// 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 { + if (is_white(piece1) && is_black(piece2)) || (is_black(piece1) && is_white(piece2)) { + Some((pos1, pos2)) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_legal_player_moves() { + let b = new(); + // At first move, white has 16 pawn moves and 4 knight moves. + let moves = get_legal_player_moves(&b, SQ_WH); + assert_eq!(moves.len(), 20); + } + + #[test] + fn test_get_legal_pawn_moves() { + let mut b = new_empty(); + + // 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_legal_piece_moves(&b, pos("d3")); + assert!(moves.len() == 1 && moves.contains( &(pos("d3"), pos("d4")) )); + + // 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_legal_piece_moves(&b, pos("e2")); + assert_eq!(moves.len(), 2); + assert!(moves.contains( &(pos("e2"), pos("e3")) )); + assert!(moves.contains( &(pos("e2"), pos("e4")) )); + + // Check that a pawn cannot move forward if a piece is blocking its path. + // 1. black pawn 2 square forward: + set_square(&mut b, pos("e4"), SQ_BL_P); + let moves = get_legal_piece_moves(&b, pos("e2")); + assert!(moves.len() == 1 && moves.contains( &(pos("e2"), pos("e3")) )); + // 2. black pawn 1 square forward: + set_square(&mut b, pos("e3"), SQ_BL_P); + let moves = get_legal_piece_moves(&b, pos("e2")); + 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_legal_piece_moves(&b, pos("e2")); + assert!(moves.len() == 1 && moves.contains( &(pos("e2"), pos("f3")) )); + set_square(&mut b, pos("d3"), SQ_BL_P); + let moves = get_legal_piece_moves(&b, pos("e2")); + assert_eq!(moves.len(), 2); + assert!(moves.contains( &(pos("e2"), pos("f3")) )); + assert!(moves.contains( &(pos("e2"), pos("d3")) )); + } + + #[test] + fn test_get_legal_bishop_moves() { + let mut b = new_empty(); + + // A bishop has maximum range when it's in a center square. + set_square(&mut b, pos("d4"), SQ_WH_B); + let moves = get_legal_piece_moves(&b, pos("d4")); + assert_eq!(moves.len(), 13); + // Going top-right. + assert!(moves.contains( &(pos("d4"), pos("e5")) )); + assert!(moves.contains( &(pos("d4"), pos("f6")) )); + assert!(moves.contains( &(pos("d4"), pos("g7")) )); + assert!(moves.contains( &(pos("d4"), pos("h8")) )); + // Going bottom-right. + assert!(moves.contains( &(pos("d4"), pos("e3")) )); + assert!(moves.contains( &(pos("d4"), pos("f2")) )); + assert!(moves.contains( &(pos("d4"), pos("g1")) )); + // Going bottom-left. + assert!(moves.contains( &(pos("d4"), pos("c3")) )); + assert!(moves.contains( &(pos("d4"), pos("b2")) )); + assert!(moves.contains( &(pos("d4"), pos("a1")) )); + // Going top-left. + assert!(moves.contains( &(pos("d4"), pos("c5")) )); + assert!(moves.contains( &(pos("d4"), pos("b6")) )); + assert!(moves.contains( &(pos("d4"), pos("a7")) )); + + // When blocking sight to one square with friendly piece, lose 2 moves. + set_square(&mut b, pos("b2"), SQ_WH_P); + assert_eq!(get_legal_piece_moves(&b, pos("d4")).len(), 11); + + // When blocking sight to one square with enemy piece, lose only 1 move. + set_square(&mut b, pos("b2"), SQ_BL_P); + assert_eq!(get_legal_piece_moves(&b, pos("d4")).len(), 12); + } + + #[test] + fn test_get_legal_knight_moves() { + let mut b = new_empty(); + + // A knight never has blocked sight; if it's in the center of the board, it can have up to + // 8 moves. + set_square(&mut b, pos("d4"), SQ_WH_N); + assert_eq!(get_legal_piece_moves(&b, pos("d4")).len(), 8); + + // If on a side if has only 4 moves. + set_square(&mut b, pos("a4"), SQ_WH_N); + assert_eq!(get_legal_piece_moves(&b, pos("a4")).len(), 4); + + // And in a corner, only 2 moves. + set_square(&mut b, pos("a1"), SQ_WH_N); + assert_eq!(get_legal_piece_moves(&b, pos("a1")).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_legal_piece_moves(&b, pos("a1")).len(), 0); + } + + #[test] + fn test_get_legal_rook_moves() { + let mut b = new_empty(); + + set_square(&mut b, pos("d4"), SQ_WH_R); + assert_eq!(get_legal_piece_moves(&b, pos("d4")).len(), 14); + set_square(&mut b, pos("d6"), SQ_BL_P); + assert_eq!(get_legal_piece_moves(&b, pos("d4")).len(), 12); + set_square(&mut b, pos("d6"), SQ_WH_P); + assert_eq!(get_legal_piece_moves(&b, pos("d4")).len(), 11); + } + + #[test] + fn test_get_legal_queen_moves() { + let mut b = new_empty(); + + set_square(&mut b, pos("d4"), SQ_WH_Q); + assert_eq!(get_legal_piece_moves(&b, pos("d4")).len(), 14 + 13); // Bishop + rook moves. + } + + #[test] + fn test_get_legal_king_moves() { + let mut b = new_empty(); + + set_square(&mut b, pos("d4"), SQ_WH_K); + assert_eq!(get_legal_piece_moves(&b, pos("d4")).len(), 8); + set_square(&mut b, pos("e5"), SQ_WH_P); + assert_eq!(get_legal_piece_moves(&b, pos("d4")).len(), 7); + } +}