From 1e77bc5ebb53e23c1e6e188ea30b256b4a1573bc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 09:45:13 -0800 Subject: [PATCH 1/4] [board] Test to verify the king can't move into or stay in check Write a test on the king move generator to verify the king can't move to a square where it would still be in check. --- board/src/move_generator/king.rs | 38 +++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/board/src/move_generator/king.rs b/board/src/move_generator/king.rs index 1d0dda2..1f35af5 100644 --- a/board/src/move_generator/king.rs +++ b/board/src/move_generator/king.rs @@ -74,7 +74,7 @@ impl MoveGeneratorInternal for KingMoveGenerator { #[cfg(test)] mod tests { use super::*; - use crate::position; + use crate::{position, r#move::AlgebraicMoveFormatter, PositionBuilder}; use chessfriend_bitboard::bitboard; use chessfriend_core::{piece, Square}; use std::collections::HashSet; @@ -154,4 +154,40 @@ mod tests { generated_moves ); } + + #[test] + fn black_king_in_check_by_rook() { + let pos = PositionBuilder::new() + .place_piece(piece!(White King on E1)) + .place_piece(piece!(White Rook on E4)) + .place_piece(piece!(Black King on E7)) + .to_move(Color::Black) + .build(); + + assert!(pos.is_king_in_check()); + + let generator = KingMoveGenerator::new(&pos, Color::Black); + let generated_moves: HashSet = generator.iter().cloned().collect(); + + let king = piece!(Black King); + let from_square = Square::E7; + let expected_moves = HashSet::from_iter([ + MoveBuilder::new(king, from_square, Square::D6).build(), + MoveBuilder::new(king, from_square, Square::D7).build(), + MoveBuilder::new(king, from_square, Square::D8).build(), + MoveBuilder::new(king, from_square, Square::F6).build(), + MoveBuilder::new(king, from_square, Square::F7).build(), + MoveBuilder::new(king, from_square, Square::F8).build(), + ]); + + assert_eq!( + generated_moves, + expected_moves, + "Difference: {:?}", + generated_moves + .symmetric_difference(&expected_moves) + .map(|mv| format!("{}", AlgebraicMoveFormatter::new(mv, &pos))) + .collect::>() + ); + } } From 66d03d35144ab0201aaf2ad2b25c905ddb460322 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 09:46:38 -0800 Subject: [PATCH 2/4] [board] Clean up a bunch of imports --- bitboard/src/bitboard.rs | 2 +- board/src/position/diagram_formatter.rs | 1 - board/src/sight.rs | 5 +++-- explorer/src/main.rs | 5 ++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/bitboard/src/bitboard.rs b/bitboard/src/bitboard.rs index 3858ebb..f9babd4 100644 --- a/bitboard/src/bitboard.rs +++ b/bitboard/src/bitboard.rs @@ -2,7 +2,7 @@ use crate::library::{library, FILES, RANKS}; use crate::LeadingBitScanner; -use chessfriend_core::{Direction, Square}; +use chessfriend_core::{Color, Direction, Square}; use std::fmt; use std::ops::Not; diff --git a/board/src/position/diagram_formatter.rs b/board/src/position/diagram_formatter.rs index 80bbbe0..9dfb5c7 100644 --- a/board/src/position/diagram_formatter.rs +++ b/board/src/position/diagram_formatter.rs @@ -41,7 +41,6 @@ impl<'a> fmt::Display for DiagramFormatter<'a> { mod tests { use super::*; use crate::{position, Position}; - use chessfriend_core::piece; #[test] #[ignore] diff --git a/board/src/sight.rs b/board/src/sight.rs index eead56e..76005a2 100644 --- a/board/src/sight.rs +++ b/board/src/sight.rs @@ -1,9 +1,8 @@ // Eryn Wells use crate::position::piece_sets::PieceBitBoards; -use crate::Position; use chessfriend_bitboard::BitBoard; -use chessfriend_core::{Color, Direction, Piece, PlacedPiece, Shape, Square}; +use chessfriend_core::{Color, Direction, PlacedPiece, Shape, Square}; pub(crate) trait SightExt { fn sight(&self, pieces: &PieceBitBoards, en_passant_square: Option) -> BitBoard; @@ -13,11 +12,13 @@ pub(crate) trait SightExt { pieces: &PieceBitBoards, en_passant_square: Option, ) -> BitBoard; + fn black_pawn_sight( &self, pieces: &PieceBitBoards, en_passant_square: Option, ) -> BitBoard; + fn knight_sight(&self, pieces: &PieceBitBoards) -> BitBoard; fn bishop_sight(&self, pieces: &PieceBitBoards) -> BitBoard; fn rook_sight(&self, pieces: &PieceBitBoards) -> BitBoard; diff --git a/explorer/src/main.rs b/explorer/src/main.rs index fedd11c..54b9fdd 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -1,6 +1,5 @@ -use board::piece::{Color, Piece, Shape}; -use board::Position; -use chessfriend_core::Square; +use board::{fen::ToFen, MakeMoveBuilder, MoveBuilder, Position, PositionBuilder}; +use chessfriend_core::{Color, Piece, PlacedPiece, Shape, Square}; use clap::{Arg, Command}; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; From 6bd3787a24cf4e6dfc435850038e9fc834ba412b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 09:47:25 -0800 Subject: [PATCH 3/4] [board] Write test_position!({starting,empty}) macros These produce starting and empty positions, respectively, and then print the position. --- board/src/macros.rs | 14 ++++++++++++++ board/src/position/position.rs | 3 +-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/board/src/macros.rs b/board/src/macros.rs index 0ee322b..13d2c38 100644 --- a/board/src/macros.rs +++ b/board/src/macros.rs @@ -28,4 +28,18 @@ macro_rules! test_position { pos } }; + (empty) => { + { + let pos = Position::empty(); + println!("{pos}"); + pos + } + }; + (starting) => { + { + let pos = Position::starting(); + println!("{pos}"); + pos + } + }; } diff --git a/board/src/position/position.rs b/board/src/position/position.rs index acb6efa..6a09226 100644 --- a/board/src/position/position.rs +++ b/board/src/position/position.rs @@ -333,8 +333,7 @@ mod tests { #[test] fn piece_in_starting_position() { - let pos = Position::starting(); - println!("{pos}"); + let pos = test_position!(starting); assert_eq!( pos.piece_on_square(Square::H1), From 8eb180df6714d1c9ba65f5db0b47db8a4efcdbd4 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 28 Jan 2024 09:50:39 -0800 Subject: [PATCH 4/4] [explorer] Add fen and make commands Clean up the implementation of the place command. Track state with a State struct that contains a position and a builder. The place command will place a new piece and then regenerate the position. The make command makes a move. The syntax is: make [color:w|b] [shape] [from square] [to square] The fen command prints a FEN string representing the position. --- explorer/src/main.rs | 105 ++++++++++++++++++++++++++++++++----------- 1 file changed, 80 insertions(+), 25 deletions(-) diff --git a/explorer/src/main.rs b/explorer/src/main.rs index 54b9fdd..0dfe2ad 100644 --- a/explorer/src/main.rs +++ b/explorer/src/main.rs @@ -29,28 +29,31 @@ fn command_line() -> Command { {all-args} "; - // strip out name/version - const APPLET_TEMPLATE: &str = "\ - {about-with-newline}\n\ - {usage-heading}\n {usage}\n\ - \n\ - {all-args}{after-help}\ - "; - Command::new("explorer") .multicall(true) .arg_required_else_help(true) .subcommand_required(true) - .subcommand_value_name("APPLET") - .subcommand_help_heading("APPLETS") + .subcommand_value_name("CMD") + .subcommand_help_heading("COMMANDS") .help_template(PARSER_TEMPLATE) - .subcommand(Command::new("print").about("Print the board")) + .subcommand(Command::new("fen").about("Print the current position as a FEN string")) + .subcommand( + Command::new("make") + .arg(Arg::new("piece").required(true)) + .arg(Arg::new("from").required(true)) + .arg(Arg::new("to").required(true)) + .about("Make a move"), + ) .subcommand( Command::new("place") + .arg(Arg::new("color").required(true)) .arg(Arg::new("piece").required(true)) - .arg(Arg::new("square").required(true)), + .arg(Arg::new("square").required(true)) + .about("Place a piece on the board"), ) - .subcommand(Command::new("quit").about("Quit the program")) + .subcommand(Command::new("print").about("Print the board")) + .subcommand(Command::new("quit").alias("exit").about("Quit the program")) + .subcommand(Command::new("starting").about("Reset the board to the starting position")) } fn respond(line: &str, state: &mut State) -> Result { @@ -67,20 +70,72 @@ fn respond(line: &str, state: &mut State) -> Result { result.should_continue = false; result.should_print_position = false; } - Some(("place", _matches)) => { - let piece_arg = _matches.get_one::("piece").unwrap(); - let shape = match piece_arg.chars().nth(0) { - Some(c) => Shape::try_from(c).map_err(|_| ()), - None => Err(()), - } - .map_err(|_| "Error: invalid piece specifier")?; + Some(("fen", _matches)) => { + println!( + "{}", + state + .position + .to_fen() + .map_err(|_| "error: Unable to generate FEN for current position")? + ); - let square_arg = _matches.get_one::("square").unwrap(); - let square = Square::from_algebraic_str(square_arg) + result.should_print_position = false; + } + Some(("make", matches)) => { + let shape = matches + .get_one::("piece") + .ok_or("Missing piece descriptor")?; + let shape = Shape::try_from(shape).map_err(|_| "Invalid piece descriptor")?; + + let from_square = Square::from_algebraic_str( + matches.get_one::("from").ok_or("Missing square")?, + ) + .map_err(|_| "Error: invalid square specifier")?; + + let to_square = Square::from_algebraic_str( + matches.get_one::("to").ok_or("Missing square")?, + ) + .map_err(|_| "Error: invalid square specifier")?; + + let mv = MoveBuilder::new( + Piece::new(state.position.player_to_move(), shape), + from_square, + to_square, + ) + .build(); + + state.position = MakeMoveBuilder::new(&state.position) + .make(&mv) + .map_err(|err| format!("error: Cannot make move: {:?}", err))? + .build(); + state.builder = PositionBuilder::from_position(&state.position); + } + Some(("place", matches)) => { + let color = matches + .get_one::("color") + .ok_or("Missing color descriptor")?; + let color = Color::try_from(color).map_err(|_| "Invalid color descriptor")?; + + let shape = matches + .get_one::("piece") + .ok_or("Missing piece descriptor")?; + let shape = Shape::try_from(shape).map_err(|_| "Invalid piece descriptor")?; + + let square = matches + .get_one::("square") + .ok_or("Missing square")?; + let square = Square::from_algebraic_str(square) .map_err(|_| "Error: invalid square specifier")?; - pos.place_piece(&Piece::new(Color::White, shape), &square) - .map_err(|_| "Error: Unable to place piece")?; + let piece = PlacedPiece::new(Piece::new(color, shape), square); + + state.builder.place_piece(piece); + state.position = state.builder.build(); + } + Some(("starting", _matches)) => { + let starting_position = Position::starting(); + state.builder = PositionBuilder::from_position(&starting_position); + state.position = starting_position; } Some((name, _matches)) => unimplemented!("{name}"), None => unreachable!("Subcommand required"), @@ -107,7 +162,7 @@ fn main() -> Result<(), String> { println!("{} to move.", state.position.player_to_move()); } - let readline = editor.readline("? "); + let readline = editor.readline("\n? "); match readline { Ok(line) => { let line = line.trim();