diff --git a/src/engine/board.rs b/src/engine/board.rs index 95bb64c..c378d4a 100644 --- a/src/engine/board.rs +++ b/src/engine/board.rs @@ -1,96 +1,90 @@ +use std::collections::HashSet; + use super::{ coord::{at, Coord}, piece::Piece, - square::Square, + r#move::Move, + square::{sq, Square}, }; pub(crate) struct Board { - cells: [[Square; 4]; 4], + pub(crate) cells: [[Square; 4]; 4], + legal_moves: HashSet, + pieces_remaining: i8, + game_state: GameState, } -pub(crate) struct Move { - piece: Piece, - from: Coord, - to: Coord, - target: Piece, +#[derive(PartialEq, Eq, Debug)] +pub enum GameState { + NotStarted, + InProgress, + Lost, + Won, } impl Board { pub(crate) fn new() -> Self { Board { cells: [ - [ - Square::Empty(at!("a4")), - Square::Empty(at!("b4")), - Square::Empty(at!("c4")), - Square::Empty(at!("d4")), - ], - [ - Square::Empty(at!("a3")), - Square::Empty(at!("b3")), - Square::Empty(at!("c3")), - Square::Empty(at!("d3")), - ], - [ - Square::Empty(at!("a2")), - Square::Empty(at!("b2")), - Square::Empty(at!("c2")), - Square::Empty(at!("d2")), - ], - [ - Square::Empty(at!("a1")), - Square::Empty(at!("b1")), - Square::Empty(at!("c1")), - Square::Empty(at!("d1")), - ], + [sq!("a4"), sq!("b4"), sq!("c4"), sq!("d4")], + [sq!("a3"), sq!("b3"), sq!("c3"), sq!("d3")], + [sq!("a2"), sq!("b2"), sq!("c2"), sq!("d2")], + [sq!("a1"), sq!("b1"), sq!("c1"), sq!("d1")], ], + legal_moves: HashSet::new(), + pieces_remaining: 0, + game_state: GameState::NotStarted, } } - pub(crate) fn place(&mut self, square: Square) -> Square { + pub(crate) fn set(&mut self, square: Square) -> Square { let coord = square.coord(); - std::mem::replace(&mut self.cells[coord.file][coord.rank], square) - } + let new_is_occuppied = square.occupied().is_some(); + let existing = std::mem::replace(&mut self.cells[coord.file][coord.rank], square); - pub(crate) fn legal_moves(&self) -> Vec { - let mut legal_moves = Vec::new(); - for file in 0..4 { - for rank in 0..4 { - if let Square::Occupied(piece, _) = self.cells[file][rank] { - let mut moves = match piece { - Piece::King => self.king_legal_moves(Coord::new(file, rank)), - _ => Vec::with_capacity(0), - }; - - legal_moves.append(&mut moves); - } - } - } - legal_moves - } - - fn king_legal_moves(&self, start: Coord) -> Vec { - let mut candidates = Vec::new(); - let x = start.file; - let y = start.rank; - for file in (x.saturating_sub(1))..(x.saturating_add(2)) { - for rank in (y.saturating_sub(1))..(y.saturating_add(2)) { - if file == x && rank == y { - continue; - } - let target = &self.cells[file][rank]; - if let Square::Occupied(piece, _) = target { - candidates.push(Move { - piece: Piece::King, - from: Coord::new(x, y), - to: Coord::new(file, rank), - target: *piece, - }); - } - } + // If placing a piece on a blank, increment piece count + if existing.is_empty() && new_is_occuppied { + self.pieces_remaining += 1; } - candidates + // If placing a blank on a piece, decrement piece count + if existing.occupied().is_some() && !new_is_occuppied { + self.pieces_remaining -= 1; + } + + self.calc_legal_moves(); + self.calc_game_state(); + existing + } + + pub(crate) fn make_move(&mut self, mv: Move) -> Option { + if self.legal_moves.contains(&mv) { + // Remove from source + let source = std::mem::replace( + &mut self.cells[mv.from.coord_ref().file][mv.from.coord_ref().rank], + Square::Empty(mv.from.coord()), + ); + + let target = Square::Occupied(source.occupied().unwrap(), mv.to.coord()); + + // Place it on target + std::mem::replace( + &mut self.cells[mv.to.coord_ref().file][mv.to.coord_ref().rank], + target, + ); + + self.pieces_remaining -= 1; + self.calc_legal_moves(); + self.calc_game_state(); + Some(mv) + } else { + println!("Invalid move - {}", mv.notation()); + println!("Legal moves - "); + for m in &self.legal_moves { + println!("{}", m.notation()); + } + None + } } pub(crate) fn print(&self) -> String { @@ -112,11 +106,246 @@ impl Board { builder.iter().collect::() } + + fn calc_legal_moves(&mut self) { + self.legal_moves.clear(); + for file in 0..4 { + for rank in 0..4 { + if let Square::Occupied(piece, _) = self.cells[file][rank] { + let source = &self.cells[file][rank]; + let mut moves = match piece { + Piece::King => self.king_legal_moves(source), + Piece::Pawn => self.pawn_legal_moves(source), + Piece::Knight => self.knight_legal_moves(source), + Piece::Bishop => self.bishop_legal_moves(source), + Piece::Rook => self.rook_legal_moves(source), + Piece::Queen => self.queen_legal_moves(source), + }; + + moves.into_iter().for_each(|m| { + self.legal_moves.insert(m); + }); + } + } + } + } + + fn calc_game_state(&mut self) { + self.game_state = if self.pieces_remaining == 0 { + GameState::NotStarted + } else if self.pieces_remaining == 1 { + GameState::Won + } else if self.legal_moves.is_empty() { + GameState::Lost + } else { + GameState::InProgress + } + } + + fn king_legal_moves(&self, start: &Square) -> Vec { + self.rect(start.coord(), 1) + .into_iter() + .map(|s| Board::create_move(start, s)) + .collect() + } + + fn pawn_legal_moves(&self, start: &Square) -> Vec { + self.rect(start.coord(), 1) + .into_iter() + .filter(|target| { + target.coord_ref().rank < start.coord_ref().rank + && target.coord_ref().file != start.coord_ref().file + }) + .map(|s| Board::create_move(start, s)) + .collect() + } + + fn knight_legal_moves(&self, start: &Square) -> Vec { + self.rect(start.coord(), 2) + .into_iter() + .filter(|target| { + let dx = (start.coord_ref().file as isize - target.coord_ref().file as isize).abs(); + let dy = (start.coord_ref().rank as isize - target.coord_ref().rank as isize).abs(); + (dx == 1 && dy == 2) || (dx == 2 && dy == 1) + }) + .map(|s| Board::create_move(start, s)) + .collect() + } + + fn bishop_legal_moves(&self, start: &Square) -> Vec { + self.diag(start.coord()) + .into_iter() + .map(|s| Board::create_move(start, s)) + .collect() + } + + fn rook_legal_moves(&self, start: &Square) -> Vec { + self.line(start.coord()) + .into_iter() + .map(|s| Board::create_move(start, s)) + .collect() + } + + fn queen_legal_moves(&self, start: &Square) -> Vec { + let line = self.line(start.coord()).into_iter(); + let diag = self.diag(start.coord()).into_iter(); + line.chain(diag) + .map(|s| Board::create_move(start, s)) + .collect() + } + + fn rect(&self, start: Coord, radius: usize) -> Vec { + let mut range = Vec::new(); + let x_min = start.file.saturating_sub(radius); + let y_min = start.rank.saturating_sub(radius); + let x_max = start.file + radius + 1; + let y_max = start.rank + radius + 1; + + for file in (x_min)..(x_max) { + for rank in (y_min)..(y_max) { + if file > 3 || rank > 3 { + continue; + } + + if (file, rank) == (start.file, start.rank) { + continue; + } + + if self.cells[file][rank].occupied().is_none() { + continue; + } + + range.push(self.cells[file][rank].clone()); + } + } + range + } + + fn diag(&self, start: Coord) -> Vec { + let mut range = Vec::new(); + + // North West + if (start.rank > 0 && start.file > 0) { + let mut north = start.rank; + let mut west = start.file; + while north != 0 && west != 0 { + north -= 1; + west -= 1; + if let Some(piece) = self.cells[west][north].occupied() { + range.push(self.cells[west][north].clone()); + break; + } + } + } + + // North East + if (start.rank > 0 && start.file < 3) { + let mut north = start.rank; + let mut east = start.file; + while north != 0 && east < 3 { + north -= 1; + east += 1; + if let Some(piece) = self.cells[east][north].occupied() { + range.push(self.cells[east][north].clone()); + break; + } + } + } + + // South West + if (start.rank < 3 && start.file > 0) { + let mut south = start.rank; + let mut west = start.file; + while south < 4 && west != 0 { + south += 1; + west -= 1; + if let Some(piece) = self.cells[west][south].occupied() { + range.push(self.cells[west][south].clone()); + break; + } + } + } + + // South East + if (start.rank < 3 && start.file < 3) { + let mut south = start.rank; + let mut east = start.file; + while south < 3 && east < 3 { + south += 1; + east += 1; + if let Some(piece) = self.cells[east][south].occupied() { + range.push(self.cells[east][south].clone()); + break; + } + } + } + + range + } + + fn line(&self, start: Coord) -> Vec { + let mut range = Vec::new(); + + if (start.rank > 0) { + // North + let mut north = start.rank; + while north != 0 { + north -= 1; + if let Some(piece) = self.cells[start.file][north].occupied() { + range.push(self.cells[start.file][north].clone()); + break; + } + } + } + + if (start.rank < 3) { + // South + let mut south = start.rank; + while south < 3 { + south += 1; + if let Some(piece) = self.cells[start.file][south].occupied() { + range.push(self.cells[start.file][south].clone()); + break; + } + } + } + + if (start.file > 0) { + //West + let mut west = start.file; + while west != 0 { + west -= 1; + if let Some(piece) = self.cells[west][start.rank].occupied() { + range.push(self.cells[west][start.rank].clone()); + break; + } + } + } + + if (start.file < 3) { + // East + let mut east = start.file; + while east < 3 { + east += 1; + if let Some(piece) = self.cells[east][start.rank].occupied() { + range.push(self.cells[east][start.rank].clone()); + break; + } + } + } + + range + } + + fn create_move(start: &Square, target: Square) -> Move { + Move::new(start.clone(), target) + } } #[cfg(test)] mod tests { use crate::engine::piece::p; + use crate::engine::r#move::mv; use crate::engine::square::sq; use super::*; @@ -131,47 +360,181 @@ mod tests { }; } + macro_rules! validate_legal_moves { + ($board:expr, $($move:expr,)*) => { + let mut legal_moves = $board.legal_moves.iter().map(|m| m.clone()).collect::>(); + + $( + assert!(legal_moves.contains(&$move)); + let position = legal_moves.iter().position(|m| m == &$move).unwrap(); + legal_moves.remove(position); + )* + + if (legal_moves.len() > 0) { + println!("The following moves were not matched - "); + for m in &legal_moves { + println!("{}", m.notation()); + } + + assert!(false); + } + }; + } + #[test] fn test_board_place() { let mut board = Board::new(); - assert!(board.place(sq!("K", "a1")).is_empty()); - assert!(board.place(sq!("Q", "a2")).is_empty()); - assert!(board.place(sq!("B", "c3")).is_empty()); - assert!(board.place(sq!("N", "c4")).is_empty()); - assert!(board.place(sq!("R", "d1")).is_empty()); - assert!(board.place(sq!("P", "d4")).is_empty()); - assert!(board.place(sq!("N", "b2")).is_empty()); - let existing = board.place(sq!("P", "c4")); + assert!(board.set(sq!("K", "a1")).is_empty()); + assert!(board.set(sq!("Q", "a2")).is_empty()); + assert!(board.set(sq!("B", "c3")).is_empty()); + assert!(board.set(sq!("N", "c4")).is_empty()); + assert!(board.set(sq!("R", "d1")).is_empty()); + assert!(board.set(sq!("P", "d4")).is_empty()); + assert!(board.set(sq!("N", "b2")).is_empty()); + let existing = board.set(sq!("P", "c4")); assert!(existing.occupied().is_some()); assert_eq!(Piece::Knight, existing.occupied().unwrap()); validate_board!(board, "..PP", "..B.", "QN..", "K..R"); } #[test] - fn test_legal_moves_king_corner() { + fn test_legal_moves() { let mut board = Board::new(); - board.place(sq!("K", "a2")); - board.place(sq!("P", "a1")); - board.place(sq!("P", "c4")); + assert_eq!(0, board.pieces_remaining); + assert_eq!(0, board.legal_moves.len()); + assert!(board.make_move(mv!("R", "b2", "d1", "N")).is_none()); - let legal_moves = board.legal_moves(); - assert_eq!(legal_moves.len(), 1); + board.set(sq!("Q", "a4")); + board.set(sq!("K", "a2")); + board.set(sq!("P", "a1")); + board.set(sq!("P", "b3")); + board.set(sq!("R", "b2")); + board.set(sq!("P", "c4")); + board.set(sq!("K", "c3")); + board.set(sq!("B", "c1")); + board.set(sq!("B", "d2")); + board.set(sq!("N", "d1")); - board.place(sq!("P", "b1")); - let legal_moves = board.legal_moves(); - assert_eq!(legal_moves.len(), 2); + assert_eq!(10, board.pieces_remaining); + + // Q . P . + // . P K . + // K R . B + // P . B N + validate_legal_moves!( + board, + mv!("K", "a2", "a1", "P"), + mv!("K", "a2", "b2", "R"), + mv!("K", "a2", "b3", "P"), + mv!("K", "c3", "b2", "R"), + mv!("K", "c3", "b3", "P"), + mv!("K", "c3", "c4", "P"), + mv!("K", "c3", "d2", "B"), + mv!("P", "a1", "b2", "R"), + mv!("P", "b3", "c4", "P"), + mv!("P", "b3", "a4", "Q"), + mv!("Q", "a4", "a2", "K"), + mv!("Q", "a4", "b3", "P"), + mv!("Q", "a4", "c4", "P"), + mv!("R", "b2", "a2", "K"), + mv!("R", "b2", "b3", "P"), + mv!("R", "b2", "d2", "B"), + mv!("B", "c1", "b2", "R"), + mv!("B", "c1", "d2", "B"), + mv!("B", "d2", "c3", "K"), + mv!("B", "d2", "c1", "B"), + mv!("N", "d1", "b2", "R"), + mv!("N", "d1", "c3", "K"), + ); + + assert_eq!(10, board.pieces_remaining); + + // Validate some illegal moves + assert!(board.make_move(mv!("K", "a2", "a2", "P")).is_none()); + assert!(board.make_move(mv!("R", "b2", "d1", "N")).is_none()); + + board.set(sq!("b2")); + board.set(sq!("c4")); + board.set(sq!("R", "c1")); + + // Q . . . + // . P K . + // K . . B + // P . R N + validate_legal_moves!( + board, + mv!("K", "a2", "a1", "P"), + mv!("K", "a2", "b3", "P"), + mv!("K", "c3", "b3", "P"), + mv!("K", "c3", "d2", "B"), + mv!("P", "b3", "a4", "Q"), + mv!("B", "d2", "c3", "K"), + mv!("B", "d2", "c1", "R"), + mv!("Q", "a4", "a2", "K"), + mv!("Q", "a4", "b3", "P"), + mv!("R", "c1", "a1", "P"), + mv!("R", "c1", "c3", "K"), + mv!("R", "c1", "d1", "N"), + mv!("N", "d1", "c3", "K"), + ); + + assert_eq!(8, board.pieces_remaining); } #[test] - fn test_legal_moves_king_center() { + fn test_smoke_puzzle() { let mut board = Board::new(); - board.place(sq!("K", "c3")); - board.place(sq!("P", "a1")); - board.place(sq!("P", "c4")); - board.place(sq!("P", "b2")); - board.place(sq!("P", "b3")); + assert_eq!(GameState::NotStarted, board.game_state); + assert_eq!(0, board.pieces_remaining); - let legal_moves = board.legal_moves(); - assert_eq!(legal_moves.len(), 3); + // K . . . + // . P . . + // . . R . + // N . . . + board.set(sq!("K", "a4")); + assert_eq!(GameState::Won, board.game_state); + + board.set(sq!("P", "b3")); + board.set(sq!("R", "c2")); + board.set(sq!("N", "a1")); + + assert_eq!(GameState::InProgress, board.game_state); + assert_eq!(4, board.pieces_remaining); + + assert!(board.make_move(mv!("N", "a1", "c2", "R")).is_some()); + assert_eq!(3, board.pieces_remaining); + assert_eq!(GameState::InProgress, board.game_state); + + assert!(board.make_move(mv!("P", "b3", "a4", "K")).is_some()); + assert_eq!(2, board.pieces_remaining); + assert_eq!(GameState::Lost, board.game_state); + + // P . . . + // . . . . + // . . N . + // . . . . + + board.set(sq!("P", "a1")); + board.set(sq!("Q", "a3")); + + // P . . . + // Q . . . + // . . N . + // P . . . + assert_eq!(4, board.pieces_remaining); + assert_eq!(GameState::InProgress, board.game_state); + + board.make_move(mv!("Q", "a3", "a4", "P")); + board.make_move(mv!("N", "c2", "a1", "P")); + assert_eq!(2, board.pieces_remaining); + assert_eq!(GameState::InProgress, board.game_state); + + // Q . . . + // . . . . + // . . . . + // N . . . + board.make_move(mv!("Q", "a4", "a1", "N")); + assert_eq!(1, board.pieces_remaining); + assert_eq!(GameState::Won, board.game_state); } } diff --git a/src/engine/coord.rs b/src/engine/coord.rs index 3938954..fcef771 100644 --- a/src/engine/coord.rs +++ b/src/engine/coord.rs @@ -1,6 +1,6 @@ use core::fmt; -#[derive(Clone, PartialEq)] +#[derive(Clone, Eq, Hash, PartialEq)] pub(crate) struct Coord { // a = 0, b = 1, c = 2, d = 3 pub(crate) file: usize, @@ -43,7 +43,7 @@ impl Coord { } } - fn get_notation(rank: usize, file: usize) -> String { + fn get_notation(file: usize, rank: usize) -> String { format!("{}{}", "abcd".chars().nth(file).unwrap(), 4 - rank) } } @@ -72,6 +72,10 @@ mod tests { assert_eq!(coord.file, $file); assert_eq!(coord.rank, $rank); assert_eq!(coord.notation, $notation); + let coord = Coord::new($file, $rank); + assert_eq!(coord.file, $file); + assert_eq!(coord.rank, $rank); + assert_eq!(coord.notation, $notation); }; } diff --git a/src/engine/move.rs b/src/engine/move.rs index ee51da9..73fe287 100644 --- a/src/engine/move.rs +++ b/src/engine/move.rs @@ -1,28 +1,25 @@ -use super::{coord::Coord, piece::Piece}; +use super::{board::Board, coord::Coord, piece::Piece, square::Square}; +#[derive(PartialEq, Hash, Eq, Clone)] pub(crate) struct Move { - piece: Piece, - from: Coord, - to: Coord, - target: Piece, + pub(crate) from: Square, + pub(crate) to: Square, } impl Move { - pub(crate) fn new(piece: Piece, from: Coord, to: Coord, target: Piece) -> Self { - Move { - piece, - from, - to, - target, - } + pub(crate) fn new(from: Square, to: Square) -> Self { + Move { from, to } } pub(crate) fn notation(&self) -> String { - format!( - "{}{}{}", - self.piece.notation(), - self.from.notation, - self.to.notation - ) + format!("{} -> {}", self.from.notation(), self.to.notation()) } } + +macro_rules! mv { + ($piece:literal, $from:literal, $to:literal, $target:literal) => { + Move::new(sq!($piece, $from), sq!($target, $to)) + }; +} + +pub(crate) use mv; diff --git a/src/engine/piece.rs b/src/engine/piece.rs index aed3fa8..7efde54 100644 --- a/src/engine/piece.rs +++ b/src/engine/piece.rs @@ -1,4 +1,4 @@ -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Eq, Hash, Copy, Debug, PartialEq)] pub(crate) enum Piece { King, Queen, diff --git a/src/engine/square.rs b/src/engine/square.rs index 0242065..1dc6adc 100644 --- a/src/engine/square.rs +++ b/src/engine/square.rs @@ -1,6 +1,6 @@ use super::{coord::Coord, piece::Piece}; -#[derive(Clone, Debug)] +#[derive(Clone, Hash, Eq, PartialEq, Debug)] pub(crate) enum Square { Empty(Coord), Occupied(Piece, Coord), @@ -14,6 +14,13 @@ impl Square { } } + pub(crate) fn coord_ref(&self) -> &Coord { + match self { + Square::Empty(coord) => coord, + Square::Occupied(_, coord) => coord, + } + } + pub(crate) fn coord(&self) -> Coord { match self { Square::Empty(coord) => coord.clone(), @@ -27,6 +34,15 @@ impl Square { _ => None, } } + + pub(crate) fn notation(&self) -> String { + match self { + Square::Empty(coord) => coord.notation.clone(), + Square::Occupied(piece, coord) => { + format!("{}{}", piece.notation(), coord.notation.clone()) + } + } + } } macro_rules! sq { diff --git a/src/main.rs b/src/main.rs index 5d2f332..1aab287 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +#[allow(unused)] mod engine; fn main() {}