diff --git a/Cargo.lock b/Cargo.lock index f2a7268..0dc8367 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + [[package]] name = "bumpalo" version = "3.16.0" @@ -159,7 +165,21 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "wasm-bindgen", + "windows-targets", ] [[package]] @@ -205,9 +225,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", @@ -319,7 +339,7 @@ version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", @@ -392,7 +412,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -441,6 +461,7 @@ name = "sol_chess" version = "0.1.1" dependencies = [ "argh", + "getrandom 0.3.1", "indicatif", "macroquad", "rand", @@ -488,10 +509,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.99" +name = "wasi" +version = "0.13.3+wasi-0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", @@ -500,9 +530,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", @@ -514,9 +544,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -524,9 +554,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -537,9 +567,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-time" @@ -646,6 +679,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 8df5834..ef954c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ default-run = "sol_chess" [dependencies] argh = "0.1.13" +getrandom = { version = "0.3.1", features = ["wasm_js"] } indicatif = "0.17.9" macroquad = "0.4.13" rand = "0.8.5" diff --git a/README.md b/README.md index 12cc0bb..3c0950f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Goal: Generate 'hard' puzzles. ## Usage -- [WIP] Run `sol_chess` to start a windowed GUI game. +- Run `sol_chess` to start a windowed GUI game. - Run `sol_cli` to start the CLI tool. ## CLI Usage diff --git a/src/board.rs b/src/board.rs index 5778109..d09fa47 100644 --- a/src/board.rs +++ b/src/board.rs @@ -4,7 +4,12 @@ pub mod errors; pub mod piece; pub mod square; -use std::{collections::HashSet, mem}; +use core::fmt; +use std::{ + collections::HashSet, + fmt::{Display, Formatter}, + mem, +}; use cmove::CMove; use constants::BOARD_SIZE; @@ -16,12 +21,12 @@ use square::{Square, SquarePair}; pub struct Board { pub cells: [[Option; BOARD_SIZE]; BOARD_SIZE], pub legal_moves: HashSet, - pub game_state: GameState, + pub game_state: BoardState, pieces_remaining: u8, } #[derive(PartialEq, Eq, Debug, Clone)] -pub enum GameState { +pub enum BoardState { NotStarted, InProgress, Lost, @@ -34,7 +39,7 @@ impl Board { cells: [[None; BOARD_SIZE]; BOARD_SIZE], legal_moves: HashSet::new(), pieces_remaining: 0, - game_state: GameState::NotStarted, + game_state: BoardState::NotStarted, } } @@ -258,13 +263,13 @@ impl Board { fn calc_game_state(&mut self) { self.game_state = if self.pieces_remaining == 0 { - GameState::NotStarted + BoardState::NotStarted } else if self.pieces_remaining == 1 { - GameState::Won + BoardState::Won } else if self.legal_moves.is_empty() { - GameState::Lost + BoardState::Lost } else { - GameState::InProgress + BoardState::InProgress } } @@ -350,6 +355,19 @@ fn get_square_for_display(piece: &Option, pretty: bool) -> String { } } +impl Display for BoardState { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let display = match self { + BoardState::NotStarted => "Not Started", + BoardState::InProgress => "In Progress", + BoardState::Lost => "Lost", + BoardState::Won => "Won", + }; + + write!(f, "{}", display) + } +} + #[cfg(test)] mod tests { use super::*; @@ -502,7 +520,7 @@ mod tests { #[test] fn test_smoke_puzzle() { let mut board = Board::new(); - assert_eq!(GameState::NotStarted, board.game_state); + assert_eq!(BoardState::NotStarted, board.game_state); assert_eq!(0, board.pieces_remaining); // K . . . @@ -510,22 +528,22 @@ mod tests { // . . R . // N . . . board.set(sq!("Ka4")); - assert_eq!(GameState::Won, board.game_state); + assert_eq!(BoardState::Won, board.game_state); board.set(sq!("Pb3")); board.set(sq!("Rc2")); board.set(sq!("Na1")); - assert_eq!(GameState::InProgress, board.game_state); + assert_eq!(BoardState::InProgress, board.game_state); assert_eq!(4, board.pieces_remaining); assert!(board.make_move(mv!("Na1", "Rc2")).is_some()); assert_eq!(3, board.pieces_remaining); - assert_eq!(GameState::InProgress, board.game_state); + assert_eq!(BoardState::InProgress, board.game_state); assert!(board.make_move(mv!("Pb3", "Ka4")).is_some()); assert_eq!(2, board.pieces_remaining); - assert_eq!(GameState::Lost, board.game_state); + assert_eq!(BoardState::Lost, board.game_state); // P . . . // . . . . @@ -540,12 +558,12 @@ mod tests { // . . N . // P . . . assert_eq!(4, board.pieces_remaining); - assert_eq!(GameState::InProgress, board.game_state); + assert_eq!(BoardState::InProgress, board.game_state); board.make_move(mv!("Qa3", "Pa4")); board.make_move(mv!("Nc2", "Pa1")); assert_eq!(2, board.pieces_remaining); - assert_eq!(GameState::InProgress, board.game_state); + assert_eq!(BoardState::InProgress, board.game_state); // Q . . . // . . . . @@ -553,7 +571,7 @@ mod tests { // N . . . board.make_move(mv!("Qa4", "Na1")); assert_eq!(1, board.pieces_remaining); - assert_eq!(GameState::Won, board.game_state); + assert_eq!(BoardState::Won, board.game_state); } #[test] diff --git a/src/generator.rs b/src/generator.rs index cbe850f..c6be747 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -161,7 +161,7 @@ fn try_generate( #[cfg(test)] mod tests { - use crate::{board::GameState, solver::Solver}; + use crate::{board::BoardState, solver::Solver}; use super::*; @@ -170,7 +170,7 @@ mod tests { for _ in 0..10 { let gen_stats = generate(5, 5); let board = gen_stats.board.expect("No puzzle was generated"); - assert_eq!(board.game_state, GameState::InProgress); + assert_eq!(board.game_state, BoardState::InProgress); let solutions = Solver::new(board).solve(); assert!(solutions.len() <= 5); diff --git a/src/main.rs b/src/main.rs index 7cb8d73..c513e8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,39 +1,42 @@ +use core::fmt; +use std::fmt::{Display, Formatter}; + use game::texture::PieceTexture; use macroquad::prelude::*; -use sol_chess::board::{square::Square, Board}; +use sol_chess::{ + board::{Board, BoardState}, + generator, +}; mod game; #[macroquad::main("Solitaire Chess")] async fn main() { let background_color = Color::from_rgba(196, 195, 208, 255); - let game = init().await; + let mut game = init().await; loop { clear_background(background_color); + draw_heading("Solitaire Chess"); + game.handle_input(); game.draw(); next_frame().await } } +fn draw_heading(title: &str) { + let dims = measure_text(title, None, 60, 1.0); + let x = screen_width() / 2.0 - dims.width / 2.0; + let y = 2.0 * dims.height; + draw_text(title, x, y, 60.0, BLACK); +} + 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")); - + let generate = generator::generate(6, 100); + let board = generate.board().expect("No puzzle was generated"); let square_width = 128.0; let num_squares = 4; let x = (screen_width() - (square_width * num_squares as f32)) / 2.0; @@ -44,10 +47,31 @@ async fn init() -> Game { } struct Game { + original_board: Board, board: Board, squares: Vec, texture_res: Texture2D, num_squares: usize, + state: GameState, + debug: bool, + info_square: Rect, +} + +struct GameSquare { + rect: Rect, + color: Color, + is_source: bool, + is_target: bool, + is_previous_target: bool, + i: usize, + j: usize, +} + +#[derive(Copy, Clone)] +enum GameState { + SelectSource(Option<(usize, usize)>), + SelectTarget((usize, usize)), + GameOver((usize, usize)), } impl Game { @@ -72,15 +96,31 @@ impl Game { _ => light, }; - rects.push(GameSquare { rect, color, i, j }); + rects.push(GameSquare { + rect, + color, + i, + j, + is_source: false, + is_target: false, + is_previous_target: false, + }); } } + let info_x = x; + let info_y = y + (num_squares as f32 * square_width) + square_width / 2.0; + let info_w = square_width * num_squares as f32; + Self { + original_board: board.clone(), board, squares: rects, num_squares, texture_res, + state: GameState::SelectSource(None), + debug: false, + info_square: Rect::new(info_x, info_y, info_w, square_width), } } @@ -90,33 +130,307 @@ impl Game { fn draw(&self) { let sprite_size = 100.0; + let mut selected_square = None; self.squares.iter().for_each(|square| { + let color = if square.is_source { + Color::from_rgba(152, 152, 152, 255) + } else if square.is_target { + Color::from_rgba(152, 129, 123, 255) + } else { + square.color + }; + draw_rectangle( square.rect.x, square.rect.y, square.rect.w, square.rect.h, - square.color, + 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); + if !square.is_source { + draw_texture_ex( + &self.texture_res, + square.rect.x + offset, + square.rect.y + offset, + WHITE, + dtp, + ); + } else { + selected_square = Some(square); + } + } + }); + + if let Some(selected_square) = selected_square { + if let Some(p) = self.board.cells[selected_square.i][selected_square.j] { + let dtp = PieceTexture::for_piece(p, sprite_size); draw_texture_ex( &self.texture_res, - square.rect.x + offset, - square.rect.y + offset, + mouse_position().0 - sprite_size / 2.0, + mouse_position().1 - sprite_size / 2.0, WHITE, dtp, ); } - }); + } + + draw_text( + &format!("Press 'R' to reset"), + self.info_square.x + 20.0, + self.info_square.y + 20.0, + 20.0, + BLACK, + ); + + draw_text( + &format!("Press 'N' for new game (when the current game is won)"), + self.info_square.x + 20.0, + self.info_square.y + 40.0, + 20.0, + BLACK, + ); + + draw_text( + &format!("Press 'D' to toggle debug mode"), + self.info_square.x + 20.0, + self.info_square.y + 60.0, + 20.0, + GRAY, + ); + + if self.debug { + let mut debug_lines = vec![]; + let (mx, my) = mouse_position(); + let hover_square = self.squares.iter().find(|s| { + let c = Circle::new(mx, my, 0.0); + if c.overlaps_rect(&s.rect) { + return true; + } + return false; + }); + debug_lines.push(format!("Game State: {}", self.state)); + debug_lines.push(format!("Board State: {}", self.board.game_state)); + if let Some(hover_square) = hover_square { + debug_lines.push(format!("Hover: [ {}, {} ]", hover_square.i, hover_square.j)); + } + self.add_debug_info(debug_lines); + + self.show_fps(); + } + } + + fn handle_input(&mut self) { + if is_key_released(KeyCode::R) { + self.reset(); + return; + } + + if is_key_released(KeyCode::N) { + if let GameState::GameOver(_) = self.state { + self.next_puzzle(); + } + return; + } + + if is_key_released(KeyCode::D) { + self.debug = !self.debug; + return; + } + + if is_mouse_button_pressed(MouseButton::Right) { + let current_state = self.state.clone(); + let new_state = match current_state { + GameState::SelectSource(_) => GameState::SelectSource(None), + GameState::SelectTarget((_, _)) => { + self.reset_squares(); + GameState::SelectSource(None) + } + GameState::GameOver((i, j)) => GameState::SelectSource(Some((i, j))), + }; + self.state = new_state; + return; + } + + if is_mouse_button_pressed(MouseButton::Left) { + let current_state = self.state.clone(); + let new_state = match current_state { + GameState::SelectSource(previous_target) => { + self.handle_select_source(mouse_position(), previous_target) + } + GameState::SelectTarget(source) => GameState::SelectTarget(source), + GameState::GameOver(previous_target) => GameState::GameOver(previous_target), + }; + + self.state = new_state; + } + + if is_mouse_button_released(MouseButton::Left) { + let current_state = self.state.clone(); + let new_state = match current_state { + GameState::SelectSource(previous_target) => { + GameState::SelectSource(previous_target) + } + GameState::SelectTarget(source) => { + self.handle_select_target(mouse_position(), source) + } + GameState::GameOver(previous_target) => GameState::GameOver(previous_target), + }; + + self.state = new_state; + } + } + + fn handle_select_source( + &mut self, + mouse_position: (f32, f32), + previous_target: Option<(usize, usize)>, + ) -> GameState { + self.reset_squares(); + let (x, y) = mouse_position; + let mouse = Circle::new(x, y, 0.0); + 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] { + selected = Some((square.i, square.j)); + } + } + } + + 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() { + if m.from.file == i && m.from.rank == j { + target_squares.push((m.to.file, m.to.rank)); + } + } + + for (i, j) in target_squares { + self.get(i, j).is_target = true; + } + + return GameState::SelectTarget(selected.unwrap()); + } + + if let Some((i, j)) = previous_target { + self.get(i, j).is_previous_target = true; + } + + return GameState::SelectSource(None); + } + + fn handle_select_target( + &mut self, + mouse_position: (f32, f32), + source: (usize, usize), + ) -> GameState { + let (x, y) = mouse_position; + let mouse = Circle::new(x, y, 0.0); + + 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] { + selected = Some((square.i, square.j)); + } + } + } + + let (s_x, s_y) = source; + let Some((x, y)) = selected else { + self.get(s_x, s_y).is_source = true; + return GameState::SelectTarget(source); + }; + + if x == s_x && y == s_y { + self.get(s_x, s_y).is_source = true; + return GameState::SelectTarget(source); + } + + let mut is_legal = false; + if self.get(x, y).is_target { + is_legal = true; + } + + if is_legal { + let m = self.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()); + + if self.board.game_state == BoardState::Won || self.board.game_state == BoardState::Lost + { + self.reset_squares(); + return GameState::GameOver((x, y)); + } + + self.reset_squares(); + self.get(x, y).is_target = true; + return GameState::SelectSource(Some((x, y))); + } + + self.reset_squares(); + return GameState::SelectSource(None); + } + + fn reset(&mut self) { + self.board = self.original_board.clone(); + self.reset_squares(); + self.state = GameState::SelectSource(None); + } + + fn next_puzzle(&mut self) { + self.reset(); + let generate = generator::generate(6, 100); + let board = generate.board().expect("No puzzle was generated"); + self.original_board = board.clone(); + self.board = board; + } + + fn reset_squares(&mut self) { + for i in 0..self.num_squares { + for j in 0..self.num_squares { + self.get(i, j).is_source = false; + self.get(i, j).is_target = false; + } + } + } + + fn add_debug_info(&self, lines: Vec) { + let mut y = 20.0; + for line in lines { + draw_text(&line, 10.0, y, 20.0, BLACK); + y += 25.0; + } + } + + fn show_fps(&self) { + let fps = get_fps(); + draw_text( + &format!("FPS: {}", fps), + 10.0, + screen_height() - 20.0, + 20.0, + BLACK, + ); } } -struct GameSquare { - rect: Rect, - color: Color, - i: usize, - j: usize, +impl Display for GameState { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + GameState::SelectSource(Some(x)) => write!(f, "Select Source [ {}, {} ]", x.0, x.1), + GameState::SelectSource(None) => write!(f, "Select Source [ ]"), + GameState::SelectTarget(x) => write!(f, "Select Target [ {}, {} ]", x.0, x.1), + GameState::GameOver(x) => write!(f, "Game Over [ {}, {} ]", x.0, x.1), + } + } } diff --git a/src/solver.rs b/src/solver.rs index 978add3..dca7490 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -1,6 +1,6 @@ use crate::board::{ cmove::CMove, - {Board, GameState}, + {Board, BoardState}, }; pub struct Solver { @@ -26,12 +26,12 @@ impl Solver { pub fn solve(&self) -> Vec> { let mut solutions = Vec::new(); - if let GameState::Won = self.board.game_state { + if let BoardState::Won = self.board.game_state { solutions.push(self.moves.clone()); return solutions; } - let GameState::InProgress = self.board.game_state else { + let BoardState::InProgress = self.board.game_state else { return solutions; }; @@ -81,7 +81,7 @@ mod tests { solution .into_iter() .for_each(|m| assert!(board.make_move(m).is_some())); - assert_eq!(GameState::Won, board.game_state); + assert_eq!(BoardState::Won, board.game_state); } }