From 61ddacd20d9c3fffd9182b191a004ec368f987bb Mon Sep 17 00:00:00 2001 From: cool-mist Date: Fri, 3 Jan 2025 00:29:28 +0530 Subject: [PATCH] Add king moves --- .gitignore | 2 +- Cargo.lock | 7 ++ Cargo.toml | 6 ++ src/engine/board.rs | 177 +++++++++++++++++++++++++++++++++++++++++++ src/engine/coord.rs | 97 ++++++++++++++++++++++++ src/engine/mod.rs | 5 ++ src/engine/move.rs | 28 +++++++ src/engine/piece.rs | 57 ++++++++++++++ src/engine/square.rs | 41 ++++++++++ src/main.rs | 3 + 10 files changed, 422 insertions(+), 1 deletion(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/engine/board.rs create mode 100644 src/engine/coord.rs create mode 100644 src/engine/mod.rs create mode 100644 src/engine/move.rs create mode 100644 src/engine/piece.rs create mode 100644 src/engine/square.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore index d01bd1a..290f8c5 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,4 @@ Cargo.lock # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +.idea/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ed9b31a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "sol_chess" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c3f9ac4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "sol_chess" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/src/engine/board.rs b/src/engine/board.rs new file mode 100644 index 0000000..95bb64c --- /dev/null +++ b/src/engine/board.rs @@ -0,0 +1,177 @@ +use super::{ + coord::{at, Coord}, + piece::Piece, + square::Square, +}; + +pub(crate) struct Board { + cells: [[Square; 4]; 4], +} + +pub(crate) struct Move { + piece: Piece, + from: Coord, + to: Coord, + target: Piece, +} + +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")), + ], + ], + } + } + + pub(crate) fn place(&mut self, square: Square) -> Square { + let coord = square.coord(); + 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, + }); + } + } + } + + candidates + } + + pub(crate) fn print(&self) -> String { + let mut builder: Vec = Vec::new(); + for rank in 0..4 { + for file in 0..4 { + match self.cells[file][rank] { + Square::Empty(_) => builder.push('.'), + Square::Occupied(Piece::King, _) => builder.push('K'), + Square::Occupied(Piece::Queen, _) => builder.push('Q'), + Square::Occupied(Piece::Bishop, _) => builder.push('B'), + Square::Occupied(Piece::Knight, _) => builder.push('N'), + Square::Occupied(Piece::Rook, _) => builder.push('R'), + Square::Occupied(Piece::Pawn, _) => builder.push('P'), + } + } + builder.push('\n'); + } + + builder.iter().collect::() + } +} + +#[cfg(test)] +mod tests { + use crate::engine::piece::p; + use crate::engine::square::sq; + + use super::*; + + macro_rules! validate_board { + ($board:expr, $row1:literal, $row2:literal, $row3:literal, $row4:literal) => { + let printed = $board.print(); + assert_eq!( + printed, + format!("{}\n{}\n{}\n{}\n", $row1, $row2, $row3, $row4) + ); + }; + } + + #[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!(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() { + let mut board = Board::new(); + board.place(sq!("K", "a2")); + board.place(sq!("P", "a1")); + board.place(sq!("P", "c4")); + + let legal_moves = board.legal_moves(); + assert_eq!(legal_moves.len(), 1); + + board.place(sq!("P", "b1")); + let legal_moves = board.legal_moves(); + assert_eq!(legal_moves.len(), 2); + } + + #[test] + fn test_legal_moves_king_center() { + 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")); + + let legal_moves = board.legal_moves(); + assert_eq!(legal_moves.len(), 3); + } +} diff --git a/src/engine/coord.rs b/src/engine/coord.rs new file mode 100644 index 0000000..3938954 --- /dev/null +++ b/src/engine/coord.rs @@ -0,0 +1,97 @@ +use core::fmt; + +#[derive(Clone, PartialEq)] +pub(crate) struct Coord { + // a = 0, b = 1, c = 2, d = 3 + pub(crate) file: usize, + + // 1 = 0, 2 = 1, 3 = 2, 4 = 3 + pub(crate) rank: usize, + pub(crate) notation: String, +} + +impl Coord { + pub(crate) fn parse(coord: &str) -> Self { + let mut chars = coord.chars(); + let file = chars.next().expect("file missing"); + let file = match file { + 'a' => 0, + 'b' => 1, + 'c' => 2, + 'd' => 3, + _ => panic!("file should be between a-d"), + }; + + let rank = chars.next().unwrap().to_digit(10).expect("rank missing") as usize; + if rank < 1 || rank > 4 { + panic!("rank should be between 1-4"); + } + let rank = 4 - rank; + + Coord { + file, + rank, + notation: coord.to_string(), + } + } + + pub(crate) fn new(file: usize, rank: usize) -> Self { + Coord { + file, + rank, + notation: Self::get_notation(file, rank), + } + } + + fn get_notation(rank: usize, file: usize) -> String { + format!("{}{}", "abcd".chars().nth(file).unwrap(), 4 - rank) + } +} + +macro_rules! at { + ($coord:literal) => { + Coord::parse($coord) + }; +} + +pub(crate) use at; + +impl fmt::Debug for Coord { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}({},{})", self.notation, self.file, self.rank) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! validate_coord { + ($notation:literal, $file:expr, $rank:expr) => { + let coord = at!($notation); + assert_eq!(coord.file, $file); + assert_eq!(coord.rank, $rank); + assert_eq!(coord.notation, $notation); + }; + } + + #[test] + fn test_coord_parse() { + validate_coord!("a1", 0, 3); + validate_coord!("a2", 0, 2); + validate_coord!("a3", 0, 1); + validate_coord!("a4", 0, 0); + validate_coord!("b1", 1, 3); + validate_coord!("b2", 1, 2); + validate_coord!("b3", 1, 1); + validate_coord!("b4", 1, 0); + validate_coord!("c1", 2, 3); + validate_coord!("c2", 2, 2); + validate_coord!("c3", 2, 1); + validate_coord!("c4", 2, 0); + validate_coord!("d1", 3, 3); + validate_coord!("d2", 3, 2); + validate_coord!("d3", 3, 1); + validate_coord!("d4", 3, 0); + } +} diff --git a/src/engine/mod.rs b/src/engine/mod.rs new file mode 100644 index 0000000..192f4fc --- /dev/null +++ b/src/engine/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod board; +pub(crate) mod coord; +pub(crate) mod r#move; +pub(crate) mod piece; +pub(crate) mod square; diff --git a/src/engine/move.rs b/src/engine/move.rs new file mode 100644 index 0000000..ee51da9 --- /dev/null +++ b/src/engine/move.rs @@ -0,0 +1,28 @@ +use super::{coord::Coord, piece::Piece}; + +pub(crate) struct Move { + piece: Piece, + from: Coord, + to: Coord, + target: Piece, +} + +impl Move { + pub(crate) fn new(piece: Piece, from: Coord, to: Coord, target: Piece) -> Self { + Move { + piece, + from, + to, + target, + } + } + + pub(crate) fn notation(&self) -> String { + format!( + "{}{}{}", + self.piece.notation(), + self.from.notation, + self.to.notation + ) + } +} diff --git a/src/engine/piece.rs b/src/engine/piece.rs new file mode 100644 index 0000000..aed3fa8 --- /dev/null +++ b/src/engine/piece.rs @@ -0,0 +1,57 @@ +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum Piece { + King, + Queen, + Bishop, + Knight, + Rook, + Pawn, +} + +impl Piece { + pub(crate) fn parse(piece: &str) -> Self { + match piece { + "K" => Piece::King, + "Q" => Piece::Queen, + "B" => Piece::Bishop, + "N" => Piece::Knight, + "R" => Piece::Rook, + "P" => Piece::Pawn, + p => panic!("Invalid piece {}", p), + } + } + + pub(crate) fn notation(&self) -> &str { + match self { + Piece::King => "K", + Piece::Queen => "Q", + Piece::Bishop => "B", + Piece::Knight => "N", + Piece::Rook => "R", + Piece::Pawn => "P", + } + } +} + +macro_rules! p { + ($piece:literal) => { + Piece::parse($piece) + }; +} + +pub(crate) use p; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_piece_parse() { + assert_eq!(p!("K"), Piece::King); + assert_eq!(p!("Q"), Piece::Queen); + assert_eq!(p!("B"), Piece::Bishop); + assert_eq!(p!("N"), Piece::Knight); + assert_eq!(p!("R"), Piece::Rook); + assert_eq!(p!("P"), Piece::Pawn); + } +} diff --git a/src/engine/square.rs b/src/engine/square.rs new file mode 100644 index 0000000..0242065 --- /dev/null +++ b/src/engine/square.rs @@ -0,0 +1,41 @@ +use super::{coord::Coord, piece::Piece}; + +#[derive(Clone, Debug)] +pub(crate) enum Square { + Empty(Coord), + Occupied(Piece, Coord), +} + +impl Square { + pub(crate) fn is_empty(&self) -> bool { + match self { + Square::Empty(_) => true, + _ => false, + } + } + + pub(crate) fn coord(&self) -> Coord { + match self { + Square::Empty(coord) => coord.clone(), + Square::Occupied(_, coord) => coord.clone(), + } + } + + pub(crate) fn occupied(&self) -> Option { + match self { + Square::Occupied(piece, _) => Some(*piece), + _ => None, + } + } +} + +macro_rules! sq { + ($coord:literal) => { + Square::Empty(at!($coord)) + }; + ($piece:literal, $coord:literal) => { + Square::Occupied(p!($piece), at!($coord)) + }; +} + +pub(crate) use sq; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5d2f332 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +mod engine; + +fn main() {}