diff --git a/Cargo.lock b/Cargo.lock index ed9b31a..4e7ae11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,139 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "sol_chess" version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "syn" +version = "2.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index c3f9ac4..815756c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] +rand = "0.8.5" diff --git a/README.md b/README.md new file mode 100644 index 0000000..439b984 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Solitaire Chess Puzzle Generator + +Goal: Generate 'hard' puzzles. + +## Heuristics of current algorithm + +1. About 6-7 pieces on the board. +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. + +## 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 235b064..97735b2 100644 --- a/src/engine/board.rs +++ b/src/engine/board.rs @@ -88,6 +88,18 @@ impl Board { } } + pub(crate) fn empty_squares(&self) -> Vec { + let mut empty_squares = Vec::new(); + for file in 0..4 { + for rank in 0..4 { + if self.cells[file][rank].is_empty() { + empty_squares.push(Coord::new(file, rank)); + } + } + } + empty_squares + } + pub(crate) fn print(&self) -> String { let mut builder: Vec = Vec::new(); for rank in 0..4 { diff --git a/src/generator/generator.rs b/src/generator/generator.rs new file mode 100644 index 0000000..62bbabe --- /dev/null +++ b/src/generator/generator.rs @@ -0,0 +1,86 @@ +use crate::{ + engine::{board::Board, coord::Coord, piece::Piece, square::Square}, + solver::{self, solver::Solver}, +}; +use rand::{seq::*, Rng}; + +pub(crate) fn generate() -> Option { + let mut rand = rand::thread_rng(); + let candidate_pieces = vec![ + Piece::Pawn, + Piece::Pawn, + Piece::Pawn, + Piece::Rook, + Piece::Bishop, + Piece::Knight, + Piece::Knight, + Piece::King, + Piece::Queen, + ]; + let num_pieces = 7; + let attempts = 1000; + for i in 0..attempts { + let board = try_generate(num_pieces, candidate_pieces.clone(), rand.clone()); + if let Some(board) = board { + return Some(board); + } + } + + None +} + +fn try_generate( + num_pieces: u32, + mut candidate_pieces: Vec, + mut rand: rand::prelude::ThreadRng, +) -> Option { + let mut board = Board::new(); + 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; + } + + attempts -= 1; + + let index = rand.gen_range(0..candidate_pieces.len()); + let piece = candidate_pieces[index]; + let coord = empty_squares.choose(&mut rand).unwrap().clone(); + + board.set(Square::Occupied(piece.clone(), coord.clone())); + let solutions = Solver::new(board.clone()).solve(); + if solutions.len() > 0 { + placed = true; + candidate_pieces.remove(index); + continue; + } + board.set(Square::Empty(coord)); + } + } + + let solutions = Solver::new(board.clone()).solve(); + if solutions.len() > 1 { + None + } else { + Some(board) + } +} + +#[cfg(test)] +mod tests { + use crate::{engine::board::GameState, solver::solver::Solver}; + + use super::*; + + #[test] + fn generator_smoke() { + let board = generate().unwrap(); + assert_eq!(board.game_state, GameState::InProgress); + + let solutions = Solver::new(board).solve(); + assert_ne!(solutions.len(), 0); + } +} diff --git a/src/generator/mod.rs b/src/generator/mod.rs new file mode 100644 index 0000000..abd9b55 --- /dev/null +++ b/src/generator/mod.rs @@ -0,0 +1 @@ +pub(crate) mod generator; diff --git a/src/main.rs b/src/main.rs index cc451e9..4655118 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,4 +4,34 @@ mod engine; #[allow(unused)] mod solver; -fn main() {} +#[allow(unused)] +mod generator; + +use crate::generator::generator::generate; +use crate::solver::solver::Solver; + +fn main() { + let start = std::time::Instant::now(); + let board = generate(); + let elapsed = start.elapsed(); + + println!("Generated a problem in {} ms", elapsed.as_millis()); + + let Some(board) = board else { + println!( + "Failed to generate a board after {} ms, Try again", + elapsed.as_millis() + ); + return; + }; + + println!("{}", board.print()); + let solutions = Solver::new(board).solve(); + 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()); + }); +}