From b6a6d570e1620c0da7b0694563435d8d174677b1 Mon Sep 17 00:00:00 2001 From: cool-mist Date: Sat, 11 Jan 2025 19:15:49 +0530 Subject: [PATCH] Add progress bar, Add stats, pretty print --- Cargo.lock | 218 ++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 +- README.md | 111 ++++++++----------- src/engine/board.rs | 50 ++++++--- src/engine/piece.rs | 11 ++ src/generator/generator.rs | 104 +++++++++++++++--- src/main.rs | 46 ++++---- 7 files changed, 430 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55b87e3..cad299a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byteorder" version = "1.5.0" @@ -46,6 +52,25 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "console" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "getrandom" version = "0.2.15" @@ -57,12 +82,59 @@ dependencies = [ "wasi", ] +[[package]] +name = "indicatif" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "log" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6ea2a48c204030ee31a7d7fc72c93294c92fe87ecb1789881c9543516e1a0d" + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -148,9 +220,10 @@ dependencies = [ [[package]] name = "sol_chess" -version = "0.1.0" +version = "0.1.1" dependencies = [ "argh", + "indicatif", "rand", ] @@ -171,12 +244,155 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index d7a179e..663a4a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,11 @@ [package] name = "sol_chess" -version = "0.1.0" +version = "0.1.1" edition = "2021" [dependencies] argh = "0.1.13" +indicatif = "0.17.9" rand = "0.8.5" [profile.release] diff --git a/README.md b/README.md index 41c6b4d..1b17206 100644 --- a/README.md +++ b/README.md @@ -14,46 +14,67 @@ Goal: Generate 'hard' puzzles. ```bash $ sol_chess -g -n 6 -Generated a puzzle with 6 pieces after 330 ms -PP.. -..PB -.K.. -.N.. +Generating a puzzle with 6 pieces with a maximum of 5 solutions + Total attempts: 7 + Total pieces placed: 71 + Success pieces placed: 42 + Total time (ms): 69 + + ♘ . . . + + ♙ . ♖ . + + ♔ . ♘ ♙ + + . . . . ``` - Solve a puzzle ```bash -$ sol_chess -- --solve PP....PB.K...N.. -PP.. -..PB -.K.. -.N.. +$ sol_chess -- --solve N...P.R.K.NP.... + ♘ . . . -Found 1 solutions -1. Nb1 -> c3 -2. Nc3 -> a4 -3. Na4 -> b2 -4. Nb2 -> d3 -5. Nd3 -> b4 + ♙ . ♖ . + + ♔ . ♘ ♙ + + . . . . + + +Found 3 solutions +1. Rc3 -> a3 +2. Ra3 -> a4 +3. Ra4 -> a2 +4. Ra2 -> c2 +5. Rc2 -> d2 ``` - Generate and solve a puzzle ```bash $ sol_chess -g -n 6 --print -Generated a puzzle with 6 pieces after 933 ms -.P.N -B.R. -.K.. -..N. +Generating a puzzle with 6 pieces with a maximum of 5 solutions + Total attempts: 4 + Total pieces placed: 34 + Success pieces placed: 24 + Total time (ms): 38 -Found 1 solutions -1. Ba3 -> b4 -2. Bb4 -> c3 -3. Bc3 -> d4 -4. Bd4 -> b2 -5. Bb2 -> c1 + . . ♙ . + + ♕ . . ♘ + + . . . . + + ♗ ♖ . ♘ + + +Found 5 solutions +1. Rb1 -> a1 +2. Ra1 -> d1 +3. Rd1 -> d3 +4. Qa3 -> d3 +5. Qd3 -> c4 ``` ## Heuristics of current algorithm @@ -62,40 +83,4 @@ Found 1 solutions 2. Select pieces to place based on its weight. 3. Eg: Queen is too powerful, so it has lower weightage. 4. Eg: Knights are confusing. More knights. -3. Discard puzzles with more than one solution. -## Example puzzles generated - -1. - -``` -N... -P.B. -.R.. -..KP -``` - -2. - -``` -R... -..P. -..B. -.KNN -``` -3. - -``` -.PN. -P... -K..P -.R.. -``` - -4. - -``` -..PK -...R -P..P -B..N diff --git a/src/engine/board.rs b/src/engine/board.rs index a7de1bf..dd4607a 100644 --- a/src/engine/board.rs +++ b/src/engine/board.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::{collections::HashSet, fmt::Display}; use super::{ coord::{at, Coord}, @@ -100,24 +100,26 @@ impl Board { empty_squares } - pub(crate) fn print(&self) -> String { - let mut builder: Vec = Vec::new(); + pub(crate) fn pretty_print(&self) { + println!("{}", self.print(true)); + } + + fn print(&self, pretty: bool) -> String { + let mut board_string = String::new(); for rank in 0..4 { + let mut row = String::new(); 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'), - } + print_square(&mut row, &self.cells[file][rank], pretty); } - builder.push('\n'); + if pretty { + board_string.push_str(&format!("{:^40}\n", row)); + } else { + board_string.push_str(&row); + } + board_string.push('\n'); } - builder.iter().collect::() + board_string } fn calc_legal_moves(&mut self) { @@ -385,6 +387,24 @@ impl Board { } } +fn print_square(row: &mut String, square: &Square, pretty: bool) { + let contents = if let Square::Occupied(piece, _) = square { + if pretty { + piece.pretty() + } else { + piece.notation() + } + } else { + "." + }; + + if pretty { + row.push_str(&format!(" {} ", contents)); + } else { + row.push_str(contents); + } +} + #[cfg(test)] mod tests { use crate::engine::piece::p; @@ -395,7 +415,7 @@ mod tests { macro_rules! validate_board { ($board:expr, $row1:literal, $row2:literal, $row3:literal, $row4:literal) => { - let printed = $board.print(); + let printed = $board.print(false); assert_eq!( printed, format!("{}\n{}\n{}\n{}\n", $row1, $row2, $row3, $row4) diff --git a/src/engine/piece.rs b/src/engine/piece.rs index 7efde54..b465c10 100644 --- a/src/engine/piece.rs +++ b/src/engine/piece.rs @@ -31,6 +31,17 @@ impl Piece { Piece::Pawn => "P", } } + + pub(crate) fn pretty(&self) -> &str { + match self { + Piece::King => "♔", + Piece::Queen => "♕", + Piece::Bishop => "♗", + Piece::Knight => "♘", + Piece::Rook => "♖", + Piece::Pawn => "♙", + } + } } macro_rules! p { diff --git a/src/generator/generator.rs b/src/generator/generator.rs index 38fb939..c89f890 100644 --- a/src/generator/generator.rs +++ b/src/generator/generator.rs @@ -1,10 +1,13 @@ +use std::{fmt::Display, thread::Builder, time::Duration}; + use crate::{ engine::{board::Board, coord::Coord, piece::Piece, square::Square}, solver::{self, solver::Solver}, }; +use indicatif::ProgressBar; use rand::{seq::*, Rng}; -pub(crate) fn generate(num_pieces: u32) -> Option { +pub(crate) fn generate(num_pieces: u32, num_solutions: u32) -> GenerateStats { let mut rand = rand::thread_rng(); let candidate_pieces = vec![ Piece::Pawn, @@ -17,33 +20,104 @@ pub(crate) fn generate(num_pieces: u32) -> Option { Piece::King, Piece::Queen, ]; - let attempts = 1000; + let attempts: u32 = 1000; + let bar = ProgressBar::new_spinner(); + bar.enable_steady_tick(Duration::from_millis(100)); + let mut overall_stats = GenerateStats::new(0, 0, 0, 0, None); for i in 0..attempts { - let board = try_generate(num_pieces, candidate_pieces.clone(), rand.clone()); - if let Some(board) = board { - return Some(board); + let stats = try_generate( + num_pieces, + num_solutions, + candidate_pieces.clone(), + rand.clone(), + ); + overall_stats.piece_total += stats.piece_total; + overall_stats.piece_success += stats.piece_success; + overall_stats.total += stats.total; + overall_stats.total_millis += stats.total_millis; + overall_stats.board = stats.board; + bar.set_message(format!( + "Generating puzzle.. Elapsed: {}s", + overall_stats.total_millis / 1000 + )); + if overall_stats.board.is_some() { + return overall_stats; } } - None + bar.finish_and_clear(); + overall_stats +} + +pub(crate) struct GenerateStats { + piece_total: u32, + piece_success: u32, + total: u32, + total_millis: u128, + board: Option, +} + +impl GenerateStats { + fn new( + piece_total: u32, + piece_success: u32, + total: u32, + total_millis: u128, + board: Option, + ) -> Self { + Self { + piece_total, + piece_success, + total, + total_millis, + board, + } + } + + pub(crate) fn print_stats(&self) { + let mut stats = String::new(); + add_stat(&mut stats, "Total attempts", self.total); + add_stat(&mut stats, "Total pieces placed", self.piece_total); + add_stat(&mut stats, "Success pieces placed", self.piece_success); + add_stat(&mut stats, "Total time (ms)", self.total_millis); + + println!("{}", stats); + } + + pub(crate) fn board(mut self) -> Option { + self.board + } +} + +fn add_stat(stats: &mut String, name: &str, val: T) +where + T: Display, +{ + stats.push_str(&format!("{:>30}:{:>6}\n", name, val)); } fn try_generate( num_pieces: u32, + num_solutions: u32, mut candidate_pieces: Vec, mut rand: rand::prelude::ThreadRng, -) -> Option { +) -> GenerateStats { let mut board = Board::new(); + let mut piece_total = 0; + let mut piece_success = 0; + let mut now = std::time::Instant::now(); for _ in 0..num_pieces { let mut placed = false; let empty_squares = board.empty_squares(); let mut attempts = 15; while !placed { if attempts == 0 { - return None; + let elapsed = now.elapsed().as_millis(); + return GenerateStats::new(piece_total, piece_success, 1, elapsed, None); } attempts -= 1; + piece_total += 1; let index = rand.gen_range(0..candidate_pieces.len()); let piece = candidate_pieces[index]; @@ -53,6 +127,7 @@ fn try_generate( let solutions = Solver::new(board.clone()).solve(); if solutions.len() > 0 { placed = true; + piece_success += 1; candidate_pieces.remove(index); continue; } @@ -61,10 +136,11 @@ fn try_generate( } let solutions = Solver::new(board.clone()).solve(); - if solutions.len() > 1 { - None + let elapsed = now.elapsed().as_millis(); + if solutions.len() > num_solutions as usize { + GenerateStats::new(piece_total, piece_success, 1, elapsed, None) } else { - Some(board) + GenerateStats::new(piece_total, piece_success, 1, elapsed, Some(board)) } } @@ -77,11 +153,13 @@ mod tests { #[test] fn generator_smoke() { for _ in 0..10 { - let board = generate(5).unwrap(); + let gen_stats = generate(5, 5); + let board = gen_stats.board.unwrap(); assert_eq!(board.game_state, GameState::InProgress); let solutions = Solver::new(board).solve(); - assert_eq!(solutions.len(), 1); + assert!(solutions.len() <= 5); + assert!(solutions.len() >= 1); } } } diff --git a/src/main.rs b/src/main.rs index 77cf715..385a182 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,13 +16,12 @@ use crate::solver::solver::Solver; fn main() { let args: Args = argh::from_env(); if args.generate { - let puzzle = generate_puzzle(args.num_pieces); + let puzzle = generate_puzzle(args.num_pieces, args.solutions); let Some(board) = puzzle else { return; }; - println!("{}", board.print()); - + board.pretty_print(); if args.print { solve_puzzle(board); } @@ -32,7 +31,7 @@ fn main() { println!("Invalid board string"); return; }; - println!("{}", board.print()); + board.pretty_print(); solve_puzzle(board); } else { println!("Use --help to see available options"); @@ -54,26 +53,29 @@ fn solve_puzzle(board: Board) { }); } -fn generate_puzzle(num_pieces: Option) -> Option { - let start = std::time::Instant::now(); - let num_pieces = num_pieces.unwrap_or(5); - let board = generate(num_pieces); - let elapsed = start.elapsed(); +fn generate_puzzle(num_pieces: Option, num_solutions: Option) -> Option { + let mut num_pieces = num_pieces.unwrap_or(5); + if num_pieces < 2 { + num_pieces = 2; + } - let Some(board) = board else { - println!( - "Failed to generate a puzzle with {} pieces after {} ms, Try again", - num_pieces, - elapsed.as_millis() - ); + let mut num_solutions = num_solutions.unwrap_or(5); + if num_solutions < 1 { + num_solutions = 5; + } + + println!( + "Generating a puzzle with {} pieces with a maximum of {} solutions", + num_pieces, num_solutions + ); + let gen = generate(num_pieces, num_solutions); + gen.print_stats(); + + let Some(board) = gen.board() else { + println!("Failed to generate a puzzle, try again"); return None; }; - println!( - "Generated a puzzle with {} pieces after {} ms", - num_pieces, - elapsed.as_millis() - ); Some(board) } @@ -89,6 +91,10 @@ struct Args { /// number of pieces to place on the board while generating a puzzle num_pieces: Option, + #[argh(option)] + /// maximum number of solutions allowed for the generated puzzle. atleast 1. defaults to 5 + solutions: Option, + #[argh(switch)] /// print the solution. When solving a puzzle, this is always set to true print: bool,