diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a607d3d..d7e08cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: run: cargo build --verbose - name: Run tests working-directory: ./chusst-gen - run: cargo test --verbose + run: cargo test --release --verbose - name: Install React dependencies run: npm install if: startsWith(github.ref, 'refs/tags/') @@ -66,7 +66,7 @@ jobs: run: cargo build --verbose - name: Run tests working-directory: ./chusst-gen - run: cargo test --verbose + run: cargo test --release --verbose - name: Install React dependencies run: npm install - name: Install Rust dependencies diff --git a/Cargo.lock b/Cargo.lock index 917a442..60febe9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,18 @@ version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "async-stream" version = "0.3.5" @@ -256,12 +268,6 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" -[[package]] -name = "bencher" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfdb4953a096c551ce9ace855a604d702e6e62d77fac690575ae347571717f5" - [[package]] name = "bitflags" version = "1.3.2" @@ -320,6 +326,15 @@ dependencies = [ "serde", ] +[[package]] +name = "btoi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd6407f73a9b8b6162d8a2ef999fe6afd7cc15902ebf42c5cd296addf17e0ad" +dependencies = [ + "num-traits", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -429,6 +444,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chess" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ed299b171ec34f372945ad6726f7bc1d2afd5f59fb8380f64f48e2bab2f0ec8" +dependencies = [ + "arrayvec 0.5.2", + "failure", + "nodrop", + "rand 0.7.3", +] + [[package]] name = "chrono" version = "0.4.31" @@ -459,11 +486,13 @@ version = "0.10.0" dependencies = [ "anyhow", "atty", - "bencher", + "chess", "colored", + "divan", "lazy_static", "rand 0.8.5", "serde", + "shakmaty", ] [[package]] @@ -499,6 +528,7 @@ dependencies = [ "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] @@ -581,6 +611,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "condtype" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" + [[package]] name = "console-api" version = "0.6.0" @@ -866,6 +902,31 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "divan" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d567df2c9c2870a43f3f2bd65aaeb18dbce1c18f217c3e564b4fbaeb3ee56c" +dependencies = [ + "cfg-if", + "clap", + "condtype", + "divan-macros", + "libc", + "regex-lite", +] + +[[package]] +name = "divan-macros" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27540baf49be0d484d8f0130d7d8da3011c32a44d4fc873368154f1510e574a2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "dtoa" version = "1.0.9" @@ -937,6 +998,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + [[package]] name = "fastrand" version = "2.0.1" @@ -2707,6 +2790,12 @@ dependencies = [ "regex-syntax 0.8.2", ] +[[package]] +name = "regex-lite" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" + [[package]] name = "regex-syntax" version = "0.6.29" @@ -2970,6 +3059,17 @@ dependencies = [ "digest", ] +[[package]] +name = "shakmaty" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2320d3932d5b410b2bdee568152b51f2373f018d3425dc7b424ab9b23a5d13" +dependencies = [ + "arrayvec 0.7.4", + "bitflags 2.4.1", + "btoi", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3119,6 +3219,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "system-deps" version = "5.0.0" @@ -3431,6 +3543,16 @@ dependencies = [ "utf-8", ] +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "thin-slice" version = "0.1.1" @@ -3811,6 +3933,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "unsafe-libyaml" version = "0.2.10" diff --git a/chusst-gen/Cargo.toml b/chusst-gen/Cargo.toml index bebc242..c0d7bec 100644 --- a/chusst-gen/Cargo.toml +++ b/chusst-gen/Cargo.toml @@ -14,7 +14,9 @@ rand = "0.8.5" serde = { version = "1.0.195", features = ["derive"] } [dev-dependencies] -bencher = "0.1.5" +chess = "3.2.0" +divan = "0.1.14" +shakmaty = "0.27.0" [[bench]] name = "search" diff --git a/chusst-gen/benches/search.rs b/chusst-gen/benches/search.rs index 1570513..81f9125 100644 --- a/chusst-gen/benches/search.rs +++ b/chusst-gen/benches/search.rs @@ -1,24 +1,122 @@ +use divan::Bencher; + use chusst_gen::eval::{Game, SilentSearchFeedback}; use chusst_gen::game::BitboardGame; -#[macro_use] -extern crate bencher; - -use bencher::Bencher; - -fn search(bench: &mut Bencher) { - let mut searched = 0u64; - bench.iter(|| { +#[divan::bench] +fn search(bench: Bencher) { + bench.bench_local(|| { let game = BitboardGame::new(); let best_branch = game .get_best_move_recursive(4, &mut (), &mut SilentSearchFeedback::default()) .unwrap(); - searched = u64::from(best_branch.searched); + best_branch.searched }); +} - bench.bytes = searched; +#[divan::bench_group] +mod perft { + struct PerftDepth { + pub depth: u8, + pub moves: u64, + } + + const PERFT_DEPTHS: [PerftDepth; 6] = [ + PerftDepth { + depth: 1, + moves: 20, + }, + PerftDepth { + depth: 2, + moves: 400, + }, + PerftDepth { + depth: 3, + moves: 8902, + }, + PerftDepth { + depth: 4, + moves: 197281, + }, + PerftDepth { + depth: 5, + moves: 4865609, + }, + PerftDepth { + depth: 6, + moves: 119060324, + }, + ]; + + const PERFT_DEPTH: &PerftDepth = &PERFT_DEPTHS[3]; + + #[divan::bench(items_count = PERFT_DEPTH.moves)] + fn chusst() { + use chusst_gen::eval::Game; + use chusst_gen::game::BitboardGame; + + fn possible_moves_recursive(game: BitboardGame, depth: u8) { + if depth == 0 { + return; + } + + for mv in game.get_all_possible_moves() { + if depth == 1 { + continue; + } + let mut game = game.clone(); + game.do_move(&mv); + possible_moves_recursive(game, depth - 1); + } + } + + possible_moves_recursive(BitboardGame::new(), PERFT_DEPTH.depth); + } + + #[divan::bench(items_count = PERFT_DEPTH.moves)] + fn chess() { + use chess::{Board, MoveGen}; + + fn possible_moves_recursive(board: Board, depth: u8) { + if depth == 0 { + return; + } + + for mv in MoveGen::new_legal(&board) { + if depth == 1 { + continue; + } + let board = board.make_move_new(mv); + possible_moves_recursive(board, depth - 1); + } + } + + possible_moves_recursive(Board::default(), PERFT_DEPTH.depth); + } + + #[divan::bench(items_count = PERFT_DEPTH.moves)] + pub fn shackmaty() { + use shakmaty::{Chess, Position}; + + fn possible_moves_recursive(game: Chess, depth: u8) { + if depth == 0 { + return; + } + + for mv in game.legal_moves() { + if depth == 1 { + continue; + } + let mut game = game.clone(); + game.play_unchecked(&mv); + possible_moves_recursive(game, depth - 1); + } + } + + possible_moves_recursive(Chess::default(), PERFT_DEPTH.depth); + } } fn game_benchmark() -> u64 { @@ -75,13 +173,11 @@ fn game_benchmark() -> u64 { // ); } -fn game(bench: &mut Bencher) { - let mut searched = 0u64; - bench.iter(|| { - searched = game_benchmark(); - }); - bench.bytes = searched; +#[divan::bench] +fn game(bench: Bencher) { + bench.bench_local(game_benchmark); } -benchmark_group!(benches, game, search); -benchmark_main!(benches); +fn main() { + divan::main(); +} diff --git a/chusst-gen/src/eval.rs b/chusst-gen/src/eval.rs index 68ad38f..28d0d13 100644 --- a/chusst-gen/src/eval.rs +++ b/chusst-gen/src/eval.rs @@ -265,6 +265,10 @@ trait GamePrivate: PlayableGame + ModifiableGame if can_promote { // I don't think there is an easy way to iterate through all the values of an enum :( possible_moves.push(mva!(position, target)); + possible_moves.push(MoveAction { + mv: mv!(position, target), + move_type: MoveActionType::Promotion(PromotionPieces::Knight), + }); possible_moves.push(MoveAction { mv: mv!(position, target), move_type: MoveActionType::Promotion(PromotionPieces::Bishop), diff --git a/chusst-gen/src/eval/tests.rs b/chusst-gen/src/eval/tests.rs index 98bad3d..299c30b 100644 --- a/chusst-gen/src/eval/tests.rs +++ b/chusst-gen/src/eval/tests.rs @@ -91,6 +91,152 @@ fn custom_game(board_opt: &Option<&str>, player: Player) -> GameState< game } +fn game_from_fen(fen: &str) -> TestGame { + TestGame::try_from_fen( + fen.split_ascii_whitespace() + .collect::>() + .as_slice(), + ) + .unwrap_or_else(|| panic!("Failed to parse FEN string {}", fen)) +} + +impl From for Position { + fn from(pos: shakmaty::Square) -> Self { + pos!(pos.rank() as usize, pos.file() as usize) + } +} + +impl From for MoveAction { + fn from(mv: shakmaty::Move) -> Self { + match mv { + shakmaty::Move::Normal { + from, + to, + promotion, + .. + } => { + let from = from.into(); + let to = to.into(); + match promotion { + Some(shakmaty::Role::Knight) => mva!(from, to, PromotionPieces::Knight), + Some(shakmaty::Role::Bishop) => mva!(from, to, PromotionPieces::Bishop), + Some(shakmaty::Role::Rook) => mva!(from, to, PromotionPieces::Rook), + Some(shakmaty::Role::Queen) => mva!(from, to, PromotionPieces::Queen), + _ => mva!(from, to), + } + } + shakmaty::Move::Castle { king, rook } => { + if rook.file() as usize == 0 { + mva!(king.into(), pos!(king.rank().into(), 2)) + } else { + mva!(king.into(), pos!(king.rank().into(), 6)) + } + } + shakmaty::Move::EnPassant { from, to } => mva!(from.into(), to.into()), + _ => panic!("Shakmaty move not supported"), + } + } +} + +fn perft_compare_against_shakmaty(fen: &str, depth: u8) { + let chusst_game = game_from_fen(fen); + let shakmaty_game = fen + .parse::() + .expect("Failed to parse FEN string") + .into_position::(shakmaty::CastlingMode::Standard) + .expect("Failed to convert FEN to position"); + + fn perft_compare( + chusst_game: TestGame, + shakmaty_game: shakmaty::Chess, + depth: u8, + moves: &[&TestGame], + ) { + use shakmaty::Position; + use std::collections::HashMap; + + if depth == 0 { + return; + } + + let chusst_moves = chusst_game.get_all_possible_moves(); + let shakmaty_moves = shakmaty_game.legal_moves(); + let shakmaty_moves_map: HashMap = HashMap::from_iter( + shakmaty_moves + .iter() + .map(|mv| (mv.clone(), MoveAction::from(mv.clone()))), + ); + + // Compare the list of moves and panic if they don't match, displaying the moves that are different + let chusst_moves_not_in_shakmaty = chusst_moves + .iter() + .filter(|mv| !shakmaty_moves_map.values().collect::>().contains(mv)) + .cloned() + .collect::>(); + let shakmaty_moves_not_in_chusst = shakmaty_moves_map + .values() + .filter(|mv| !chusst_moves.contains(mv)) + .cloned() + .collect::>(); + + if !chusst_moves_not_in_shakmaty.is_empty() || !shakmaty_moves_not_in_chusst.is_empty() { + fn format_mv_list<'a, I>(moves: I) -> String + where + I: Iterator, + { + moves + .map(|mv| format!("{}", mv.mv)) + .collect::>() + .join(", ") + } + + let shakmaty_moves_not_in_chusst_names = shakmaty_moves_not_in_chusst + .iter() + .map(|mv| { + format!( + "{}", + shakmaty_moves_map.iter().find(|(_, v)| *v == mv).unwrap().0 + ) + }) + .collect::>(); + + for game in moves { + println!("After:\n{}", game.board()); + } + + panic!( + "Player {} in board:\n{}\nChusst moves: [{}]\nShakmaty moves: [{}]\nMoves not in Shakmaty: [{}]\nMoves not in Chusst: [{}]\n [{}]", + chusst_game.player(), + chusst_game.board(), + format_mv_list(chusst_moves.iter()), + format_mv_list(shakmaty_moves_map.values()), + format_mv_list(chusst_moves_not_in_shakmaty.iter()), + format_mv_list(shakmaty_moves_not_in_chusst.iter()), + shakmaty_moves_not_in_chusst_names.join(", "), + ); + } + + for chusst_mv in chusst_moves.iter() { + let mut new_chusst_game = chusst_game.clone(); + let mut shakmaty_game = shakmaty_game.clone(); + let shakmaty_mv = shakmaty_moves_map + .iter() + .find(|(_, mv)| *mv == chusst_mv) + .unwrap() + .0; + + new_chusst_game.do_move(chusst_mv); + shakmaty_game.play_unchecked(shakmaty_mv); + + let moves = Vec::from_iter(moves.iter().chain(&[&chusst_game]).copied()); + + perft_compare(new_chusst_game, shakmaty_game, depth - 1, &moves); + } + } + + perft_compare(chusst_game.clone(), shakmaty_game, depth, &[]); +} + #[test] fn move_reversable() { let test_boards = [ @@ -413,6 +559,99 @@ fn fen_parsing() { assert_eq!(game, TestGame::new(), "\n{}", game.board()); } +fn perft_impl(force_comparison: bool) { + fn mv_rec(game: &TestGame, depth: u8) -> u64 { + if depth == 0 { + return 1; + } + + let mut nodes = 0; + for mv in game.get_all_possible_moves() { + if depth == 1 { + nodes += 1; + continue; + } + let mut game_copy = game.clone(); + game_copy.do_move(&mv); + nodes += mv_rec(&game_copy, depth - 1); + } + + nodes + } + + let assert_perft = |fen: &str, name: &str, depth: u8, expected: u64| { + let game = game_from_fen(fen); + + if force_comparison { + perft_compare_against_shakmaty(fen, depth); + return; + } + + let nodes = mv_rec(&game, depth); + if nodes != expected { + println!( + "Perft {} depth {} expected {}, got {}", + name, depth, expected, nodes + ); + println!("Comparing against Shakmaty:"); + + perft_compare_against_shakmaty(fen, depth); + + panic!("Perft failed"); // just in case the comparison didn't panic + } + }; + + let fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; + + assert_perft(fen, "position 1", 1, 20); + assert_perft(fen, "position 1", 2, 400); + assert_perft(fen, "position 1", 3, 8902); + assert_perft(fen, "position 1", 4, 197281); + + // Perft position 3 from https://www.chessprogramming.org/Perft_Results + let fen = "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1"; + + assert_perft(fen, "position 3", 1, 14); + assert_perft(fen, "position 3", 2, 191); + assert_perft(fen, "position 3", 3, 2812); + assert_perft(fen, "position 3", 4, 43238); + + // Perft position 4 from https://www.chessprogramming.org/Perft_Results + let fen = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1"; + + assert_perft(fen, "position 4", 1, 6); + assert_perft(fen, "position 4", 2, 264); + assert_perft(fen, "position 4", 3, 9467); + assert_perft(fen, "position 4", 4, 422333); + + // Perft position 5 from https://www.chessprogramming.org/Perft_Results + let fen = "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8"; + + assert_perft(fen, "position 5", 1, 44); + assert_perft(fen, "position 5", 2, 1486); + assert_perft(fen, "position 5", 3, 62379); + assert_perft(fen, "position 5", 4, 2103487); + + // Perft position 6 from https://www.chessprogramming.org/Perft_Results + let fen = "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10"; + + assert_perft(fen, "position 6", 1, 46); + assert_perft(fen, "position 6", 2, 2079); + assert_perft(fen, "position 6", 3, 89890); + assert_perft(fen, "position 6", 4, 3894594); +} + +#[test] +fn perft() { + perft_impl(false); +} + +#[test] +#[ignore] +fn perft_slow() { + perft_impl(true); +} + // Template to quickly test a specific board/move #[test] #[ignore] diff --git a/chusst-gen/src/game/play.rs b/chusst-gen/src/game/play.rs index 3de0511..df4f0e1 100644 --- a/chusst-gen/src/game/play.rs +++ b/chusst-gen/src/game/play.rs @@ -89,6 +89,8 @@ impl ModifiableGame for GameState { _ => MoveExtraInfo::Other, }; + let captured = self.board.at(&mv.target); + self.move_piece(&mv.source, &mv.target); match move_info { @@ -135,6 +137,16 @@ impl ModifiableGame for GameState { } } + if let Some(captured) = captured { + if captured.piece == PieceType::Rook && mv.target.rank == B::home_rank(&captured.player) { + match mv.target.file { + 0 => self.disable_castle_queenside(captured.player), + 7 => self.disable_castle_kingside(captured.player), + _ => (), + } + } + } + self.data.player = !self.data.player; self.data.last_move = Some(MoveInfo { mv: *mv,