remove recursion in solver

This commit is contained in:
cool-mist
2026-01-21 20:14:57 +05:30
parent 27d57299ba
commit 8cd1f86ba6
9 changed files with 267 additions and 223 deletions
+3 -3
View File
@@ -15,15 +15,15 @@ mod texture;
use button::Button;
use macroquad::prelude::*;
use sol_chess::board::{Board, BoardState};
use sol_chess::{board::{Board, BoardState}, generator::Puzzle};
use sound::Sounds;
pub struct Game {
// The generated puzzle. We keep a copy of this to reset the game.
original_board: Board,
puzzle: Puzzle,
// What is shown to the user
board: Board,
current_board: Board,
// Constants througout the game
texture_res: Texture2D,
+3 -3
View File
@@ -249,7 +249,7 @@ impl Game {
color,
);
if let Some(p) = &self.board.cells[square.i][square.j] {
if let Some(p) = &self.current_board.cells[square.i][square.j] {
let offset = (square.rect.w - sprite_size) / 2.0;
let dtp = PieceTexture::for_piece(*p, sprite_size);
if !square.is_source {
@@ -267,7 +267,7 @@ impl Game {
});
if let Some(selected_square) = selected_square {
if let Some(p) = self.board.cells[selected_square.i][selected_square.j] {
if let Some(p) = self.current_board.cells[selected_square.i][selected_square.j] {
let dtp = PieceTexture::for_piece(p, sprite_size);
draw_texture_ex(
&self.texture_res,
@@ -306,7 +306,7 @@ impl Game {
return false;
});
debug_lines.push(format!("Game State: {}", self.state));
debug_lines.push(format!("Board State: {}", self.board.game_state));
debug_lines.push(format!("Board State: {}", self.current_board.game_state));
if let Some(hover_square) = hover_square {
debug_lines.push(format!("Hover: [ {}, {} ]", hover_square.i, hover_square.j));
}
+22 -20
View File
@@ -1,14 +1,13 @@
use std::{collections::HashMap, rc::Rc};
use super::{
constants, sound::Sounds, Board, BoardState, ButtonAction, Game, GameMode, GameSquare,
GameState,
constants, sound::Sounds, BoardState, ButtonAction, Game, GameMode, GameSquare, GameState,
};
use macroquad::prelude::*;
use sol_chess::{
board,
generator::{self, RandomRange},
generator::{self, Puzzle, RandomRange},
};
impl Game {
@@ -139,7 +138,7 @@ impl Game {
let mut selected = None;
for square in &mut self.squares {
if mouse.overlaps_rect(&square.rect) {
if let Some(_) = self.board.cells[square.i][square.j] {
if let Some(_) = self.current_board.cells[square.i][square.j] {
selected = Some((square.i, square.j));
}
}
@@ -148,7 +147,7 @@ impl Game {
if let Some((i, j)) = selected {
self.get(i, j).is_source = true;
let mut target_squares = vec![];
for m in self.board.legal_moves.iter() {
for m in self.current_board.legal_moves.iter() {
if m.from.file == i && m.from.rank == j {
target_squares.push((m.to.file, m.to.rank));
}
@@ -179,7 +178,7 @@ impl Game {
let mut selected = None;
for square in &mut self.squares {
if mouse.overlaps_rect(&square.rect) {
if let Some(_) = self.board.cells[square.i][square.j] {
if let Some(_) = self.current_board.cells[square.i][square.j] {
selected = Some((square.i, square.j));
}
}
@@ -202,17 +201,18 @@ impl Game {
}
if is_legal {
let m = self.board.legal_moves.iter().find(|m| {
let m = self.current_board.legal_moves.iter().find(|m| {
m.from.file == s_x && m.from.rank == s_y && m.to.file == x && m.to.rank == y
});
let m = m.expect("legal move should be found");
self.board.make_move(m.clone());
self.current_board.make_move(m.clone());
if self.board.game_state == BoardState::Won || self.board.game_state == BoardState::Lost
if self.current_board.game_state == BoardState::Won
|| self.current_board.game_state == BoardState::Lost
{
self.reset_squares();
if self.board.game_state == BoardState::Won {
if self.current_board.game_state == BoardState::Won {
let next_btn = self
.gp_btns
.get_mut(&ButtonAction::Next)
@@ -237,7 +237,7 @@ impl Game {
}
fn reset(&mut self) {
self.board = self.original_board.clone();
self.current_board = self.puzzle.board.clone();
self.reset_squares();
let next_button = self
@@ -251,9 +251,9 @@ impl Game {
fn next_puzzle(&mut self) {
self.reset();
let board = Game::generate_puzzle(self.game_mode);
self.original_board = board.clone();
self.board = board;
let puzzle = Game::generate_puzzle(self.game_mode);
self.current_board = puzzle.board.clone();
self.puzzle = puzzle;
}
fn reset_squares(&mut self) {
@@ -265,25 +265,27 @@ impl Game {
}
}
fn generate_puzzle(mode: GameMode) -> Board {
fn generate_puzzle(mode: GameMode) -> Puzzle {
let piece_count = match mode {
GameMode::Easy => 3,
GameMode::Medium => 5,
GameMode::Hard => 7,
};
let generate = generator::generate(piece_count, 100, &MacroquadRandAdapter);
generate.board().expect("No puzzle was generated")
let generated = generator::generate(piece_count, 100, &MacroquadRandAdapter);
let puzzle = generated.puzzle();
puzzle.expect("No puzzle was generated")
}
pub fn new_game(texture_res: Texture2D, sounds: Sounds, font: Font) -> Self {
let num_squares: usize = board::constants::BOARD_SIZE;
let game_mode = GameMode::Medium;
let board = Game::generate_puzzle(game_mode);
let puzzle = Game::generate_puzzle(game_mode);
let current_board = puzzle.board.clone();
Self {
original_board: board.clone(),
board,
puzzle,
current_board,
board_rect: Rect::new(0., 0., 0., 0.),
squares: Vec::new(),
heading_rect: Rect::new(0., 0., 0., 0.),
+33 -27
View File
@@ -1,8 +1,8 @@
use argh::FromArgs;
use sol_chess::board::cmove::CMove;
use sol_chess::board::Board;
use sol_chess::generator::{self, RandomRange};
use sol_chess::solver::Solver;
use sol_chess::generator::{self, Puzzle, RandomRange};
// Learn how to specify a different dependency for this binary
struct MacroquadRngTodo;
@@ -14,37 +14,40 @@ impl RandomRange for MacroquadRngTodo {
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 {
let Some(puzzle) = puzzle else {
println!("Failed to generate a puzzle, try adjusting the generation parameters");
return;
};
board.pretty_print();
if args.print {
solve_puzzle(board);
print_solutions(puzzle);
}
} 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);
return;
}
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;
};
let puzzle = board.solve();
print_solutions(puzzle);
}
fn solve_puzzle(board: Board) {
let solutions = Solver::new(board).solve();
fn print_solutions(puzzle: Puzzle) {
puzzle.board.pretty_print();
let solutions = puzzle.solutions;
if solutions.len() == 0 {
println!("No solutions found");
return;
@@ -55,10 +58,13 @@ fn solve_puzzle(board: Board) {
idx += 1;
println!("{}. {}", idx, m.notation());
});
println!("There are atleast {} solutions to this puzzle", solutions.len());
println!(
"There are atleast {} solutions to this puzzle",
solutions.len()
);
}
fn generate_puzzle(num_pieces: Option<u32>, num_solutions: Option<u32>) -> Option<Board> {
fn generate_puzzle(num_pieces: Option<u32>, num_solutions: Option<u32>) -> Option<Puzzle> {
let mut num_pieces = num_pieces.unwrap_or(5);
if num_pieces < 2 {
num_pieces = 2;
@@ -76,12 +82,12 @@ fn generate_puzzle(num_pieces: Option<u32>, num_solutions: Option<u32>) -> Optio
let gen = generator::generate(num_pieces, num_solutions, &MacroquadRngTodo);
gen.print_stats();
let Some(board) = gen.board() else {
let Some(puzzle) = gen.puzzle() else {
println!("Failed to generate a puzzle, try again");
return None;
};
Some(board)
Some(puzzle)
}
/// Solitaire Chess puzzle generator and solver
+104 -3
View File
@@ -17,7 +17,7 @@ use errors::SError;
use piece::Piece;
use square::{Square, SquarePair};
use crate::util;
use crate::generator::Puzzle;
#[derive(Clone)]
pub struct Board {
@@ -54,7 +54,7 @@ impl Board {
let mut board_id_bytes = [0; 8];
board_id_bytes.copy_from_slice(board_id.as_bytes());
let mut working_bytes_slice = [0; 6];
util::b64_decode_48(&board_id_bytes, &mut working_bytes_slice);
b64_decode_exact_48(&board_id_bytes, &mut working_bytes_slice);
let mut working_bytes = [0; 8];
working_bytes[2..].copy_from_slice(&working_bytes_slice);
@@ -155,6 +155,68 @@ impl Board {
println!("{:^40}\n", format!("id: {}", self.id));
}
pub fn solve(&self) -> Puzzle {
struct StackItem {
board: Board,
moves_so_far: Vec<CMove>,
next_move: CMove,
}
if let BoardState::Won = self.game_state {
return Puzzle {
board: self.clone(),
solutions: vec![vec![]],
solved: true,
};
}
let mut stack = Vec::new();
for mv in &self.legal_moves {
let item = StackItem {
board: self.clone(),
moves_so_far: vec![],
next_move: mv.clone(),
};
stack.push(item);
}
let mut solutions = Vec::new();
loop {
let top = stack.pop();
let Some(top) = top else {
let solved = solutions.len() > 0;
return Puzzle {
board: self.clone(),
solutions,
solved,
};
};
let (mut board, mut moves_so_far, next) = (top.board, top.moves_so_far, top.next_move);
board.make_move(next.clone());
match board.game_state {
BoardState::Won => {
moves_so_far.push(next);
solutions.push(moves_so_far);
}
BoardState::InProgress => {
moves_so_far.push(next);
for mv in &board.legal_moves {
let item = StackItem {
board: board.clone(),
moves_so_far: moves_so_far.clone(),
next_move: mv.clone(),
};
stack.push(item);
}
}
_ => {}
}
}
}
fn encode(cells: [[Option<Piece>; BOARD_SIZE]; BOARD_SIZE]) -> String {
let mut res: u64 = 0;
@@ -168,7 +230,7 @@ impl Board {
let mut id_bytes = [0; 6];
id_bytes.copy_from_slice(&res.to_be_bytes()[2..]);
util::b64_encode_48(&id_bytes)
b64_encode_exact_48(&id_bytes)
}
fn print(&self, pretty: bool) -> String {
@@ -391,6 +453,45 @@ impl Display for BoardState {
}
}
fn b64_encode_exact_48(input: &[u8; 6]) -> String {
let mut output = [0 as char; 8];
for (byte_chunk, output_slice) in input.chunks_exact(3).zip(output.chunks_exact_mut(4)) {
let byte1 = byte_chunk[0];
let byte2 = byte_chunk[1];
let byte3 = byte_chunk[2];
output_slice[0] = lookup((byte1 & 0b1111_1100) >> 2);
output_slice[1] = lookup((byte1 & 0b0000_0011) << 4 | (byte2 & 0b1111_0000) >> 4);
output_slice[2] = lookup((byte2 & 0b0000_1111) << 2 | (byte3 & 0b1100_0000) >> 6);
output_slice[3] = lookup(byte3 & 0b0011_1111);
}
output.iter().collect()
}
fn b64_decode_exact_48(input: &[u8; 8], output: &mut [u8; 6]) {
for (char_chunk, output_slice) in input.chunks_exact(4).zip(output.chunks_exact_mut(3)) {
let char_1 = reverse_lookup(char_chunk[0] as char);
let char_2 = reverse_lookup(char_chunk[1] as char);
let char_3 = reverse_lookup(char_chunk[2] as char);
let char_4 = reverse_lookup(char_chunk[3] as char);
output_slice[0] = (char_1 << 2) | (char_2 >> 4);
output_slice[1] = (char_2 << 4) | (char_3 >> 2);
output_slice[2] = (char_3 << 6) | char_4;
}
}
const ALPHABET: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
fn lookup(idx: u8) -> char {
ALPHABET.chars().nth(idx as usize).unwrap()
}
fn reverse_lookup(c: char) -> u8 {
ALPHABET.chars().position(|x| x == c).unwrap() as u8
}
#[cfg(test)]
mod tests {
use super::*;
+40 -19
View File
@@ -1,9 +1,6 @@
use std::fmt::Display;
use crate::{
board::{piece::Piece, Board},
solver::Solver,
};
use crate::board::{cmove::CMove, piece::Piece, Board};
pub trait RandomRange {
fn gen_range(&self, min: usize, max: usize) -> usize;
@@ -35,7 +32,7 @@ pub fn generate(num_pieces: u32, num_solutions: u32, rand: &impl RandomRange) ->
}
let attempts: u32 = 1000;
let mut overall_stats = GenerateStats::new(0, 0, 0, None);
let mut overall_stats = GenerateStats::new(0, 0, 0, None, vec![]);
for _ in 0..attempts {
let stats = try_generate(num_pieces, num_solutions, rand, candidate_pieces.clone());
overall_stats.piece_total += stats.piece_total;
@@ -50,20 +47,34 @@ pub fn generate(num_pieces: u32, num_solutions: u32, rand: &impl RandomRange) ->
overall_stats
}
pub struct Puzzle {
pub board: Board,
pub solutions: Vec<Vec<CMove>>,
pub solved: bool,
}
pub struct GenerateStats {
piece_total: u32,
piece_success: u32,
total: u32,
board: Option<Board>,
solutions: Vec<Vec<CMove>>,
}
impl GenerateStats {
fn new(piece_total: u32, piece_success: u32, total: u32, board: Option<Board>) -> Self {
fn new(
piece_total: u32,
piece_success: u32,
total: u32,
board: Option<Board>,
solutions: Vec<Vec<CMove>>,
) -> Self {
Self {
piece_total,
piece_success,
total,
board,
solutions,
}
}
@@ -76,8 +87,18 @@ impl GenerateStats {
println!("{}", stats);
}
pub fn board(self) -> Option<Board> {
self.board
pub fn puzzle(self) -> Option<Puzzle> {
let Some(board) = self.board else {
return None;
};
let solved = self.solutions.len() > 0;
Some(Puzzle {
board,
solutions: self.solutions,
solved,
})
}
}
@@ -103,7 +124,7 @@ fn try_generate(
let mut attempts = 15;
while !placed {
if attempts == 0 {
return GenerateStats::new(piece_total, piece_success, 1, None);
return GenerateStats::new(piece_total, piece_success, 1, None, vec![]);
}
attempts -= 1;
@@ -115,8 +136,8 @@ fn try_generate(
let mut random_square = empty_squares[square_index].clone();
random_square.piece = Some(piece);
board.set(random_square.clone());
let solutions = Solver::new(board.clone()).solve();
if solutions.len() > 0 {
let puzzle = board.solve();
if puzzle.solutions.len() > 0 {
placed = true;
piece_success += 1;
candidate_pieces.remove(index);
@@ -128,17 +149,17 @@ fn try_generate(
}
}
let solutions = Solver::new(board.clone()).solve();
if solutions.len() > num_solutions as usize {
GenerateStats::new(piece_total, piece_success, 1, None)
let puzzle = board.solve();
if puzzle.solutions.len() > num_solutions as usize {
GenerateStats::new(piece_total, piece_success, 1, None, vec![])
} else {
GenerateStats::new(piece_total, piece_success, 1, Some(board))
GenerateStats::new(piece_total, piece_success, 1, Some(puzzle.board), puzzle.solutions)
}
}
#[cfg(test)]
mod tests {
use crate::{board::BoardState, solver::Solver};
use crate::board::BoardState;
use super::*;
@@ -158,9 +179,9 @@ mod tests {
let board = gen_stats.board.expect("No puzzle was generated");
assert_eq!(board.game_state, BoardState::InProgress);
let solutions = Solver::new(board).solve();
assert!(solutions.len() <= 5);
assert!(solutions.len() >= 1);
let puzzle = board.solve();
assert!(puzzle.solutions.len() <= 5);
assert!(puzzle.solutions.len() >= 1);
}
}
}
-1
View File
@@ -1,4 +1,3 @@
pub mod board;
pub mod generator;
pub mod solver;
mod util;
+62 -109
View File
@@ -1,109 +1,62 @@
use crate::board::{
cmove::CMove,
{Board, BoardState},
};
pub struct Solver {
pub board: Board,
moves: Vec<CMove>,
}
impl Solver {
pub fn new(board: Board) -> Solver {
Solver {
board,
moves: vec![],
}
}
fn clone(&self, m: CMove) -> Self {
let mut moves = self.moves.clone();
let mut board = self.board.clone();
moves.push(m.clone());
board.make_move(m);
Solver { board, moves }
}
pub fn solve(&self) -> Vec<Vec<CMove>> {
let mut solutions = Vec::new();
if let BoardState::Won = self.board.game_state {
solutions.push(self.moves.clone());
return solutions;
}
let BoardState::InProgress = self.board.game_state else {
return solutions;
};
self.board.legal_moves.iter().for_each(|m| {
let solver = self.clone(m.clone());
let more_solutions = solver.solve();
solutions.extend(more_solutions);
});
solutions
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::board::{square::Square, Board};
macro_rules! sq {
($sq:literal) => {
Square::parse($sq)
};
}
#[test]
fn solver_smoke() {
let mut board = Board::new();
// . R . .
// R . . P
// B . B N
// P . N .
board.set(sq!("Pa1"));
board.set(sq!("Ba2"));
board.set(sq!("Ra3"));
board.set(sq!("Rb4"));
board.set(sq!("Nc1"));
board.set(sq!("Bc2"));
board.set(sq!("Nd2"));
board.set(sq!("Pd3"));
let solver = Solver::new(board.clone());
let solutions = solver.solve();
for solution in solutions {
let mut board = board.clone();
solution
.into_iter()
.for_each(|m| assert!(board.make_move(m).is_some()));
assert_eq!(BoardState::Won, board.game_state);
}
}
#[test]
fn solver_smoke_no_solution() {
// . R . .
// R . . .
// B . B N
// P . N .
let mut board = Board::new();
board.set(sq!("Pa1"));
board.set(sq!("Ba2"));
board.set(sq!("Ra3"));
board.set(sq!("Rb4"));
board.set(sq!("Nc1"));
board.set(sq!("Bc2"));
board.set(sq!("Nd2"));
let solver = Solver::new(board.clone());
let solutions = solver.solve();
assert_eq!(0, solutions.len());
}
}
// #[cfg(test)]
// mod tests {
// use super::*;
// use crate::board::{square::Square, Board};
//
// macro_rules! sq {
// ($sq:literal) => {
// Square::parse($sq)
// };
// }
//
// #[test]
// fn solver_smoke() {
// let mut board = Board::new();
// // . R . .
// // R . . P
// // B . B N
// // P . N .
//
// board.set(sq!("Pa1"));
// board.set(sq!("Ba2"));
// board.set(sq!("Ra3"));
// board.set(sq!("Rb4"));
// board.set(sq!("Nc1"));
// board.set(sq!("Bc2"));
// board.set(sq!("Nd2"));
// board.set(sq!("Pd3"));
//
// let solver = Solver::new(board.clone());
// let solutions = solver.solve();
//
// for solution in solutions {
// let mut board = board.clone();
// solution
// .into_iter()
// .for_each(|m| assert!(board.make_move(m).is_some()));
// assert_eq!(BoardState::Won, board.game_state);
// }
// }
//
// #[test]
// fn solver_smoke_no_solution() {
// // . R . .
// // R . . .
// // B . B N
// // P . N .
//
// let mut board = Board::new();
// board.set(sq!("Pa1"));
// board.set(sq!("Ba2"));
// board.set(sq!("Ra3"));
// board.set(sq!("Rb4"));
// board.set(sq!("Nc1"));
// board.set(sq!("Bc2"));
// board.set(sq!("Nd2"));
//
// let solver = Solver::new(board.clone());
// let solutions = solver.solve();
//
// assert_eq!(0, solutions.len());
// }
// }
-38
View File
@@ -1,38 +0,0 @@
pub(crate) fn b64_encode_48(input: &[u8; 6]) -> String {
let mut output = [0 as char; 8];
for (byte_chunk, output_slice) in input.chunks_exact(3).zip(output.chunks_exact_mut(4)) {
let byte1 = byte_chunk[0];
let byte2 = byte_chunk[1];
let byte3 = byte_chunk[2];
output_slice[0] = lookup((byte1 & 0b1111_1100) >> 2);
output_slice[1] = lookup((byte1 & 0b0000_0011) << 4 | (byte2 & 0b1111_0000) >> 4);
output_slice[2] = lookup((byte2 & 0b0000_1111) << 2 | (byte3 & 0b1100_0000) >> 6);
output_slice[3] = lookup(byte3 & 0b0011_1111);
}
output.iter().collect()
}
pub(crate) fn b64_decode_48(input: &[u8; 8], output: &mut [u8; 6]) {
for (char_chunk, output_slice) in input.chunks_exact(4).zip(output.chunks_exact_mut(3)) {
let char_1 = reverse_lookup(char_chunk[0] as char);
let char_2 = reverse_lookup(char_chunk[1] as char);
let char_3 = reverse_lookup(char_chunk[2] as char);
let char_4 = reverse_lookup(char_chunk[3] as char);
output_slice[0] = (char_1 << 2) | (char_2 >> 4);
output_slice[1] = (char_2 << 4) | (char_3 >> 2);
output_slice[2] = (char_3 << 6) | char_4;
}
}
const ALPHABET: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
fn lookup(idx: u8) -> char {
ALPHABET.chars().nth(idx as usize).unwrap()
}
fn reverse_lookup(c: char) -> u8 {
ALPHABET.chars().position(|x| x == c).unwrap() as u8
}