basic uci protocol

This commit is contained in:
Moritz 2025-11-15 01:04:03 +01:00
parent 9d527634eb
commit 66cea5a2bf
16 changed files with 94 additions and 942 deletions

36
src/engine.rs Normal file
View file

@ -0,0 +1,36 @@
// ... (your use statements)
use crate::board::Board;
use crate::search::minimax::minimax;
pub struct Engine {
pub name: String,
pub author: String,
pub board: Board,
}
impl Engine {
pub fn new(name: String, author: String) -> Engine {
// Use the standard starting position
let board = Board::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
Engine {
name,
author,
board
}
}
pub fn setpos(&mut self, fen: &str) {
self.board = Board::from_fen(fen);
}
pub fn search(&mut self, depth: u8) {
let (opt_move, _score) = minimax(&mut self.board, depth);
if let Some(mv) = opt_move {
println!("bestmove {}", mv);
} else {
// UCI format for no legal moves (checkmate/stalemate)
println!("bestmove null");
}
}
}

View file

@ -6,3 +6,4 @@ pub mod eval;
pub mod display; pub mod display;
pub mod parsing; pub mod parsing;
pub mod search; pub mod search;
pub mod engine;

View file

@ -1,16 +1,52 @@
use chess_engine::board::Board; use std::io::{self, BufRead};
use chess_engine::movegen::generate_pseudo_legal_moves; use chess_engine::engine::Engine;
use chess_engine::movegen::legal_check::is_king_attacked;
use chess_engine::r#move::*;
use chess_engine::search::minimax;
use chess_engine::search::minimax::minimax;
fn main() { fn main() {
let mut board = Board::from_fen("rnb1kbnr/pppppppp/8/8/8/4q3/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); // Create a new engine instance
let (opt_move, _) = minimax(&mut board, 5); let mut engine = Engine::new("Yakari".to_string(), "EiSiMo".to_string());
if let Some(mv) = opt_move {
println!("Found best move: {}", mv) loop {
} else { // Start the main UCI loop
println!("No moves found") for line in io::stdin().lock().lines() {
let input = line.unwrap_or_else(|_| "quit".to_string());
let tokens: Vec<&str> = input.split_whitespace().collect();
if tokens.is_empty() {
continue;
}
match tokens[0] {
"uci" => {
println!("id name {}", engine.name);
println!("id author {}", engine.author);
println!("uciok");
}
"isready" => {
println!("readyok");
}
"position" => {
// Example: "position startpos moves e2e4 e7e5"
// Or: "position fen rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
// You'll need to write a parser for this!
// For now, let's just handle the "fen" part simply.
if tokens.len() > 1 && tokens[1] == "fen" {
let fen = tokens[2..].join(" ");
engine.setpos(&fen);
}
}
"go" => {
// Example: "go depth 6"
// For now, we'll just use the fixed depth from your search function.
engine.search(5);
}
"quit" => {
break; // Exit the loop and the program
}
_ => {
// Unknown command, just ignore
}
}
}
} }
} }

View file

@ -4,7 +4,7 @@ use crate::movegen::tables::{get_bishop_attacks, get_rook_attacks, ATTACKING_PAW
use crate::square::{Square, SQUARES}; use crate::square::{Square, SQUARES};
/// Checks if the king of the side that is NOT to move is in check /// Checks if the king of the side that is NOT to move is in check
pub fn is_king_attacked(board: &Board) -> bool { pub fn is_other_king_attacked(board: &Board) -> bool {
let king = board.pieces[PieceType::King as usize][!board.side_to_move as usize]; let king = board.pieces[PieceType::King as usize][!board.side_to_move as usize];
is_square_attacked(board, SQUARES[king.trailing_zeros() as usize], board.side_to_move) is_square_attacked(board, SQUARES[king.trailing_zeros() as usize], board.side_to_move)
} }

View file

@ -250,16 +250,6 @@ impl Move {
pub fn to_algebraic(&self) -> String { pub fn to_algebraic(&self) -> String {
let flags = self.get_flags(); let flags = self.get_flags();
// Handle castling first. In this new format, the "to" square is
// the *king's* destination square (g1/c1 or g8/c8).
// Your old implementation reading the file is still fine.
if (flags == MOVE_FLAG_WK_CASTLE) || (flags == MOVE_FLAG_BK_CASTLE) {
return "O-O".to_string();
}
if (flags == MOVE_FLAG_WQ_CASTLE) || (flags == MOVE_FLAG_BQ_CASTLE) {
return "O-O-O".to_string();
}
let from_val = self.0 & MOVE_FROM_MASK; let from_val = self.0 & MOVE_FROM_MASK;
let to_val = (self.0 & MOVE_TO_MASK) >> 6; let to_val = (self.0 & MOVE_TO_MASK) >> 6;
@ -281,4 +271,7 @@ impl Move {
format!("{}{}", from_str, to_str) format!("{}{}", from_str, to_str)
} }
} }
// TODO
// pub fn from_algebraic(s: &str, board: &Board) -> Move {}
} }

View file

@ -1,7 +1,7 @@
use crate::board::{Board, Color}; // <-- Assuming you have a Color enum (e.g., Color::White, Color::Black) use crate::board::{Board, Color}; // <-- Assuming you have a Color enum (e.g., Color::White, Color::Black)
use crate::eval::basic::evaluate_board; use crate::eval::basic::evaluate_board;
use crate::movegen::generate_pseudo_legal_moves; use crate::movegen::generate_pseudo_legal_moves;
use crate::movegen::legal_check::is_king_attacked; use crate::movegen::legal_check::is_other_king_attacked;
use crate::r#move::{Move, MoveList}; use crate::r#move::{Move, MoveList};
@ -26,7 +26,7 @@ pub fn minimax(board: &mut Board, depth: u8) -> (Option<Move>, i32) {
for mv in list.iter() { for mv in list.iter() {
let undo_mv = board.make_move(*mv); let undo_mv = board.make_move(*mv);
let is_illegal = is_king_attacked(board); let is_illegal = is_other_king_attacked(board);
if is_illegal { if is_illegal {
board.undo_move(undo_mv); board.undo_move(undo_mv);
continue; continue;
@ -44,7 +44,7 @@ pub fn minimax(board: &mut Board, depth: u8) -> (Option<Move>, i32) {
} }
if !legal_moves_found { if !legal_moves_found {
if is_king_attacked(board) { if is_other_king_attacked(board) {
return (None, -i32::MAX); return (None, -i32::MAX);
} else { } else {
return (None, 0); return (None, 0);

View file

@ -1,85 +0,0 @@
use chess_engine::board::Board;
use chess_engine::movegen::sliders::generate_bishop_moves;
use chess_engine::r#move::MoveList;
/// Compares two move list strings ignoring the order of moves.
fn assert_moves_equal(actual_str: &str, expected_str: &str) {
let mut actual_moves: Vec<&str> = actual_str.split_whitespace().collect();
let mut expected_moves: Vec<&str> = expected_str.split_whitespace().collect();
actual_moves.sort();
expected_moves.sort();
assert_eq!(actual_moves, expected_moves);
}
#[test]
fn test_bishop_moves_single() {
let fen_standard = "8/8/8/1p1p4/2B5/8/8/8 w - - 0 1";
let board = Board::from_fen(fen_standard);
let mut list = MoveList::new();
generate_bishop_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "c4b5 c4d5 c4b3 c4d3 c4a2 c4e2 c4f1");
}
#[test]
fn test_bishop_moves_empty_board_center() {
// Läufer in der Mitte (e4) auf einem leeren Brett
let fen = "8/8/8/8/4B3/8/8/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_bishop_moves(&board, &mut list);
let expected = "e4d3 e4c2 e4b1 e4f5 e4g6 e4h7 e4d5 e4c6 e4b7 e4a8 e4f3 e4g2 e4h1";
assert_moves_equal(&list.to_string(), expected);
}
#[test]
fn test_bishop_moves_from_corner() {
// Läufer in der Ecke (a1) auf einem leeren Brett
let fen = "8/8/8/8/8/8/8/B7 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_bishop_moves(&board, &mut list);
let expected = "a1b2 a1c3 a1d4 a1e5 a1f6 a1g7 a1h8";
assert_moves_equal(&list.to_string(), expected);
}
#[test]
fn test_bishop_moves_friendly_blockers() {
// Läufer auf c1, blockiert von EIGENEN Bauern auf b2 und d2.
// Darf b2/d2 NICHT schlagen und nicht darüber springen.
let fen = "8/8/8/8/8/8/1P1P4/2B5 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_bishop_moves(&board, &mut list);
// Es sollten KEINE Züge generiert werden.
assert_moves_equal(&list.to_string(), "");
}
#[test]
fn test_bishop_moves_mixed_blockers_and_captures() {
let fen = "8/8/1p3P2/8/3B4/8/1p3P2/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_bishop_moves(&board, &mut list);
let expected = "d4c3 d4e3 d4b2 d4c5 d4b6 d4e5";
assert_moves_equal(&list.to_string(), expected);
}
#[test]
fn test_bishop_moves_black_turn() {
// Schwarzer Läufer auf c5, weiße Bauern auf b4 und d4.
let fen = "8/8/8/2b5/1P1P4/8/8/8 b - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_bishop_moves(&board, &mut list);
// Schlagzüge: b4, d4
// Ruhige Züge: b6, a7, d6, e7, f8
let expected = "c5b4 c5d4 c5b6 c5a7 c5d6 c5e7 c5f8";
assert_moves_equal(&list.to_string(), expected);
}

View file

@ -1,52 +0,0 @@
use chess_engine::board::{Board, Color};
use chess_engine::movegen::legal_check::is_square_attacked;
use chess_engine::square::Square;
fn assert_square_attacked(board: &mut Board, square: Square, white: bool, black: bool) {
assert_eq!(is_square_attacked(board, square, Color::White), white, "{}", square);
assert_eq!(is_square_attacked(board, square, Color::Black), black, "{}", square);
}
#[test]
fn test_is_attacked_kiwipete() {
let mut board = Board::from_fen("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1");
assert_square_attacked(&mut board, Square::B1, true, false);
assert_square_attacked(&mut board, Square::C1, true, false);
assert_square_attacked(&mut board, Square::D1, true, false);
assert_square_attacked(&mut board, Square::F1, true, false);
assert_square_attacked(&mut board, Square::G1, true, false);
assert_square_attacked(&mut board, Square::A3, true, true);
assert_square_attacked(&mut board, Square::B3, true, false);
assert_square_attacked(&mut board, Square::D3, true, true);
assert_square_attacked(&mut board, Square::E3, true, false);
assert_square_attacked(&mut board, Square::G3, true, false);
assert_square_attacked(&mut board, Square::A4, true, true);
assert_square_attacked(&mut board, Square::C4, true, true);
assert_square_attacked(&mut board, Square::D4, false, false);
assert_square_attacked(&mut board, Square::F4, true, false);
assert_square_attacked(&mut board, Square::G4, true, true);
assert_square_attacked(&mut board, Square::H4, false, true);
assert_square_attacked(&mut board, Square::A5, false, false);
assert_square_attacked(&mut board, Square::B5, true, true);
assert_square_attacked(&mut board, Square::C5, false, true);
assert_square_attacked(&mut board, Square::F5, true, true);
assert_square_attacked(&mut board, Square::G5, true, false);
assert_square_attacked(&mut board, Square::H5, true, true);
assert_square_attacked(&mut board, Square::C6, true, true);
assert_square_attacked(&mut board, Square::D6, false, true);
assert_square_attacked(&mut board, Square::H6, true, true);
assert_square_attacked(&mut board, Square::B7, false, true);
assert_square_attacked(&mut board, Square::H7, false, true);
assert_square_attacked(&mut board, Square::B8, false, true);
assert_square_attacked(&mut board, Square::C8, false, true);
assert_square_attacked(&mut board, Square::D8, false, true);
assert_square_attacked(&mut board, Square::F8, false, true);
assert_square_attacked(&mut board, Square::G8, false, true);
}

View file

@ -1,145 +0,0 @@
use chess_engine::board::Board;
use chess_engine::movegen::non_sliders::generate_king_moves;
use chess_engine::r#move::MoveList;
/// Compares two move list strings ignoring the order of moves.
fn assert_moves_equal(actual_str: &str, expected_str: &str) {
let mut actual_moves: Vec<&str> = actual_str.split_whitespace().collect();
let mut expected_moves: Vec<&str> = expected_str.split_whitespace().collect();
actual_moves.sort();
expected_moves.sort();
assert_eq!(actual_moves, expected_moves);
}
#[test]
fn test_king_moves_start_pos_blocked() {
let fen_standard = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
let board = Board::from_fen(fen_standard);
let mut list = MoveList::new();
generate_king_moves(&board, &mut list);
// King is completely blocked in the start position
assert_moves_equal(&list.to_string(), "");
}
#[test]
fn test_king_moves_center() {
let fen = "8/8/8/8/4K3/8/8/8 w - - 0 1"; // King on e4
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_king_moves(&board, &mut list);
// 8 moves from e4
assert_moves_equal(&list.to_string(), "e4d3 e4d4 e4d5 e4e3 e4e5 e4f3 e4f4 e4f5");
}
#[test]
fn test_king_moves_corner() {
let fen = "K7/8/8/8/8/8/8/8 w - - 0 1"; // King on a8
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_king_moves(&board, &mut list);
// 3 moves from a8
assert_moves_equal(&list.to_string(), "a8a7 a8b7 a8b8");
}
#[test]
fn test_king_moves_blocked_friendly() {
// King on d4, surrounded by friendly pawns
let fen = "8/8/8/3P1P2/3K4/3P1P2/8/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_king_moves(&board, &mut list);
// d3, d5, f3, f5 are blocked.
// c3, c4, c5, e3, e4, e5 are free.
assert_moves_equal(&list.to_string(), "d4c3 d4c4 d4c5 d4e3 d4e4 d4e5");
}
#[test]
fn test_king_moves_capture_and_blocked() {
// King on d4
// Friendly: c3, e5
// Enemy: c5, e3
let fen = "8/8/8/2p1P3/3K4/2P1p3/8/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_king_moves(&board, &mut list);
// Blocked: c3, e5
// Captures: c5, e3
// Empty: c4, d3, d5, e4, f3, f4, f5
// Note: f3, f4, f5 are valid moves
assert_moves_equal(&list.to_string(), "d4c4 d4d3 d4d5 d4e4 d4c5 d4e3");
}
#[test]
fn test_king_moves_black() {
let fen = "8/8/8/8/4k3/8/8/8 b - - 0 1"; // Black king on e4
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_king_moves(&board, &mut list);
// 8 moves from e4
assert_moves_equal(&list.to_string(), "e4d3 e4d4 e4d5 e4e3 e4e5 e4f3 e4f4 e4f5");
}
#[test]
fn test_king_moves_castling_white_all() {
// King on e1, rooks on a1, h1. All rights. No pieces between.
let fen = "r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_king_moves(&board, &mut list);
// 5 standard moves + 2 castling moves (e1g1, e1c1)
assert_moves_equal(&list.to_string(), "e1d1 e1d2 e1e2 e1f1 e1f2 O-O O-O-O");
}
#[test]
fn test_king_moves_castling_black_all() {
// King on e8, rooks on a8, h8. All rights. No pieces between.
let fen = "r3k2r/8/8/8/8/8/8/R3K2R b KQkq - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_king_moves(&board, &mut list);
// 5 standard moves + 2 castling moves (e8g8, e8c8)
assert_moves_equal(&list.to_string(), "O-O O-O-O e8d8 e8e7 e8f8 e8d7 e8f7");
}
#[test]
fn test_king_moves_castling_blocked_pieces_white() {
// White: Queenside blocked by knight
let fen = "r3k2r/8/8/8/8/8/8/RN2K2R w KQkq - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_king_moves(&board, &mut list);
// Should only get Kingside castling (e1g1) and standard moves
assert_moves_equal(&list.to_string(), "e1d1 e1d2 e1e2 e1f1 e1f2 O-O");
}
#[test]
fn test_king_moves_castling_blocked_pieces_black() {
// Black: Kingside blocked by bishop
let fen = "r3kb1r/8/8/8/8/8/8/R3K2R b KQkq - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_king_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "e8d8 e8d7 e8e7 e8f7 O-O-O");
}
#[test]
fn test_king_moves_castling_no_rights() {
// Same as `test_king_moves_castling_white_all` but no castling rights
let fen = "r3k2r/8/8/8/8/8/8/R3K2R w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_king_moves(&board, &mut list);
// 5 standard moves, 0 castling
assert_moves_equal(&list.to_string(), "e1d1 e1d2 e1e2 e1f1 e1f2");
}
#[test]
fn test_king_moves_empty_board() {
let fen = "8/8/8/8/8/8/8/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_king_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "");
}

View file

@ -1,96 +0,0 @@
use chess_engine::board::Board;
use chess_engine::movegen::non_sliders::generate_knight_moves;
use chess_engine::r#move::MoveList;
/// Compares two move list strings ignoring the order of moves.
fn assert_moves_equal(actual_str: &str, expected_str: &str) {
let mut actual_moves: Vec<&str> = actual_str.split_whitespace().collect();
let mut expected_moves: Vec<&str> = expected_str.split_whitespace().collect();
actual_moves.sort();
expected_moves.sort();
assert_eq!(actual_moves, expected_moves);
}
#[test]
fn test_knight_move_generation() {
let fen_standard = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
let board = Board::from_fen(fen_standard);
let mut list = MoveList::new();
generate_knight_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "b1a3 b1c3 g1f3 g1h3");
}
#[test]
fn test_knight_move_generation_black() {
let fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_knight_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "b8a6 b8c6 g8f6 g8h6");
}
#[test]
fn test_knight_moves_center() {
let fen = "8/8/8/3N4/8/8/8/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_knight_moves(&board, &mut list);
// 8 moves from d5
assert_moves_equal(&list.to_string(), "d5b4 d5b6 d5c3 d5c7 d5e3 d5e7 d5f4 d5f6");
}
#[test]
fn test_knight_moves_capture() {
// Black knight on e5, white pawn on d3
let fen = "8/8/8/4n3/8/3P4/8/8 b - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_knight_moves(&board, &mut list);
// 8 moves, including capture on d3
assert_moves_equal(&list.to_string(), "e5c4 e5c6 e5d3 e5d7 e5f3 e5f7 e5g4 e5g6");
}
#[test]
fn test_knight_moves_blocked_friendly() {
// Knight on d4, some moves are blocked, some are not.
let fen = "8/8/3P1P2/3P1P2/3N4/3P1P2/3P1P2/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_knight_moves(&board, &mut list);
// f3, f5, d3, d5, d6, f6 are blocked.
// c2, e2, b3, b5, c6, e6 are free.
assert_moves_equal(&list.to_string(), "d4c2 d4e2 d4b3 d4b5 d4c6 d4e6");
}
#[test]
fn test_knight_moves_corner() {
let fen = "N7/8/8/8/8/8/8/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_knight_moves(&board, &mut list);
// 2 moves from a8
assert_moves_equal(&list.to_string(), "a8b6 a8c7");
}
#[test]
fn test_knight_moves_capture_and_blocked() {
// White knights on b1 and g1.
// b1: a3 (friendly), c3 (friendly), d2 (enemy)
// g1: f3 (empty), h3 (empty), e2 (empty)
let fen = "8/8/8/8/8/P1P5/3p4/1N4NR w K - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_knight_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "b1d2 g1e2 g1f3 g1h3");
}
#[test]
fn test_knight_moves_empty_board() {
let fen = "8/8/8/8/8/8/8/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_knight_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "");
}

View file

@ -1,108 +0,0 @@
use chess_engine::board::Board;
use chess_engine::movegen::generate_pseudo_legal_moves;
use chess_engine::r#move::MoveList;
/// Helper function to run the make/undo integrity check for any given board state.
/// This avoids code duplication in all the specific test cases.
fn assert_make_undo_integrity(board: &mut Board) {
let mut list = MoveList::new();
generate_pseudo_legal_moves(&board, &mut list);
// If no moves are generated (e.g., stalemate/checkmate), the test passes.
if list.is_empty() {
return;
}
for mv in list.iter() {
let board_before_make = board.to_fen();
let undo_obj = board.make_move(*mv);
let board_after_make = board.to_fen();
// Ensure the board actually changed
assert_ne!(
board_before_make, board_after_make,
"Board did not change after make_move for move: {:?}", mv
);
board.undo_move(undo_obj);
let board_after_undo = board.to_fen();
// Ensure the board is perfectly restored
assert_eq!(
board_before_make, board_after_undo,
"Board state mismatch after undo_move for move: {:?}", mv
);
}
}
#[test]
fn test_make_undo_standard() {
let fen_standard = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
let mut board = Board::from_fen(fen_standard);
assert_make_undo_integrity(&mut board);
}
#[test]
/// Tests a complex, mid-game position with many interactions.
/// This is the famous "Kiwipete" FEN used for perft testing.
fn test_make_undo_kiwipete() {
let fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1";
let mut board = Board::from_fen(fen);
assert_make_undo_integrity(&mut board);
}
#[test]
/// Tests the en passant capture for White (e.g., e5xd6).
/// This ensures the black pawn on d5 is correctly removed and restored.
fn test_make_undo_en_passant_white() {
// Position after 1. e4 e6 2. e5 d5
let fen = "rnbqkbnr/ppp1p1pp/8/3pPp2/8/8/PPP2PPP/RNBQKBNR w KQkq d6 0 3";
let mut board = Board::from_fen(fen);
assert_make_undo_integrity(&mut board);
}
#[test]
/// Tests the en passant capture for Black (e.g., d5xe6).
/// This ensures the white pawn on e5 is correctly removed and restored.
fn test_make_undo_en_passant_black() {
// Position after 1. d4 c5 2. d5 e5
let fen = "rnbqkbnr/pp1p1ppp/8/2pPp3/8/8/PPP1PPPP/RNBQKBNR b KQkq e6 0 3";
let mut board = Board::from_fen(fen);
assert_make_undo_integrity(&mut board);
}
#[test]
/// Tests White's kingside (O-O) and queenside (O-O-O) castling.
/// Ensures both rook and king moves are correctly undone.
fn test_make_undo_castling_white() {
let fen = "r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1";
let mut board = Board::from_fen(fen);
assert_make_undo_integrity(&mut board);
}
#[test]
/// Tests Black's kingside (O-O) and queenside (O-O-O) castling.
/// Ensures both rook and king moves are correctly undone.
fn test_make_undo_castling_black() {
let fen = "r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R b KQkq - 0 1";
let mut board = Board::from_fen(fen);
assert_make_undo_integrity(&mut board);
}
#[test]
/// Tests white pawn promotions (quiet and capture) to Q, R, B, N.
fn test_make_undo_promotions_white() {
// White pawn on c7, black rooks on b8 and d8 to test capture-promotions
let fen = "1r1rkb1r/2Ppppb1/8/8/8/8/1PPPPPP1/RNBQKBNR w KQk - 0 1";
let mut board = Board::from_fen(fen);
assert_make_undo_integrity(&mut board);
}
#[test]
/// Tests black pawn promotions (quiet and capture) to q, r, b, n.
fn test_make_undo_promotions_black() {
// Black pawn on g2, white rooks on f1 and h1 to test capture-promotions
let fen = "RNBQKBNR/1PPPPP2/8/8/8/8/6p1/R4RK1 b Qkq - 0 1";
let mut board = Board::from_fen(fen);
assert_make_undo_integrity(&mut board);
}

View file

@ -1,92 +0,0 @@
use chess_engine::r#move::*;
use chess_engine::square::Square;
#[test]
fn test_quiet_move_white_pawn() {
// Test 1: Standard Quiet Move (White Pawn)
// (from: E2, to: E4)
// NOTE: This was MOVE_TYPE_FLAG_QUIET, but in the new system it's a specific flag.
// The algebraic notation is the same, so we test with the new specific flag.
let m_quiet = Move::new(Square::E2, Square::E4, MOVE_FLAG_QUIET);
assert_eq!(m_quiet.to_algebraic(), "e2e4");
}
#[test]
fn test_quiet_move_black_knight() {
// Test 2: Standard Quiet Move (Black Knight)
// (from: B8, to: C6)
let m_knight = Move::new(Square::B8, Square::C6, MOVE_FLAG_QUIET);
assert_eq!(m_knight.to_algebraic(), "b8c6");
}
#[test]
fn test_en_passant_move() {
// Test 3: En Passant Move (Notation is same as quiet move)
// (from: E5, to: F6)
let m_ep = Move::new(Square::E5, Square::F6, MOVE_FLAG_EN_PASSANT);
assert_eq!(m_ep.to_algebraic(), "e5f6");
}
#[test]
fn test_promotion_to_queen() {
// Test 4: Promotion to Queen (Push)
// (from: E7, to: E8)
let m_promo_q = Move::new(Square::E7, Square::E8, MOVE_FLAG_PROMO_Q);
assert_eq!(m_promo_q.to_algebraic(), "e7e8q");
}
#[test]
fn test_promotion_to_rook() {
// Test 5: Promotion to Rook (Push)
// (from: A7, to: A8)
let m_promo_r = Move::new(Square::A7, Square::A8, MOVE_FLAG_PROMO_R);
assert_eq!(m_promo_r.to_algebraic(), "a7a8r");
}
#[test]
fn test_promotion_to_bishop() {
// Test 6: Promotion to Bishop (Capture)
// (from: G2, to: H1)
let m_promo_b = Move::new(Square::G2, Square::H1, MOVE_FLAG_PROMO_B_CAP);
assert_eq!(m_promo_b.to_algebraic(), "g2h1b");
}
#[test]
fn test_promotion_to_knight() {
// Test 7: Promotion to Knight (Capture)
// (from: G7, to: F8)
let m_promo_n = Move::new(Square::G7, Square::F8, MOVE_FLAG_PROMO_N_CAP);
assert_eq!(m_promo_n.to_algebraic(), "g7f8n");
}
#[test]
fn test_white_kingside_castling() {
// Test 8: White Kingside Castling
// (from: E1, to: G1)
let m_castle_wk = Move::new(Square::E1, Square::G1, MOVE_FLAG_WK_CASTLE);
assert_eq!(m_castle_wk.to_algebraic(), "O-O");
}
#[test]
fn test_white_queenside_castling() {
// Test 9: White Queenside Castling
// (from: E1, to: C1)
let m_castle_wq = Move::new(Square::E1, Square::C1, MOVE_FLAG_WQ_CASTLE);
assert_eq!(m_castle_wq.to_algebraic(), "O-O-O");
}
#[test]
fn test_black_kingside_castling() {
// Test 10: Black Kingside Castling
// (from: E8, to: G8)
let m_castle_bk = Move::new(Square::E8, Square::G8, MOVE_FLAG_BK_CASTLE);
assert_eq!(m_castle_bk.to_algebraic(), "O-O");
}
#[test]
fn test_black_queenside_castling() {
// Test 11: Black Queenside Castling
// (from: E8, to: C8)
let m_castle_bq = Move::new(Square::E8, Square::C8, MOVE_FLAG_BQ_CASTLE);
assert_eq!(m_castle_bq.to_algebraic(), "O-O-O");
}

View file

@ -1,142 +0,0 @@
use chess_engine::board::Board;
use chess_engine::movegen::pawns::generate_pawn_moves;
use chess_engine::r#move::MoveList;
/// Compares two move list strings ignoring the order of moves.
fn assert_moves_equal(actual_str: &str, expected_str: &str) {
let mut actual_moves: Vec<&str> = actual_str.split_whitespace().collect();
let mut expected_moves: Vec<&str> = expected_str.split_whitespace().collect();
actual_moves.sort();
expected_moves.sort();
assert_eq!(actual_moves, expected_moves);
}
#[test]
fn test_pawn_moves_start_pos() {
let fen_standard = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
let board = Board::from_fen(fen_standard);
let mut list = MoveList::new();
generate_pawn_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "a2a3 a2a4 b2b3 b2b4 c2c3 c2c4 d2d3 d2d4 e2e3 e2e4 f2f3 f2f4 g2g3 g2g4 h2h3 h2h4");
}
#[test]
fn test_pawn_moves_black_start_pos() {
let fen_standard = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1";
let board = Board::from_fen(fen_standard);
let mut list = MoveList::new();
generate_pawn_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "a7a6 a7a5 b7b6 b7b5 c7c6 c7c5 d7d6 d7d5 e7e6 e7e5 f7f6 f7f5 g7g6 g7g5 h7h6 h7h5");
}
#[test]
fn test_pawn_moves_black_blocked_and_captures() {
// Black pawns on a7, c7, e7, g7. White pawns on b6, d6, f6.
let fen = "8/p1p1p1p1/1P1P1P2/8/8/8/8/8 b - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_pawn_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "a7a6 a7a5 a7b6 c7c6 c7c5 c7b6 c7d6 e7e6 e7e5 e7d6 e7f6 g7g6 g7g5 g7f6");
}
#[test]
fn test_pawn_moves_black_side_captures() {
// Test captures on A and H files (no wrap-around)
let fen = "8/8/8/8/8/1p4p1/P6P/8 b - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_pawn_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "b3b2 b3a2 g3g2 g3h2");
}
#[test]
fn test_pawn_moves_white_side_captures() {
// Test captures on A and H files (no wrap-around)
let fen = "8/p6p/1P4P1/8/8/8/8/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_pawn_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "b6b7 b6a7 g6g7 g6h7");
}
#[test]
fn test_pawn_moves_black_en_passant() {
// White just moved e2e4, en passant target is e3. Black pawn on d4.
let fen = "rnbqkbnr/ppp1pppp/8/8/3pP3/8/PPP2PPP/RNBQKBNR b KQkq e3 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_pawn_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "a7a6 a7a5 b7b6 b7b5 c7c6 c7c5 d4d3 d4e3 e7e6 e7e5 f7f6 f7f5 g7g6 g7g5 h7h6 h7h5");
}
#[test]
fn test_pawn_moves_white_en_passant() {
// Black just moved d7d5, en passant target is d6. White pawn on e5.
let fen = "rnbqkbnr/ppp1pppp/8/3pP3/8/8/PPPP1PPP/RNBQKBNR w KQkq d6 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_pawn_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "a2a3 a2a4 b2b3 b2b4 c2c3 c2c4 d2d3 d2d4 e5e6 e5d6 f2f3 f2f4 g2g3 g2g4 h2h3 h2h4");
}
#[test]
fn test_pawn_moves_black_promotion() {
let fen = "8/8/8/8/8/8/p1p1p1p1/8 b - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_pawn_moves(&board, &mut list);
assert_moves_equal(&list.to_string(),
"a2a1q a2a1r a2a1b a2a1n c2c1q c2c1r c2c1b c2c1n e2e1q e2e1r e2e1b e2e1n g2g1q g2g1r g2g1b g2g1n");
}
#[test]
fn test_pawn_moves_white_promotion() {
let fen = "8/P1P1P1P1/8/8/8/8/8/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_pawn_moves(&board, &mut list);
assert_moves_equal(&list.to_string(),
"a7a8q a7a8r a7a8b a7a8n c7c8q c7c8r c7c8b c7c8n e7e8q e7e8r e7e8b e7e8n g7g8q g7g8r g7g8b g7g8n");
}
#[test]
fn test_pawn_moves_black_promotion_capture() {
// Black pawn on b2. White rooks on a1 and c1.
let fen = "8/8/8/8/8/8/1p6/R1R5 b - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_pawn_moves(&board, &mut list);
assert_moves_equal(&list.to_string(),
"b2b1q b2b1r b2b1b b2b1n b2a1q b2a1r b2a1b b2a1n b2c1q b2c1r b2c1b b2c1n");
}
#[test]
fn test_pawn_moves_white_promotion_capture() {
// White pawn on a7. Black rooks on b8 and d8.
let fen = "1r1r4/P7/8/8/8/8/8/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_pawn_moves(&board, &mut list);
assert_moves_equal(&list.to_string(),
"a7a8q a7a8r a7a8b a7a8n a7b8q a7b8r a7b8b a7b8n");
}
#[test]
fn test_pawn_moves_black_midgame_random() {
let fen = "r1bqkb1r/1p2pppp/p1n2n2/3p4/2BNP3/2N5/PPP2PPP/R1BQK2R b KQkq - 1 6";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_pawn_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "a6a5 b7b6 b7b5 d5c4 d5e4 e7e6 e7e5 g7g6 g7g5 h7h6 h7h5");
}
#[test]
fn test_pawn_moves_white_midgame_random() {
let fen = "r1bqk2r/2ppbppp/p1n2n2/1p2p3/B1P1P3/5N2/PP1P1PPP/RNBQR1K1 w kq - 0 7";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_pawn_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "a2a3 b2b3 b2b4 c4c5 c4b5 d2d3 d2d4 g2g3 g2g4 h2h3 h2h4");
}

View file

@ -1,6 +1,6 @@
use chess_engine::board::Board; use chess_engine::board::Board;
use chess_engine::movegen::generate_pseudo_legal_moves; use chess_engine::movegen::generate_pseudo_legal_moves;
use chess_engine::movegen::legal_check::is_king_attacked; use chess_engine::movegen::legal_check::is_other_king_attacked;
use chess_engine::r#move::MoveList; use chess_engine::r#move::MoveList;
fn count_legal_moves_recursive(board: &mut Board, depth: u8) -> u64 { fn count_legal_moves_recursive(board: &mut Board, depth: u8) -> u64 {
@ -16,7 +16,7 @@ fn count_legal_moves_recursive(board: &mut Board, depth: u8) -> u64 {
// Store the undo info when making the move // Store the undo info when making the move
let undo_info = board.make_move(*mv); let undo_info = board.make_move(*mv);
if !is_king_attacked(board) { if !is_other_king_attacked(board) {
leaf_nodes += count_legal_moves_recursive(board, depth - 1); leaf_nodes += count_legal_moves_recursive(board, depth - 1);
} }

View file

@ -1,93 +0,0 @@
use chess_engine::board::Board;
use chess_engine::movegen::sliders::generate_queen_moves;
use chess_engine::r#move::MoveList;
/// Compares two move list strings ignoring the order of moves.
fn assert_moves_equal(actual_str: &str, expected_str: &str) {
let mut actual_moves: Vec<&str> = actual_str.split_whitespace().collect();
let mut expected_moves: Vec<&str> = expected_str.split_whitespace().collect();
actual_moves.sort();
expected_moves.sort();
assert_eq!(actual_moves, expected_moves);
}
#[test]
fn test_queen_moves_single_rook() {
let fen_standard = "8/1Q2p3/8/8/8/8/6p1/8 w - - 0 1";
let board = Board::from_fen(fen_standard);
let mut list = MoveList::new();
generate_queen_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "b7a8 b7a7 b7a6 b7b8 b7b6 b7b5 b7b4 b7b3 b7b2 b7b1 b7c8 b7c7 b7c6 b7d7 b7e7 b7d5 b7e4 b7f3 b7g2");
}
#[test]
fn test_queen_center_open() {
// Queen in the center, open board, should generate 27 moves
let fen = "4k3/8/8/8/3Q4/8/8/4K3 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_queen_moves(&board, &mut list);
let expected = "d4a1 d4b2 d4c3 d4e5 d4f6 d4g7 d4h8 \
d4a4 d4b4 d4c4 d4e4 d4f4 d4g4 d4h4 \
d4d1 d4d2 d4d3 d4d5 d4d6 d4d7 d4d8 \
d4a7 d4b6 d4c5 d4e3 d4f2 d4g1";
assert_moves_equal(&list.to_string(), expected);
}
#[test]
fn test_queen_corner_blocked_friendly() {
// Queen in corner, completely blocked by friendly pieces
let fen = "rnbqkbnr/pppppppp/8/8/8/8/PP1PP1PP/QNBQKBNR w Kkq - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_queen_moves(&board, &mut list);
assert_moves_equal(&list.to_string(), "d1a4 d1b3 d1c2");
}
#[test]
fn test_queen_multiple_captures_black() {
// Black queen on h8, with multiple white pieces to capture
let fen = "q3k3/P1P1P1P1/8/8/8/P1P1P1P1/8/4K3 b - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_queen_moves(&board, &mut list);
let expected = "a8b8 a8c8 a8d8 a8a7 a8b7 a8c6 a8d5 a8e4 a8f3 a8g2 a8h1";
assert_moves_equal(&list.to_string(), expected);
}
#[test]
fn test_multiple_queens() {
let fen = "4k3/8/8/8/8/8/8/Q3K2Q w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_queen_moves(&board, &mut list);
let expected = "a1a2 a1a3 a1a4 a1a5 a1a6 a1a7 a1a8 a1b1 a1c1 a1d1 a1b2 a1c3 a1d4 a1e5 a1f6 a1g7 a1h8 \
h1h2 h1h3 h1h4 h1h5 h1h6 h1h7 h1h8 h1g1 h1f1 h1g2 h1f3 h1e4 h1d5 h1c6 h1b7 h1a8";
assert_moves_equal(&list.to_string(), expected);
}
#[test]
fn test_queen_rook_only() {
// Queen on d4, bishop moves blocked by friendly pawns
let fen = "4k3/8/8/2P1P3/3Q4/2P1P3/8/4K3 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_queen_moves(&board, &mut list);
let expected = "d4a4 d4b4 d4c4 d4e4 d4f4 d4g4 d4h4 \
d4d1 d4d2 d4d3 d4d5 d4d6 d4d7 d4d8";
assert_moves_equal(&list.to_string(), expected);
}
#[test]
fn test_queen_bishop_only() {
// Queen on d4, rook moves blocked by friendly pawns
let fen = "4k3/8/8/3P4/2PQP3/3P4/8/4K3 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_queen_moves(&board, &mut list);
let expected = "d4a1 d4b2 d4c3 d4e5 d4f6 d4g7 d4h8 \
d4a7 d4b6 d4c5 d4e3 d4f2 d4g1";
assert_moves_equal(&list.to_string(), expected);
}

View file

@ -1,101 +0,0 @@
use chess_engine::board::Board;
use chess_engine::movegen::sliders::generate_rook_moves;
use chess_engine::r#move::MoveList;
/// Compares two move list strings ignoring the order of moves.
fn assert_moves_equal(actual_str: &str, expected_str: &str) {
let mut actual_moves: Vec<&str> = actual_str.split_whitespace().collect();
let mut expected_moves: Vec<&str> = expected_str.split_whitespace().collect();
actual_moves.sort();
expected_moves.sort();
assert_eq!(actual_moves, expected_moves);
}
#[test]
fn test_rook_moves_single_rook() {
let fen_standard = "8/8/8/2b5/2Rb4/2b5/8/8 w - - 0 1";
let board = Board::from_fen(fen_standard);
let mut list = MoveList::new();
generate_rook_moves(&board, &mut list);
// This FEN has a White Rook at c4, and Black Bishops at c5, d4, and c3.
// It should be able to capture all three and move left to a4/b4.
assert_moves_equal(&list.to_string(), "c4a4 c4b4 c4c3 c4c5 c4d4");
}
#[test]
fn test_rook_moves_empty_board() {
let fen = "8/8/8/8/3R4/8/8/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_rook_moves(&board, &mut list);
let expected = "d4d1 d4d2 d4d3 d4d5 d4d6 d4d7 d4d8 d4a4 d4b4 d4c4 d4e4 d4f4 d4g4 d4h4";
assert_moves_equal(&list.to_string(), expected);
}
#[test]
fn test_rook_moves_corner_blocked_black() {
let fen = "r6k/1p6/8/8/8/8/8/K7 b - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_rook_moves(&board, &mut list);
// Black rook at a8. Friendly king at h8, friendly pawn at b7.
// Rook can move down the a-file and right along the 8th rank, stopping before h8.
let expected = "a8a7 a8a6 a8a5 a8a4 a8a3 a8a2 a8a1 a8b8 a8c8 a8d8 a8e8 a8f8 a8g8";
assert_moves_equal(&list.to_string(), expected);
}
#[test]
fn test_rook_moves_double_rooks_friendly_block() {
let fen = "8/8/8/8/R3R3/8/8/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_rook_moves(&board, &mut list);
// Rooks at a4 and e4. They block each other horizontally.
// Rook a4 moves a1-a8 and b4, c4, d4.
// Rook e4 moves e1-e8, f4, g4, h4 AND d4, c4, b4.
let expected = "a4a1 a4a2 a4a3 a4a5 a4a6 a4a7 a4a8 a4b4 a4c4 a4d4 \
e4e1 e4e2 e4e3 e4e5 e4e6 e4e7 e4e8 e4f4 e4g4 e4h4 \
e4d4 e4c4 e4b4";
assert_moves_equal(&list.to_string(), expected);
}
#[test]
fn test_rook_moves_capture_stops_movegen() {
let fen = "r7/P7/8/8/8/8/8/8 b - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_rook_moves(&board, &mut list);
// Black rook at a8, White pawn at a7.
// The rook can capture at a7, but cannot move past it.
// It can still move horizontally.
let expected = "a8a7 a8b8 a8c8 a8d8 a8e8 a8f8 a8g8 a8h8";
assert_moves_equal(&list.to_string(), expected);
}
#[test]
fn test_rook_moves_completely_blocked_friendly() {
let fen = "8/8/8/1P6/PRP5/1P6/8/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_rook_moves(&board, &mut list);
// White rook at b4.
// Blocked by P(b5), P(b3), P(a4), P(c4).
// Should have 0 moves.
assert_moves_equal(&list.to_string(), "");
}
#[test]
fn test_rook_moves_ignores_absolute_pin() {
let fen = "r3k3/8/8/8/R3K3/8/8/8 w - - 0 1";
let board = Board::from_fen(fen);
let mut list = MoveList::new();
generate_rook_moves(&board, &mut list);
// White rook at a4 is absolutely pinned to King at e4 by Black rook at a8.
// A pseudo-legal generator should *ignore* the pin.
// It should generate vertical moves (including capture at a8)
// and horizontal moves (stopping before the friendly King at e4).
let expected = "a4a1 a4a2 a4a3 a4a5 a4a6 a4a7 a4a8 a4b4 a4c4 a4d4";
assert_moves_equal(&list.to_string(), expected);
}