diff --git a/Cargo.toml b/Cargo.toml index ba20b28..8df5834 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "sol_chess" version = "0.1.1" edition = "2021" +default-run = "sol_chess" [dependencies] argh = "0.1.13" diff --git a/README.md b/README.md index 1b17206..726b9de 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,18 @@ Goal: Generate 'hard' puzzles. - Install Rust from [here](https://www.rust-lang.org/tools/install). - Run `cargo install --git https://github.com/cool-mist/sol_chess` to install the tool. -- Run `sol_chess --help` to see the options. ## Usage +- Run `sol_chess` to start a windowed GUI game. +- Run `sol_cli` to start the CLI tool. + +## CLI Usage + - Generate a puzzle ```bash -$ sol_chess -g -n 6 +$ sol_cli -g -n 6 Generating a puzzle with 6 pieces with a maximum of 5 solutions Total attempts: 7 Total pieces placed: 71 @@ -27,12 +31,17 @@ Generating a puzzle with 6 pieces with a maximum of 5 solutions ♔ . ♘ ♙ . . . . + + + id: 202859896274992 ``` -- Solve a puzzle +- Solve a puzzle by ID, or by board string ```bash -$ sol_chess -- --solve N...P.R.K.NP.... +$ sol_cli --solve 202859896274992 +$ sol_cli --solve-board N...P.R.K.NP.... + ♘ . . . ♙ . ♖ . @@ -42,39 +51,15 @@ $ sol_chess -- --solve N...P.R.K.NP.... . . . . + id: 202859896274992 + Found 3 solutions -1. Rc3 -> a3 -2. Ra3 -> a4 -3. Ra4 -> a2 -4. Ra2 -> c2 -5. Rc2 -> d2 -``` +1. RxNc2 +2. RxPd2 +3. RxKa2 +4. RxPa3 +5. RxNa4 -- Generate and solve a puzzle - -```bash -$ sol_chess -g -n 6 --print -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 5 solutions -1. Rb1 -> a1 -2. Ra1 -> d1 -3. Rd1 -> d3 -4. Qa3 -> d3 -5. Qd3 -> c4 ``` ## Heuristics of current algorithm diff --git a/assets/pieces.png b/assets/pieces.png new file mode 100644 index 0000000..fe90ca1 Binary files /dev/null and b/assets/pieces.png differ diff --git a/src/bin/sol_cli.rs b/src/bin/sol_cli.rs new file mode 100644 index 0000000..4742c9e --- /dev/null +++ b/src/bin/sol_cli.rs @@ -0,0 +1,106 @@ +use argh::FromArgs; + +use sol_chess::board::Board; +use sol_chess::generator; +use sol_chess::solver::Solver; + +fn main() { + let args: Args = argh::from_env(); + + if args.generate { + let puzzle = generate_puzzle(args.num_pieces, args.solutions); + let Some(board) = puzzle else { + return; + }; + + board.pretty_print(); + if args.print { + solve_puzzle(board); + } + } else { + let board = if let Some(board_string) = args.solve_board { + Board::from_string(board_string) + } else if let Some(board_id) = args.solve { + Board::from_id(board_id) + } else { + println!("Use --help to see available options"); + return; + }; + let Ok(board) = board else { + println!("Invalid board string/id"); + return; + }; + board.pretty_print(); + solve_puzzle(board); + } +} + +fn solve_puzzle(board: Board) { + let solutions = Solver::new(board).solve(); + if solutions.len() == 0 { + println!("No solutions found"); + return; + } + println!("Found {} solutions", solutions.len()); + let solution = solutions.first().unwrap(); + let mut idx = 0; + solution.iter().for_each(|m| { + idx += 1; + println!("{}. {}", idx, m.notation()); + }); +} + +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 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 = generator::generate(num_pieces, num_solutions); + gen.print_stats(); + + let Some(board) = gen.board() else { + println!("Failed to generate a puzzle, try again"); + return None; + }; + + Some(board) +} + +/// Solitaire Chess puzzle generator and solver +/// - v0.0.1 cool-mist +#[derive(FromArgs)] +struct Args { + #[argh(switch, short = 'g')] + /// generate a puzzle + generate: bool, + + #[argh(option, short = 'n')] + /// 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, + + #[argh(option, short = 's')] + /// the id of the board to solve + solve: Option, + + #[argh(option)] + /// the board to solve in board representation + solve_board: Option, +} diff --git a/src/engine/board.rs b/src/board.rs similarity index 93% rename from src/engine/board.rs rename to src/board.rs index 4803bfb..5778109 100644 --- a/src/engine/board.rs +++ b/src/board.rs @@ -1,22 +1,22 @@ -use std::{ - collections::{HashMap, HashSet}, - fmt::Display, - mem, -}; +pub mod cmove; +mod constants; +pub mod errors; +pub mod piece; +pub mod square; -use super::{ - cmove::CMove, - constants::BOARD_SIZE, - errors::SError, - piece::Piece, - square::{Square, SquarePair}, -}; +use std::{collections::HashSet, mem}; + +use cmove::CMove; +use constants::BOARD_SIZE; +use errors::SError; +use piece::Piece; +use square::{Square, SquarePair}; #[derive(Clone)] -pub(crate) struct Board { - pub(crate) cells: [[Option; BOARD_SIZE]; BOARD_SIZE], - pub(crate) legal_moves: HashSet, - pub(crate) game_state: GameState, +pub struct Board { + pub cells: [[Option; BOARD_SIZE]; BOARD_SIZE], + pub legal_moves: HashSet, + pub game_state: GameState, pieces_remaining: u8, } @@ -29,7 +29,7 @@ pub enum GameState { } impl Board { - pub(crate) fn new() -> Self { + pub fn new() -> Self { Board { cells: [[None; BOARD_SIZE]; BOARD_SIZE], legal_moves: HashSet::new(), @@ -38,7 +38,7 @@ impl Board { } } - pub(crate) fn from_id(board_id: u128) -> Result { + pub fn from_id(board_id: u128) -> Result { let mut board = Board::new(); let mut working = board_id; for i in (0..BOARD_SIZE).rev() { @@ -53,14 +53,12 @@ impl Board { Ok(board) } - pub(crate) fn from_string(board_string: String) -> Result { + pub fn from_string(board_string: String) -> Result { if board_string.chars().count() != 16 { return Err(SError::InvalidBoard); } let mut board = Board::new(); - let mut file = 0; - let mut rank = 0; let mut chars = board_string.chars(); for r in 0..BOARD_SIZE { for f in 0..BOARD_SIZE { @@ -83,7 +81,7 @@ impl Board { Ok(board) } - pub(crate) fn set(&mut self, square: Square) -> Option { + pub fn set(&mut self, square: Square) -> Option { let new_is_occuppied = square.piece.is_some(); let existing = mem::replace(&mut self.cells[square.file][square.rank], square.piece); @@ -101,7 +99,7 @@ impl Board { existing } - pub(crate) fn make_move(&mut self, mv: CMove) -> Option { + pub fn make_move(&mut self, mv: CMove) -> Option { if !self.legal_moves.contains(&mv) { println!("Invalid move - {}", mv.notation()); println!("Legal moves - "); @@ -112,14 +110,14 @@ impl Board { } let from_piece = mem::replace(&mut self.cells[mv.from.file][mv.from.rank], None); - mem::replace(&mut self.cells[mv.to.file][mv.to.rank], from_piece); + self.cells[mv.to.file][mv.to.rank] = from_piece; self.pieces_remaining -= 1; self.board_state_changed(); Some(mv) } - pub(crate) fn empty_squares(&self) -> Vec { + pub fn empty_squares(&self) -> Vec { let mut empty_squares = Vec::new(); for file in 0..BOARD_SIZE { for rank in 0..BOARD_SIZE { @@ -131,12 +129,12 @@ impl Board { empty_squares } - pub(crate) fn pretty_print(&self) { + pub fn pretty_print(&self) { println!("{}", self.print(true)); println!("{:^40}\n", format!("id: {}", self.id())); } - pub(crate) fn id(&self) -> u128 { + pub fn id(&self) -> u128 { let mut res: u128 = 0; for i in 0..BOARD_SIZE { @@ -256,8 +254,6 @@ impl Board { return false; } } - - true } fn calc_game_state(&mut self) { @@ -356,12 +352,20 @@ fn get_square_for_display(piece: &Option, pretty: bool) -> String { #[cfg(test)] mod tests { - use crate::engine::cmove::mv; - use crate::engine::piece::p; - use crate::engine::square::sq; - use super::*; + macro_rules! sq { + ($sq:literal) => { + Square::parse($sq) + }; + } + + macro_rules! mv { + ($from:literal, $to:literal) => {{ + CMove::new(sq!($from), sq!($to)) + }}; + } + macro_rules! validate_board { ($board:expr, $row1:literal, $row2:literal, $row3:literal, $row4:literal) => { let printed = $board.print(false); diff --git a/src/engine/cmove.rs b/src/board/cmove.rs similarity index 56% rename from src/engine/cmove.rs rename to src/board/cmove.rs index f9ac898..df6e721 100644 --- a/src/engine/cmove.rs +++ b/src/board/cmove.rs @@ -1,19 +1,19 @@ -use super::{board::Board, piece::Piece, square::Square}; +use super::{piece::Piece, square::Square}; #[derive(PartialEq, Hash, Eq, Clone)] -pub(crate) struct CMove { - pub(crate) from_piece: Piece, - pub(crate) from: Square, - pub(crate) to_piece: Piece, - pub(crate) to: Square, +pub struct CMove { + pub from_piece: Piece, + pub from: Square, + pub to_piece: Piece, + pub to: Square, // Used to disambiguate when looking at notation disambig: String, } impl CMove { - pub(crate) fn new(from: Square, to: Square) -> Self { - let qualifier = String::from(""); + pub fn new(from: Square, to: Square) -> Self { + let disambig = String::from(""); let from_piece = from.piece.expect("Trying to move a blank"); let to_piece = to.piece.expect("Trying to capture a blank"); CMove { @@ -21,11 +21,11 @@ impl CMove { from, to_piece, to, - disambig: "".to_string(), + disambig } } - pub(crate) fn notation(&self) -> String { + pub fn notation(&self) -> String { let piece_qualifier = match &self.from_piece { Piece::Pawn => self.from.file_notation(), p => p.notation(), @@ -38,11 +38,3 @@ impl CMove { ) } } - -macro_rules! mv { - ($from:literal, $to:literal) => {{ - CMove::new(sq!($from), sq!($to)) - }}; -} - -pub(crate) use mv; diff --git a/src/engine/constants.rs b/src/board/constants.rs similarity index 100% rename from src/engine/constants.rs rename to src/board/constants.rs diff --git a/src/engine/errors.rs b/src/board/errors.rs similarity index 59% rename from src/engine/errors.rs rename to src/board/errors.rs index 757a19c..dd5540d 100644 --- a/src/engine/errors.rs +++ b/src/board/errors.rs @@ -1,4 +1,4 @@ #[derive(Debug)] -pub(crate) enum SError { +pub enum SError { InvalidBoard, } diff --git a/src/engine/piece.rs b/src/board/piece.rs similarity index 83% rename from src/engine/piece.rs rename to src/board/piece.rs index 068a3eb..baa395f 100644 --- a/src/engine/piece.rs +++ b/src/board/piece.rs @@ -1,5 +1,5 @@ #[derive(Clone, Eq, Hash, Copy, Debug, PartialEq)] -pub(crate) enum Piece { +pub enum Piece { King, Queen, Bishop, @@ -9,7 +9,7 @@ pub(crate) enum Piece { } impl Piece { - pub(crate) fn parse(piece: &str) -> Option { + pub fn parse(piece: &str) -> Option { match piece { "K" => Some(Piece::King), "Q" => Some(Piece::Queen), @@ -22,7 +22,7 @@ impl Piece { } } - pub(crate) fn notation(&self) -> String { + pub fn notation(&self) -> String { let n = match self { Piece::King => "K", Piece::Queen => "Q", @@ -35,7 +35,7 @@ impl Piece { n.to_string() } - pub(crate) fn pretty(&self) -> String { + pub fn pretty(&self) -> String { let n = match self { Piece::King => "♔", Piece::Queen => "♕", @@ -49,18 +49,16 @@ impl Piece { } } -macro_rules! p { - ($piece:literal) => { - Piece::parse($piece) - }; -} - -pub(crate) use p; - #[cfg(test)] mod tests { use super::*; + macro_rules! p { + ($piece:literal) => { + Piece::parse($piece) + }; + } + #[test] fn test_piece_parse() { assert_eq!(p!("K"), Some(Piece::King)); diff --git a/src/engine/square.rs b/src/board/square.rs similarity index 79% rename from src/engine/square.rs rename to src/board/square.rs index 2ea884e..0984645 100644 --- a/src/engine/square.rs +++ b/src/board/square.rs @@ -1,34 +1,33 @@ -use crate::engine::constants::BOARD_SIZE; - +use super::constants::BOARD_SIZE; use super::piece::Piece; use core::fmt; #[derive(Clone, Eq, Hash, PartialEq)] -pub(crate) struct Square { +pub struct Square { // a = 0, b = 1, c = 2, d = 3 and so on. - pub(crate) file: usize, + pub file: usize, // 1 = 0, 2 = 1, 3 = 2, 4 = 3 and so on. - pub(crate) rank: usize, + pub rank: usize, - pub(crate) piece: Option, + pub piece: Option, } -pub(crate) struct SquarePair { - pub(crate) start: Square, - pub(crate) end: Square, - pub(crate) dx: usize, - pub(crate) dy: usize, - pub(crate) x_dir: i8, - pub(crate) y_dir: i8, +pub struct SquarePair { + pub start: Square, + pub end: Square, + pub dx: usize, + pub dy: usize, + pub x_dir: i8, + pub y_dir: i8, } impl Square { - pub(crate) fn new(file: usize, rank: usize, piece: Option) -> Self { + pub fn new(file: usize, rank: usize, piece: Option) -> Self { Square { file, rank, piece } } - pub(crate) fn parse(notation: &str) -> Self { + pub fn parse(notation: &str) -> Self { let mut chars = notation.chars(); let piece = chars.next().expect("Piece missing"); let piece = Piece::parse(&piece.to_string()); @@ -49,15 +48,15 @@ impl Square { Square::new(file, rank, piece) } - pub(crate) fn file_notation(&self) -> String { + pub fn file_notation(&self) -> String { String::from("abcd".chars().nth(self.file).unwrap()) } - pub(crate) fn rank_notation(&self) -> String { + pub fn rank_notation(&self) -> String { format!("{}", BOARD_SIZE - self.rank) } - pub(crate) fn notation(&self) -> String { + pub fn notation(&self) -> String { format!( "{}{}{}", self.piece_notation(), @@ -66,7 +65,7 @@ impl Square { ) } - pub(crate) fn is_occupied(&self) -> bool { + pub fn is_occupied(&self) -> bool { self.piece.is_some() } @@ -80,7 +79,7 @@ impl Square { } impl SquarePair { - pub(crate) fn new(start: Square, end: Square) -> Self { + pub fn new(start: Square, end: Square) -> Self { let mut dx = 0; let mut dy = 0; let mut x_dir = 0; @@ -111,19 +110,11 @@ impl SquarePair { } } - pub(crate) fn is_different(&self) -> bool { + pub fn is_different(&self) -> bool { self.dx != 0 || self.dy != 0 } } -macro_rules! sq { - ($sq:literal) => { - Square::parse($sq) - }; -} - -pub(crate) use sq; - impl fmt::Debug for Square { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}({},{})", self.notation(), self.file, self.rank) diff --git a/src/engine/mod.rs b/src/engine/mod.rs deleted file mode 100644 index 8fe34b3..0000000 --- a/src/engine/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub(crate) mod constants; -pub(crate) mod errors; -pub(crate) mod board; -pub(crate) mod square; -pub(crate) mod cmove; -pub(crate) mod piece; diff --git a/src/game.rs b/src/game.rs new file mode 100644 index 0000000..0daf88a --- /dev/null +++ b/src/game.rs @@ -0,0 +1 @@ +pub mod texture; diff --git a/src/game/game.rs b/src/game/game.rs deleted file mode 100644 index e81a273..0000000 --- a/src/game/game.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) fn run() { - println!("Running game..."); -} diff --git a/src/game/mod.rs b/src/game/mod.rs deleted file mode 100644 index 1a824e3..0000000 --- a/src/game/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod game; diff --git a/src/game/texture.rs b/src/game/texture.rs new file mode 100644 index 0000000..90d4a3f --- /dev/null +++ b/src/game/texture.rs @@ -0,0 +1,45 @@ +use macroquad::prelude::*; +use sol_chess::board::piece::Piece; + +pub struct PieceTexture { + x: f32, + y: f32, + w: f32, + h: f32, +} + +impl PieceTexture { + fn new(x: u32, y: u32) -> Self { + Self { + x: x as f32 * 128.0, + y: y as f32 * 128.0, + w: 128.0, + h: 128.0, + } + } + + pub fn for_piece(piece: Piece, sprite_size: f32) -> DrawTextureParams { + let index = match piece { + Piece::Pawn => 0, + Piece::Knight => 1, + Piece::Bishop => 2, + Piece::Rook => 3, + Piece::Queen => 4, + Piece::King => 5, + }; + + let color = 0; + let texture_rect = PieceTexture::new(index, color); + + DrawTextureParams { + source: Some(Rect::new( + texture_rect.x, + texture_rect.y, + texture_rect.w, + texture_rect.h, + )), + dest_size: Some(Vec2::new(sprite_size, sprite_size)), + ..DrawTextureParams::default() + } + } +} diff --git a/src/generator/generator.rs b/src/generator.rs similarity index 88% rename from src/generator/generator.rs rename to src/generator.rs index 83ab99d..cbe850f 100644 --- a/src/generator/generator.rs +++ b/src/generator.rs @@ -1,14 +1,14 @@ -use std::{fmt::Display, thread::Builder, time::Duration}; +use std::{fmt::Display, time::Duration}; use crate::{ - engine::{board::Board, piece::Piece, square::Square}, - solver::{self, solver::Solver}, + board::{piece::Piece, Board}, + solver::Solver, }; use indicatif::ProgressBar; use rand::{seq::*, Rng}; -pub(crate) fn generate(num_pieces: u32, num_solutions: u32) -> GenerateStats { - let mut rand = rand::thread_rng(); +pub fn generate(num_pieces: u32, num_solutions: u32) -> GenerateStats { + let rand = rand::thread_rng(); let candidate_pieces = vec![ Piece::Pawn, Piece::Pawn, @@ -16,8 +16,14 @@ pub(crate) fn generate(num_pieces: u32, num_solutions: u32) -> GenerateStats { Piece::Pawn, Piece::Bishop, Piece::Bishop, + Piece::Bishop, + Piece::Bishop, Piece::Knight, Piece::Knight, + Piece::Knight, + Piece::Queen, + Piece::Rook, + Piece::Rook, ]; if num_pieces > candidate_pieces.len().try_into().unwrap() { @@ -31,7 +37,7 @@ pub(crate) fn generate(num_pieces: u32, num_solutions: u32) -> GenerateStats { 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 { + for _ in 0..attempts { let stats = try_generate( num_pieces, num_solutions, @@ -56,7 +62,7 @@ pub(crate) fn generate(num_pieces: u32, num_solutions: u32) -> GenerateStats { overall_stats } -pub(crate) struct GenerateStats { +pub struct GenerateStats { piece_total: u32, piece_success: u32, total: u32, @@ -81,7 +87,7 @@ impl GenerateStats { } } - pub(crate) fn print_stats(&self) { + pub 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); @@ -91,7 +97,7 @@ impl GenerateStats { println!("{}", stats); } - pub(crate) fn board(mut self) -> Option { + pub fn board(self) -> Option { self.board } } @@ -112,7 +118,7 @@ fn try_generate( let mut board = Board::new(); let mut piece_total = 0; let mut piece_success = 0; - let mut now = std::time::Instant::now(); + let now = std::time::Instant::now(); for _ in 0..num_pieces { let mut placed = false; let empty_squares = board.empty_squares(); @@ -155,7 +161,7 @@ fn try_generate( #[cfg(test)] mod tests { - use crate::{engine::board::GameState, solver::solver::Solver}; + use crate::{board::GameState, solver::Solver}; use super::*; diff --git a/src/generator/mod.rs b/src/generator/mod.rs deleted file mode 100644 index abd9b55..0000000 --- a/src/generator/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod generator; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..be02fcd --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub mod board; +pub mod generator; +pub mod solver; diff --git a/src/main.rs b/src/main.rs index 9d3cb54..7cb8d73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,125 +1,122 @@ -#[allow(unused)] -mod engine; +use game::texture::PieceTexture; +use macroquad::prelude::*; +use sol_chess::board::{square::Square, Board}; -#[allow(unused)] -mod solver; - -#[allow(unused)] -mod generator; - -#[allow(unused)] mod game; -use argh::FromArgs; -use engine::board::Board; +#[macroquad::main("Solitaire Chess")] +async fn main() { + let background_color = Color::from_rgba(196, 195, 208, 255); + let game = init().await; + loop { + clear_background(background_color); + game.draw(); + next_frame().await + } +} -use crate::game::game as sol_chess_game; -use crate::generator::generator::generate; -use crate::solver::solver::Solver; +async fn init() -> Game { + set_pc_assets_folder("./assets"); + let texture_res = load_texture("pieces.png").await.unwrap(); + texture_res.set_filter(FilterMode::Nearest); + build_textures_atlas(); + let mut board = Board::new(); + board.set(Square::parse("Pa4")); + board.set(Square::parse("Pa3")); + board.set(Square::parse("Na2")); + board.set(Square::parse("Na1")); + board.set(Square::parse("Bb4")); + board.set(Square::parse("Bb3")); + board.set(Square::parse("Rb2")); + board.set(Square::parse("Rb1")); + board.set(Square::parse("Kc4")); + board.set(Square::parse("Kc3")); + board.set(Square::parse("Qc2")); + board.set(Square::parse("Qc1")); -fn main() { - let args: Args = argh::from_env(); + let square_width = 128.0; + let num_squares = 4; + let x = (screen_width() - (square_width * num_squares as f32)) / 2.0; + let y = (screen_height() - (square_width * num_squares as f32)) / 2.0; + let game = Game::new(board, x, y, square_width, num_squares, texture_res); - if args.game { - sol_chess_game::run(); - } else if args.generate { - let puzzle = generate_puzzle(args.num_pieces, args.solutions); - let Some(board) = puzzle else { - return; - }; + game +} - board.pretty_print(); - if args.print { - solve_puzzle(board); +struct Game { + board: Board, + squares: Vec, + texture_res: Texture2D, + num_squares: usize, +} + +impl Game { + fn new( + board: Board, + x: f32, + y: f32, + square_width: f32, + num_squares: usize, + texture_res: Texture2D, + ) -> Self { + let dark = Color::from_rgba(83, 104, 120, 255); + let light = Color::from_rgba(190, 190, 190, 255); + let mut rects = Vec::new(); + for i in 0..num_squares { + for j in 0..num_squares { + let x_eff = x + (i as f32 * square_width); + let y_eff = y + (j as f32 * square_width); + let rect = Rect::new(x_eff, y_eff, square_width, square_width); + let color = match (i + j) % 2 { + 1 => dark, + _ => light, + }; + + rects.push(GameSquare { rect, color, i, j }); + } } - } else { - let board = if let Some(board_string) = args.solve_board { - Board::from_string(board_string) - } else if let Some(board_id) = args.solve { - Board::from_id(board_id) - } else { - println!("Use --help to see available options"); - return; - }; - let Ok(board) = board else { - println!("Invalid board string/id"); - return; - }; - board.pretty_print(); - solve_puzzle(board); + + Self { + board, + squares: rects, + num_squares, + texture_res, + } + } + + fn get(&mut self, i: usize, j: usize) -> &mut GameSquare { + &mut self.squares[i * self.num_squares + j] + } + + fn draw(&self) { + let sprite_size = 100.0; + self.squares.iter().for_each(|square| { + draw_rectangle( + square.rect.x, + square.rect.y, + square.rect.w, + square.rect.h, + square.color, + ); + + if let Some(p) = &self.board.cells[square.i][square.j] { + let offset = (square.rect.w - sprite_size) / 2.0; + let dtp = PieceTexture::for_piece(*p, sprite_size); + draw_texture_ex( + &self.texture_res, + square.rect.x + offset, + square.rect.y + offset, + WHITE, + dtp, + ); + } + }); } } -fn solve_puzzle(board: Board) { - let solutions = Solver::new(board).solve(); - if solutions.len() == 0 { - println!("No solutions found"); - return; - } - println!("Found {} solutions", solutions.len()); - let solution = solutions.first().unwrap(); - let mut idx = 0; - solution.iter().for_each(|m| { - idx += 1; - println!("{}. {}", idx, m.notation()); - }); -} - -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 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; - }; - - Some(board) -} - -/// Solitaire Chess puzzle generator and solver -/// - v0.0.1 cool-mist -#[derive(FromArgs)] -struct Args { - #[argh(switch, short = 'g')] - /// generate a puzzle - generate: bool, - - #[argh(switch)] - /// run the game - game: bool, - - #[argh(option, short = 'n')] - /// 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, - - #[argh(option, short = 's')] - /// the id of the board to solve - solve: Option, - - #[argh(option)] - /// the board to solve in board representation - solve_board: Option, +struct GameSquare { + rect: Rect, + color: Color, + i: usize, + j: usize, } diff --git a/src/solver/solver.rs b/src/solver.rs similarity index 85% rename from src/solver/solver.rs rename to src/solver.rs index 3bd38af..978add3 100644 --- a/src/solver/solver.rs +++ b/src/solver.rs @@ -1,15 +1,15 @@ -use crate::engine::{ - board::{Board, GameState}, +use crate::board::{ cmove::CMove, + {Board, GameState}, }; -pub(crate) struct Solver { - pub(crate) board: Board, +pub struct Solver { + pub board: Board, moves: Vec, } impl Solver { - pub(crate) fn new(board: Board) -> Solver { + pub fn new(board: Board) -> Solver { Solver { board, moves: vec![], @@ -24,7 +24,7 @@ impl Solver { Solver { board, moves } } - pub(crate) fn solve(&self) -> Vec> { + pub fn solve(&self) -> Vec> { let mut solutions = Vec::new(); if let GameState::Won = self.board.game_state { solutions.push(self.moves.clone()); @@ -36,7 +36,7 @@ impl Solver { }; self.board.legal_moves.iter().for_each(|m| { - let mut solver = self.clone(m.clone()); + let solver = self.clone(m.clone()); let more_solutions = solver.solve(); solutions.extend(more_solutions); }); @@ -47,12 +47,14 @@ impl Solver { #[cfg(test)] mod tests { - use crate::engine::{ - piece::{p, Piece}, - square::{sq, Square}, - }; - use super::*; + use crate::board::{square::Square, Board}; + + macro_rules! sq { + ($sq:literal) => { + Square::parse($sq) + }; + } #[test] fn solver_smoke() { diff --git a/src/solver/mod.rs b/src/solver/mod.rs deleted file mode 100644 index 7f10d6f..0000000 --- a/src/solver/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod solver; diff --git a/tools/puzzle_checker/games/1.txt b/tools/puzzle_checker/games/1.txt new file mode 100644 index 0000000..785dad5 --- /dev/null +++ b/tools/puzzle_checker/games/1.txt @@ -0,0 +1,26 @@ +*********** Game 1 ************ + +Generating a puzzle with 7 pieces with a maximum of 5 solutions + Total attempts: 328 + Total pieces placed: 3363 + Success pieces placed: 2296 + Total time (ms): 1840 + + ♗ ♗ ♙ ♘ + + . . . ♖ + + . . ♙ . + + . . . ♙ + + + id: 140771860875974 + +Found 1 solutions +1. BxPc2 +2. BxPd1 +3. RxBd1 +4. RxNd4 +5. RxPc4 +6. RxBb4 diff --git a/tools/puzzle_checker/games/10.txt b/tools/puzzle_checker/games/10.txt new file mode 100644 index 0000000..997d2a3 --- /dev/null +++ b/tools/puzzle_checker/games/10.txt @@ -0,0 +1,26 @@ +*********** Game 10 ************ + +Generating a puzzle with 7 pieces with a maximum of 5 solutions + Total attempts: 43 + Total pieces placed: 407 + Success pieces placed: 301 + Total time (ms): 238 + + ♙ . ♖ . + + . . ♘ ♙ + + . ♙ ♘ . + + ♗ . . . + + + id: 211381923512704 + +Found 4 solutions +1. BxPb2 +2. NxPa4 +3. RxNc2 +4. RxBb2 +5. NxRb2 +6. NxPd3 diff --git a/tools/puzzle_checker/games/2.txt b/tools/puzzle_checker/games/2.txt new file mode 100644 index 0000000..2da1541 --- /dev/null +++ b/tools/puzzle_checker/games/2.txt @@ -0,0 +1,26 @@ +*********** Game 2 ************ + +Generating a puzzle with 7 pieces with a maximum of 5 solutions + Total attempts: 47 + Total pieces placed: 473 + Success pieces placed: 329 + Total time (ms): 279 + + . . . ♗ + + ♘ . ♗ . + + ♙ . . . + + . ♘ ♖ ♗ + + + id: 25288852387844 + +Found 4 solutions +1. RxBd1 +2. BxBc3 +3. RxNb1 +4. NxRb1 +5. NxBc3 +6. NxPa2 diff --git a/tools/puzzle_checker/games/3.txt b/tools/puzzle_checker/games/3.txt new file mode 100644 index 0000000..366fa28 --- /dev/null +++ b/tools/puzzle_checker/games/3.txt @@ -0,0 +1,26 @@ +*********** Game 3 ************ + +Generating a puzzle with 7 pieces with a maximum of 5 solutions + Total attempts: 22 + Total pieces placed: 239 + Success pieces placed: 154 + Total time (ms): 160 + + ♗ . ♖ ♙ + + . . . ♙ + + . . . ♙ + + . ♙ . ♗ + + + id: 140737595313588 + +Found 5 solutions +1. RxBa4 +2. RxPd4 +3. RxPd3 +4. RxPd2 +5. RxBd1 +6. RxPb1 diff --git a/tools/puzzle_checker/games/4.txt b/tools/puzzle_checker/games/4.txt new file mode 100644 index 0000000..5a9ea47 --- /dev/null +++ b/tools/puzzle_checker/games/4.txt @@ -0,0 +1,26 @@ +*********** Game 4 ************ + +Generating a puzzle with 7 pieces with a maximum of 5 solutions + Total attempts: 200 + Total pieces placed: 2059 + Success pieces placed: 1388 + Total time (ms): 929 + + ♙ ♘ . ♗ + + . ♖ ♙ ♘ + + . . . . + + . . ♙ . + + + id: 211152405031232 + +Found 1 solutions +1. RxNb4 +2. RxPa4 +3. RxBd4 +4. RxNd3 +5. RxPc3 +6. RxPc1 diff --git a/tools/puzzle_checker/games/5.txt b/tools/puzzle_checker/games/5.txt new file mode 100644 index 0000000..93cb42c --- /dev/null +++ b/tools/puzzle_checker/games/5.txt @@ -0,0 +1,26 @@ +*********** Game 5 ************ + +Generating a puzzle with 7 pieces with a maximum of 5 solutions + Total attempts: 74 + Total pieces placed: 771 + Success pieces placed: 512 + Total time (ms): 437 + + ♗ ♙ . ♘ + + . ♖ . ♘ + + . . . . + + . ♗ ♗ . + + + id: 140792316316480 + +Found 4 solutions +1. BxRb3 +2. BxNd3 +3. NxBb3 +4. NxBc1 +5. NxBd3 +6. NxPb4 diff --git a/tools/puzzle_checker/games/6.txt b/tools/puzzle_checker/games/6.txt new file mode 100644 index 0000000..68c0fd6 --- /dev/null +++ b/tools/puzzle_checker/games/6.txt @@ -0,0 +1,26 @@ +*********** Game 6 ************ + +Generating a puzzle with 7 pieces with a maximum of 5 solutions + Total attempts: 1 + Total pieces placed: 9 + Success pieces placed: 7 + Total time (ms): 0 + + . ♙ . . + + . . . . + + ♗ . ♗ ♗ + + ♖ ♙ . ♘ + + + id: 2456822087717 + +Found 5 solutions +1. RxBa2 +2. RxBc2 +3. RxBd2 +4. RxNd1 +5. RxPb1 +6. RxPb4 diff --git a/tools/puzzle_checker/games/7.txt b/tools/puzzle_checker/games/7.txt new file mode 100644 index 0000000..1b08f5e --- /dev/null +++ b/tools/puzzle_checker/games/7.txt @@ -0,0 +1,26 @@ +*********** Game 7 ************ + +Generating a puzzle with 7 pieces with a maximum of 5 solutions + Total attempts: 22 + Total pieces placed: 230 + Success pieces placed: 154 + Total time (ms): 109 + + ♖ . . ♙ + + . . . ♗ + + ♗ ♙ ♘ . + + . . ♖ . + + + id: 107752945007872 + +Found 2 solutions +1. RxNc2 +2. RxPb2 +3. RxBa2 +4. RxRa4 +5. RxPd4 +6. RxBd3 diff --git a/tools/puzzle_checker/games/8.txt b/tools/puzzle_checker/games/8.txt new file mode 100644 index 0000000..4b6d58d --- /dev/null +++ b/tools/puzzle_checker/games/8.txt @@ -0,0 +1,26 @@ +*********** Game 8 ************ + +Generating a puzzle with 7 pieces with a maximum of 5 solutions + Total attempts: 63 + Total pieces placed: 639 + Success pieces placed: 441 + Total time (ms): 345 + + . . ♖ . + + . ♙ . . + + ♙ . ♗ . + + ♗ ♙ . ♗ + + + id: 3579962327044 + +Found 5 solutions +1. BxBc2 +2. RxBc2 +3. RxPa2 +4. RxBa1 +5. RxPb1 +6. RxPb3 diff --git a/tools/puzzle_checker/games/9.txt b/tools/puzzle_checker/games/9.txt new file mode 100644 index 0000000..7442e63 --- /dev/null +++ b/tools/puzzle_checker/games/9.txt @@ -0,0 +1,26 @@ +*********** Game 9 ************ + +Generating a puzzle with 7 pieces with a maximum of 5 solutions + Total attempts: 250 + Total pieces placed: 2557 + Success pieces placed: 1750 + Total time (ms): 1298 + + . . . . + + ♘ ♘ . ♙ + + . ♙ . ♗ + + ♙ . ♘ . + + + id: 22408723452320 + +Found 1 solutions +1. BxNc1 +2. NxBc1 +3. NxPd3 +4. NxPb2 +5. axNb2 +6. bxNa3 diff --git a/tools/puzzle_checker/gen_games.sh b/tools/puzzle_checker/gen_games.sh new file mode 100755 index 0000000..5274c40 --- /dev/null +++ b/tools/puzzle_checker/gen_games.sh @@ -0,0 +1,9 @@ +if [ ! -d games ]; then + mkdir games +fi + +for i in {1..10}; do + echo "*********** Game $i ************" >> games/$i.txt + echo "" >> games/$i.txt + sol_cli -g -n 7 --print >> games/$i.txt +done