23 Commits

Author SHA1 Message Date
cool-mist ab0c333935 Add sounds 2025-06-04 16:05:11 +05:30
cool-mist c551715a52 Finish, mudinji pochi 2025-05-25 23:21:47 +05:30
cool-mist d758d03f2d format difficulty buttons 2025-05-18 00:22:21 +05:30
cool-mist d90e270cd5 Add difficulty levels 2025-05-18 00:09:11 +05:30
cool-mist 373d3da91b center everything correctly 2025-05-17 23:11:02 +05:30
cool-mist 1698f3ce87 Refactor tools 2025-05-17 20:30:50 +05:30
cool-mist ace89713d7 Fix scripts 2025-05-17 14:39:59 +05:30
cool-mist 77a640e1b7 Add clean script 2025-05-17 10:37:45 +05:30
cool-mist 32ff856cfa update readme 2025-05-11 23:47:49 +05:30
cool-mist ff13e49720 decorations 2025-05-11 23:47:03 +05:30
cool-mist 654469bb4a enable tests 2025-05-11 23:10:02 +05:30
cool-mist c1b0ea24a6 Better on browsers 2025-05-11 19:25:22 +05:30
cool-mist 977c24968b Grey out inactive buttons 2025-05-11 19:19:52 +05:30
cool-mist 1dfe56ac50 Animation on button click 2025-05-11 19:00:52 +05:30
cool-mist 8b71bce89c Add shadows to buttons 2025-05-11 17:22:07 +05:30
cool-mist dc4ae27074 Correctly center button text 2025-05-11 16:48:42 +05:30
cool-mist c8f1c043ee Automatically move dev instance to different workspace 2025-05-11 16:12:05 +05:30
Surya Prakash 9221f3f38a Update README.md 2025-03-24 00:24:09 +05:30
cool-mist 6b321e034f Add buttons, refactor, make it more mouse friendly 2025-03-24 00:19:34 +05:30
cool-mist 5560013e33 Seed random number generation 2025-02-23 17:16:15 +05:30
cool-mist 89e43686ca Do the actual deployment using sudo 2025-02-23 15:31:59 +05:30
cool-mist 111a01faac Fix scripts 2025-02-23 15:26:42 +05:30
cool-mist 74a91f5a1f Remove unnecessary, package as tar 2025-02-23 15:15:26 +05:30
34 changed files with 1339 additions and 790 deletions
+2
View File
@@ -21,3 +21,5 @@ Cargo.lock
.idea/
dist/
sol_chess.tar.gz
local-deploy/
Generated
+202 -3
View File
@@ -17,7 +17,7 @@ dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
"zerocopy 0.7.35",
]
[[package]]
@@ -52,6 +52,24 @@ dependencies = [
"serde",
]
[[package]]
name = "audir-sles"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea47348666a8edb7ad80cbee3940eb2bccf70df0e6ce09009abe1a836cb779f5"
[[package]]
name = "audrey"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58b92a84e89497e3cd25d3672cd5d1c288abaac02c18ff21283f17d118b889b8"
dependencies = [
"dasp_frame",
"dasp_sample",
"hound",
"lewton",
]
[[package]]
name = "autocfg"
version = "1.4.0"
@@ -64,6 +82,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "bytemuck"
version = "1.21.0"
@@ -97,6 +121,21 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "dasp_frame"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6"
dependencies = [
"dasp_sample",
]
[[package]]
name = "dasp_sample"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
[[package]]
name = "fdeflate"
version = "0.3.7"
@@ -126,6 +165,18 @@ dependencies = [
"ttf-parser",
]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi",
]
[[package]]
name = "glam"
version = "0.27.0"
@@ -141,6 +192,12 @@ dependencies = [
"ahash",
]
[[package]]
name = "hound"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
[[package]]
name = "image"
version = "0.24.9"
@@ -154,6 +211,17 @@ dependencies = [
"png",
]
[[package]]
name = "lewton"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d542c1a317036c45c2aa1cf10cc9d403ca91eb2d333ef1a4917e5cb10628bd0"
dependencies = [
"byteorder",
"ogg",
"smallvec",
]
[[package]]
name = "libc"
version = "0.2.169"
@@ -172,6 +240,7 @@ dependencies = [
"macroquad_macro",
"miniquad",
"quad-rand",
"quad-snd",
"slotmap",
]
@@ -190,6 +259,12 @@ dependencies = [
"libc",
]
[[package]]
name = "maybe-uninit"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]]
name = "miniquad"
version = "0.4.6"
@@ -236,6 +311,15 @@ dependencies = [
"malloc_buf",
]
[[package]]
name = "ogg"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e571c3517af9e1729d4c63571a27edd660ade0667973bfc74a67c660c2b651"
dependencies = [
"byteorder",
]
[[package]]
name = "once_cell"
version = "1.20.2"
@@ -248,13 +332,22 @@ 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",
"miniz_oxide",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy 0.8.25",
]
[[package]]
name = "proc-macro2"
version = "1.0.92"
@@ -264,12 +357,34 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quad-alsa-sys"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66c2f04a6946293477973d85adc251d502da51c57b08cd9c997f0cfd8dcd4b5"
dependencies = [
"libc",
]
[[package]]
name = "quad-rand"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40"
[[package]]
name = "quad-snd"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cba0c4943fc67147fbe9d1eb731fb9e678bfc9d926507eebbbfe0103e154e5b0"
dependencies = [
"audir-sles",
"audrey",
"libc",
"quad-alsa-sys",
"winapi",
]
[[package]]
name = "quote"
version = "1.0.38"
@@ -279,6 +394,41 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "rand"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom",
]
[[package]]
name = "rust-fuzzy-search"
version = "0.1.1"
@@ -320,12 +470,23 @@ dependencies = [
"version_check",
]
[[package]]
name = "smallvec"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0"
dependencies = [
"maybe-uninit",
]
[[package]]
name = "sol_chess"
version = "0.1.1"
dependencies = [
"argh",
"macroquad",
"quad-snd",
"rand",
]
[[package]]
@@ -357,6 +518,15 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "winapi"
version = "0.3.9"
@@ -379,13 +549,31 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.0",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
dependencies = [
"zerocopy-derive 0.8.25",
]
[[package]]
@@ -398,3 +586,14 @@ dependencies = [
"quote",
"syn",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
+5 -1
View File
@@ -6,7 +6,11 @@ default-run = "sol_chess"
[dependencies]
argh = "0.1.13"
macroquad = "0.4.13"
macroquad = { version = "0.4.13", features = ["audio"] }
quad-snd = "0.2.8"
[dev-dependencies]
rand = "0.9.1"
[profile.release]
opt-level = 's'
+8
View File
@@ -2,6 +2,10 @@
Goal: Generate 'hard' puzzles.
### Play a demo of the game [here](https://games.neophyte.me/sol_chess/)
![img](./img/sol_chess.png)
## Install
- Install Rust from [here](https://www.rust-lang.org/tools/install).
@@ -13,6 +17,10 @@ Goal: Generate 'hard' puzzles.
- Run `sol_chess` to start a windowed GUI game.
- Run `sol_cli` to start the CLI tool.
## Demo site
- Play a demo of the game [here](https://games.neophyte.me/sol_chess/)
## CLI Usage
- Generate a puzzle
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
-6
View File
@@ -1,6 +0,0 @@
cargo build --target wasm32-unknown-unknown --release
mkdir -p ./dist
rm ./web/sol_chess.wasm
mv ./target/wasm32-unknown-unknown/release/sol_chess.wasm ./dist/sol_chess.wasm
basic-http-server ./dist
-6
View File
@@ -1,6 +0,0 @@
cargo build --target wasm32-unknown-unknown --release
mkdir -p ./dist
rm ./web/sol_chess.wasm
mv ./target/wasm32-unknown-unknown/release/sol_chess.wasm ./dist/sol_chess.wasm
basic-http-server ./dist
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

+10 -2
View File
@@ -1,9 +1,17 @@
use argh::FromArgs;
use sol_chess::board::Board;
use sol_chess::generator;
use sol_chess::generator::{self, RandomRange};
use sol_chess::solver::Solver;
// Learn how to specify a different dependency for this binary
struct MacroquadRngTodo;
impl RandomRange for MacroquadRngTodo {
fn gen_range(&self, min: usize, max: usize) -> usize {
macroquad::rand::gen_range(min, max)
}
}
fn main() {
let args: Args = argh::from_env();
@@ -65,7 +73,7 @@ fn generate_puzzle(num_pieces: Option<u32>, num_solutions: Option<u32>) -> Optio
"Generating a puzzle with {} pieces with a maximum of {} solutions",
num_pieces, num_solutions
);
let gen = generator::generate(num_pieces, num_solutions);
let gen = generator::generate(num_pieces, num_solutions, &MacroquadRngTodo);
gen.print_stats();
let Some(board) = gen.board() else {
+1 -1
View File
@@ -21,7 +21,7 @@ impl CMove {
from,
to_piece,
to,
disambig
disambig,
}
}
+696
View File
@@ -1 +1,697 @@
use std::{
collections::HashMap,
fmt::{self, Display, Formatter},
};
use button::Button;
use color::UiColor;
use macroquad::{audio, math, prelude::*, rand};
use shadow::draw_shadow;
use sol_chess::{
board::{Board, BoardState},
generator::{self, RandomRange},
};
use sound::Sounds;
use texture::PieceTexture;
pub mod button;
pub mod color;
pub mod shadow;
pub mod sound;
pub mod texture;
pub struct MacroquadRandAdapter;
impl RandomRange for MacroquadRandAdapter {
fn gen_range(&self, min: usize, max: usize) -> usize {
rand::gen_range(min, max)
}
}
pub struct Game {
// The generated puzzle. We keep a copy of this to reset the game.
original_board: Board,
// What is shown to the user
board: Board,
// Constants througout the game
texture_res: Texture2D,
sounds: Sounds,
num_squares: usize,
heading_text: String,
// Update below on handle input
state: GameState,
debug: bool,
game_mode: GameMode,
// Update below on window resize
// Used for drawing the state
square_width: f32,
window_height: f32,
window_width: f32,
board_rect: Rect,
squares: Vec<GameSquare>,
heading_rect: Rect,
heading_font_size: f32,
gp_btns: HashMap<ButtonAction, Button>,
mode_btns: HashMap<GameMode, Button>,
rules: bool,
rules_btn: Vec<Button>,
}
struct GameSquare {
rect: Rect,
color: Color,
is_source: bool,
is_target: bool,
is_previous_target: bool,
i: usize,
j: usize,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum ButtonAction {
Reset,
Next,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum GameMode {
Easy,
Medium,
Hard,
}
#[derive(Copy, Clone)]
enum GameState {
SelectSource(Option<(usize, usize)>),
SelectTarget((usize, usize)),
GameOver((usize, usize)),
}
impl Game {
pub fn new(texture_res: Texture2D, sounds: Sounds) -> Self {
let num_squares: usize = 4;
let game_mode = GameMode::Medium;
let board = Game::generate_puzzle(game_mode);
Self {
original_board: board.clone(),
board,
board_rect: Rect::new(0., 0., 0., 0.),
squares: Vec::new(),
heading_rect: Rect::new(0., 0., 0., 0.),
heading_text: "Solitaire Chess".to_string(),
heading_font_size: 0.,
num_squares,
texture_res,
sounds,
state: GameState::SelectSource(None),
game_mode,
debug: false,
gp_btns: HashMap::new(),
mode_btns: HashMap::new(),
rules: false,
rules_btn: Vec::new(),
window_height: 0.,
window_width: 0.,
square_width: 0.,
}
}
pub fn draw(&mut self) {
self.update_window_size();
self.draw_heading();
self.draw_board();
self.draw_buttons();
self.draw_debug();
}
fn update_window_size(&mut self) {
let new_height = math::clamp(screen_height(), 200.0, 10000.0);
let new_width = math::clamp(screen_width(), 200.0, 10000.0);
if new_height == self.window_height && new_width == self.window_width {
return;
}
self.window_height = new_height;
self.window_width = new_width;
self.update_drawables();
}
pub fn handle_input(&mut self) {
let mut gp_btn_clicked = None;
for btn in &mut self.gp_btns {
btn.1.handle_input();
if btn.1.is_clicked() {
gp_btn_clicked = Some(btn.0.clone());
break;
}
}
if let Some(action) = gp_btn_clicked {
match action {
ButtonAction::Reset => self.reset(),
ButtonAction::Next => self.next_puzzle(),
}
} else {
let mut mode_btn_clicked = None;
for btn in &mut self.mode_btns {
btn.1.handle_input();
if btn.1.is_clicked() {
mode_btn_clicked = Some(btn);
break;
}
}
if let Some(btn) = mode_btn_clicked {
self.game_mode = *btn.0;
self.next_puzzle();
} else {
let mut rules_btn_clicked = false;
for btn in &mut self.rules_btn {
btn.handle_input();
if btn.is_clicked() {
rules_btn_clicked = true;
break;
}
}
if rules_btn_clicked {
self.rules = !self.rules;
if self.rules {
self.rules_btn[0].text = "Close".to_string();
self.rules_btn[0].color = UiColor::Green;
} else {
self.rules_btn[0].text = "Rules".to_string();
self.rules_btn[0].color = UiColor::Brown;
}
}
}
}
for btn in &mut self.mode_btns {
if self.game_mode == *btn.0 {
btn.1.is_active = false;
} else {
btn.1.is_active = true;
}
}
if is_key_released(KeyCode::Escape) {
self.rules = false;
return;
}
if is_key_released(KeyCode::D) {
self.debug = !self.debug;
return;
}
if is_key_released(KeyCode::Q) {
std::process::exit(0);
}
if is_mouse_button_released(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) => {
let next = self.handle_select_target(mouse_position(), source);
if let GameState::SelectTarget(_) = next {
self.reset_squares();
GameState::SelectSource(None)
} else {
next
}
}
GameState::GameOver(previous_target) => GameState::GameOver(previous_target),
};
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;
}
}
fn draw_heading(&self) {
let f = self.heading_font_size.floor() as u16;
let dims = measure_text(self.heading_text.as_str(), None, f, 1.0);
draw_text(
self.heading_text.as_str(),
self.heading_rect.x,
self.heading_rect.y + dims.offset_y,
self.heading_font_size,
BLACK,
);
}
fn draw_board(&self) {
let board_shadow_width = 0.1 * self.square_width;
draw_shadow(self.board_rect, board_shadow_width);
if self.rules {
draw_rectangle(
self.board_rect.x,
self.board_rect.y,
self.board_rect.w,
self.board_rect.h,
UiColor::Brown.to_bg_color(),
);
let font_size = self.heading_font_size * 0.8;
let rules = "\
Every move should be a \n\
capture. Win when only \n\
one piece is left.\n";
let measurement = measure_text(rules, None, font_size as u16, 1.0);
draw_multiline_text(
rules,
self.board_rect.x + 0.05 * self.square_width,
self.board_rect.y + 0.5 * (self.board_rect.h - measurement.height)
- 2. * measurement.offset_y,
font_size,
Some(2.),
UiColor::Brown.to_fg_color(),
);
return;
}
let sprite_size = 0.8 * self.square_width;
let mut selected_square = None;
self.squares.iter().for_each(|square| {
let color = match square.is_source {
true => square.color,
false => match square.is_target {
true => UiColor::Pink.to_shadow_color(),
false => square.color,
},
};
draw_rectangle(
square.rect.x,
square.rect.y,
square.rect.w,
square.rect.h,
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,
mouse_position().0 - sprite_size / 2.0,
mouse_position().1 - sprite_size / 2.0,
WHITE,
dtp,
);
}
}
}
fn draw_buttons(&self) {
for btn in &self.gp_btns {
btn.1.draw();
}
for btn in &self.mode_btns {
btn.1.draw();
}
for btn in &self.rules_btn {
btn.draw();
}
}
fn draw_debug(&self) {
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 get(&mut self, i: usize, j: usize) -> &mut GameSquare {
&mut self.squares[i * self.num_squares + j]
}
fn update_drawables(&mut self) {
let min_dimension = f32::min(self.window_height, self.window_width);
self.square_width = 0.15 * min_dimension;
let board_width = self.square_width * self.num_squares as f32;
let board_x = (self.window_width - board_width) / 2.0;
let board_y = (self.window_height - board_width) / 2.0;
self.board_rect = Rect::new(board_x, board_y, board_width, board_width);
self.heading_font_size = 0.07 * min_dimension;
let f = self.heading_font_size.floor() as u16;
let dims = measure_text(self.heading_text.as_str(), None, f, 1.0);
self.heading_rect = Rect::new(
board_x + (board_width - dims.width) / 2.0,
(board_y - dims.height) / 2.0,
dims.width,
dims.height,
);
let dark = UiColor::Brown.to_bg_color();
let light = UiColor::Yellow.to_bg_color();
let mut rects = Vec::new();
for i in 0..self.num_squares {
for j in 0..self.num_squares {
let x_eff = board_x + (i as f32 * self.square_width);
let y_eff = board_y + (j as f32 * self.square_width);
let rect = Rect::new(x_eff, y_eff, self.square_width, self.square_width);
let color = match (i + j) % 2 {
1 => dark,
_ => light,
};
rects.push(GameSquare {
rect,
color,
i,
j,
is_source: false,
is_target: false,
is_previous_target: false,
});
}
}
self.squares = rects;
let btn_h = 0.08 * min_dimension;
let btn_w = board_width * 0.2;
let btn_y = board_width + board_y + 0.3 * self.square_width;
let btn_x_offset = 0.5 * (board_width / 2. - btn_w);
let reset_btn = Button::new(
"Reset",
Rect::new(board_x + btn_x_offset, btn_y, btn_w, btn_h),
UiColor::Yellow,
self.sounds.button.clone(),
);
let mut next_btn = Button::new(
"Next",
Rect::new(
board_x + (0.5 * board_width) + btn_x_offset,
btn_y,
btn_w,
btn_h,
),
UiColor::Green,
self.sounds.button.clone(),
);
next_btn.is_active = false;
let rules_button = Button::new(
"Rules",
Rect::new(
(board_x - btn_w) / 2.,
board_y + (self.square_width - btn_h) / 2.,
btn_w,
btn_h,
),
UiColor::Brown,
self.sounds.button.clone(),
);
self.rules_btn = vec![rules_button];
self.gp_btns = HashMap::new();
self.gp_btns.insert(ButtonAction::Next, next_btn);
self.gp_btns.insert(ButtonAction::Reset, reset_btn);
let easy_btn = Button::new(
"Easy",
Rect::new(
(board_x - btn_w) / 2.,
board_y + self.square_width + (self.square_width - btn_h) / 2.,
btn_w,
btn_h,
),
UiColor::Yellow,
self.sounds.mode.clone(),
);
let medium_btn = Button::new(
"Medium",
Rect::new(
(board_x - btn_w) / 2.,
board_y + 2. * self.square_width + (self.square_width - btn_h) / 2.,
btn_w,
btn_h,
),
UiColor::Yellow,
self.sounds.mode.clone(),
);
let hard_button = Button::new(
"Hard",
Rect::new(
(board_x - btn_w) / 2.,
board_y + 3. * self.square_width + (self.square_width - btn_h) / 2.,
btn_w,
btn_h,
),
UiColor::Yellow,
self.sounds.mode.clone(),
);
self.mode_btns = HashMap::new();
self.mode_btns.insert(GameMode::Easy, easy_btn);
self.mode_btns.insert(GameMode::Medium, medium_btn);
self.mode_btns.insert(GameMode::Hard, hard_button);
for btn in &mut self.mode_btns {
btn.1.is_active = true;
if self.game_mode == *btn.0 {
btn.1.is_active = false;
}
}
}
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();
if self.board.game_state == BoardState::Won {
let next_btn = self
.gp_btns
.get_mut(&ButtonAction::Next)
.expect("Cannot find next button");
next_btn.is_active = true;
audio::play_sound_once(&self.sounds.win);
} else {
audio::play_sound_once(&self.sounds.loss);
}
return GameState::GameOver((x, y));
}
self.reset_squares();
self.get(x, y).is_target = true;
audio::play_sound_once(&self.sounds.click);
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();
let next_button = self
.gp_btns
.get_mut(&ButtonAction::Next)
.expect("Cannot find next button");
next_button.is_active = false;
self.state = GameState::SelectSource(None);
}
fn next_puzzle(&mut self) {
self.reset();
let board = Game::generate_puzzle(self.game_mode);
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<String>) {
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,
);
}
fn generate_puzzle(mode: GameMode) -> Board {
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")
}
}
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),
}
}
}
+130
View File
@@ -0,0 +1,130 @@
use macroquad::{audio::{self, Sound}, prelude::*};
use super::{color::UiColor, shadow::draw_shadow};
pub struct Button {
pub is_active: bool,
pub text: String,
is_down: bool,
is_clicked: bool,
rect: Rect,
shadow_width: f32,
pub color: UiColor,
sound: Sound,
}
impl Button {
pub fn new(text: &str, rect: Rect, color: UiColor, sound: Sound) -> Self {
Self {
text: text.to_string(),
is_down: false,
is_clicked: false,
is_active: true,
rect,
shadow_width: 5.0,
color,
sound,
}
}
pub fn is_clicked(&mut self) -> bool {
if self.is_clicked {
self.is_clicked = false;
return true;
}
false
}
pub fn draw(&self) {
self.draw_button();
self.draw_label();
}
fn draw_button(&self) {
let bg_color = match self.is_active {
true => self.color.to_bg_color(),
false => self.color.to_shadow_color(),
};
let button_draw_offset = self.get_button_draw_offset();
draw_rectangle(
self.rect.x + button_draw_offset,
self.rect.y + button_draw_offset,
self.rect.w,
self.rect.h,
bg_color,
);
self.draw_shadow();
}
fn draw_shadow(&self) {
if !self.is_active {
return;
}
if self.is_down {
return;
}
draw_shadow(self.rect, self.shadow_width);
}
fn draw_label(&self) {
let font_color = match self.is_active {
true => self.color.to_fg_color(),
false => Color::from_rgba(100, 100, 100, 255),
};
let font_size = (0.3 * self.rect.w) as u16;
let dims = measure_text(&self.text, None, font_size, 1.0);
let button_draw_offset = self.get_button_draw_offset();
draw_text(
&self.text,
self.rect.x + (self.rect.w - dims.width) * 0.5 + button_draw_offset,
self.rect.y + (self.rect.h - dims.height) * 0.5 + dims.offset_y + button_draw_offset,
font_size as f32,
font_color,
);
}
fn get_button_draw_offset(&self) -> f32 {
let button_pressed_correction = match self.is_down {
true => self.shadow_width,
false => match self.is_active {
true => 0.0,
false => self.shadow_width,
},
};
button_pressed_correction
}
pub fn handle_input(&mut self) {
if !self.is_active {
self.is_down = false;
return;
}
let (mx, my) = mouse_position();
let c = Circle::new(mx, my, 0.0);
if is_mouse_button_pressed(MouseButton::Left) {
if c.overlaps_rect(&self.rect) {
self.is_down = true;
return;
}
}
if is_mouse_button_released(MouseButton::Left) {
if c.overlaps_rect(&self.rect) {
self.is_clicked = true;
audio::play_sound_once(&self.sound);
self.is_down = false;
return;
}
self.is_down = false;
}
}
}
+45
View File
@@ -0,0 +1,45 @@
use macroquad::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UiColor {
Grey,
Green,
Pink,
Brown,
Yellow,
Blue,
}
impl UiColor {
pub fn to_bg_color(&self) -> Color {
match self {
UiColor::Grey => Color::from_rgba(140, 140, 140, 200),
UiColor::Green => Color::from_rgba(16, 60, 50, 200),
UiColor::Pink => Color::from_rgba(234, 128, 71, 200),
UiColor::Brown => Color::from_rgba(123, 61, 35, 200),
UiColor::Yellow => Color::from_rgba(242, 230, 190, 200),
UiColor::Blue => Color::from_rgba(47, 85, 172, 200),
}
}
pub fn to_fg_color(&self) -> Color {
match self {
UiColor::Grey => Color::from_rgba(255, 255, 255, 200),
UiColor::Green => Color::from_rgba(255, 255, 255, 200),
UiColor::Pink => Color::from_rgba(255, 255, 255, 200),
UiColor::Brown => Color::from_rgba(255, 255, 255, 200),
UiColor::Yellow => Color::from_rgba(0, 0, 0, 200),
UiColor::Blue => Color::from_rgba(255, 255, 255, 200),
}
}
pub fn to_shadow_color(&self) -> Color {
let bg_color = self.to_bg_color();
Color::from_rgba(
(bg_color.r * 255.) as u8,
(bg_color.g * 255.) as u8,
(bg_color.b * 255.) as u8,
100,
)
}
}
+20
View File
@@ -0,0 +1,20 @@
use macroquad::prelude::*;
pub fn draw_shadow(rect: Rect, shadow_width: f32) {
let shadow_color = Color::new(0., 0., 0., 0.8);
draw_rectangle(
rect.x + rect.w,
rect.y + shadow_width,
shadow_width,
rect.h,
shadow_color,
);
draw_rectangle(
rect.x + shadow_width,
rect.y + rect.h,
rect.w - shadow_width,
shadow_width,
shadow_color,
);
}
+9
View File
@@ -0,0 +1,9 @@
use macroquad::audio::Sound;
pub struct Sounds {
pub click: Sound,
pub win: Sound,
pub loss: Sound,
pub button: Sound,
pub mode: Sound,
}
+24 -30
View File
@@ -5,9 +5,11 @@ use crate::{
solver::Solver,
};
use macroquad::{prelude::rand, time};
pub trait RandomRange {
fn gen_range(&self, min: usize, max: usize) -> usize;
}
pub fn generate(num_pieces: u32, num_solutions: u32) -> GenerateStats {
pub fn generate(num_pieces: u32, num_solutions: u32, rand: &impl RandomRange) -> GenerateStats {
let candidate_pieces = vec![
Piece::Pawn,
Piece::Pawn,
@@ -33,18 +35,13 @@ pub fn generate(num_pieces: u32, num_solutions: u32) -> GenerateStats {
}
let attempts: u32 = 1000;
let mut overall_stats = GenerateStats::new(0, 0, 0, 0., None);
let mut overall_stats = GenerateStats::new(0, 0, 0, None);
for _ in 0..attempts {
let stats = try_generate(num_pieces, num_solutions, candidate_pieces.clone());
let stats = try_generate(num_pieces, num_solutions, rand, candidate_pieces.clone());
overall_stats.piece_total += stats.piece_total;
overall_stats.piece_success += stats.piece_success;
overall_stats.total += stats.total;
overall_stats.total_seconds += stats.total_seconds;
overall_stats.board = stats.board;
println!(
"Generating puzzle.. Elapsed: {}s",
overall_stats.total_seconds,
);
if overall_stats.board.is_some() {
return overall_stats;
}
@@ -57,23 +54,15 @@ pub struct GenerateStats {
piece_total: u32,
piece_success: u32,
total: u32,
total_seconds: f64,
board: Option<Board>,
}
impl GenerateStats {
fn new(
piece_total: u32,
piece_success: u32,
total: u32,
total_millis: f64,
board: Option<Board>,
) -> Self {
fn new(piece_total: u32, piece_success: u32, total: u32, board: Option<Board>) -> Self {
Self {
piece_total,
piece_success,
total,
total_seconds: total_millis,
board,
}
}
@@ -83,7 +72,6 @@ impl GenerateStats {
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_seconds);
println!("{}", stats);
}
@@ -103,28 +91,27 @@ where
fn try_generate(
num_pieces: u32,
num_solutions: u32,
rand: &impl RandomRange,
mut candidate_pieces: Vec<Piece>,
) -> GenerateStats {
let mut board = Board::new();
let mut piece_total = 0;
let mut piece_success = 0;
let now = time::get_time();
for _ in 0..num_pieces {
let mut placed = false;
let empty_squares = board.empty_squares();
let mut attempts = 15;
while !placed {
if attempts == 0 {
let elapsed = time::get_time() - now;
return GenerateStats::new(piece_total, piece_success, 1, elapsed, None);
return GenerateStats::new(piece_total, piece_success, 1, None);
}
attempts -= 1;
piece_total += 1;
let index = rand::gen_range(0, candidate_pieces.len());
let index = rand.gen_range(0, candidate_pieces.len());
let piece = candidate_pieces[index];
let square_index = rand::gen_range(0, empty_squares.len());
let square_index = rand.gen_range(0, empty_squares.len());
let mut random_square = empty_squares[square_index].clone();
random_square.piece = Some(piece);
board.set(random_square.clone());
@@ -142,11 +129,10 @@ fn try_generate(
}
let solutions = Solver::new(board.clone()).solve();
let elapsed = time::get_time() - now;
if solutions.len() > num_solutions as usize {
GenerateStats::new(piece_total, piece_success, 1, elapsed, None)
GenerateStats::new(piece_total, piece_success, 1, None)
} else {
GenerateStats::new(piece_total, piece_success, 1, elapsed, Some(board))
GenerateStats::new(piece_total, piece_success, 1, Some(board))
}
}
@@ -156,11 +142,19 @@ mod tests {
use super::*;
// Figure out a way to remove the macroquad dependencies from this package
// #[test]
use rand::Rng;
struct TestRandom;
impl RandomRange for TestRandom {
fn gen_range(&self, min: usize, max: usize) -> usize {
rand::rng().random_range(min..max)
}
}
#[test]
fn generator_smoke() {
for _ in 0..10 {
let gen_stats = generate(5, 5);
let gen_stats = generate(5, 5, &TestRandom);
let board = gen_stats.board.expect("No puzzle was generated");
assert_eq!(board.game_state, BoardState::InProgress);
+32 -472
View File
@@ -1,34 +1,41 @@
use core::fmt;
use std::fmt::{Display, Formatter};
use game::texture::PieceTexture;
use macroquad::prelude::*;
use sol_chess::{
board::{Board, BoardState},
generator,
};
use game::{sound::Sounds, Game};
use macroquad::{audio, prelude::*};
use miniquad::date;
mod game;
#[macroquad::main("Solitaire Chess")]
fn window_conf() -> Conf {
let window_name = match std::env::var("TESTING") {
Ok(_) => "DEV TESTING MOVE TO WORKSPACE 10",
Err(_) => "Solitaire Chess",
};
Conf {
window_title: window_name.to_string(),
fullscreen: false,
..Default::default()
}
}
#[macroquad::main(window_conf)]
async fn main() {
rand::srand(date::now() as u64);
let background_color = Color::from_rgba(196, 195, 208, 255);
let mut game = init().await;
loop {
clear_background(background_color);
draw_heading("Solitaire Chess");
game.update_window_size();
game.draw();
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);
macro_rules! load_sound {
($file_name:expr) => {
audio::load_sound_from_bytes(include_bytes!($file_name))
.await
.unwrap()
};
}
async fn init() -> Game {
@@ -36,459 +43,12 @@ async fn init() -> Game {
let texture_res = Texture2D::from_file_with_format(&texture_bytes[..], None);
texture_res.set_filter(FilterMode::Nearest);
build_textures_atlas();
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;
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);
let click = load_sound!("../assets/click.wav");
let win = load_sound!("../assets/win.wav");
let loss = load_sound!("../assets/loss.wav");
let button = load_sound!("../assets/button.wav");
let mode = load_sound!("../assets/mode.wav");
let sounds = Sounds { click, win, loss, button, mode };
let game = Game::new(texture_res, sounds);
game
}
struct Game {
original_board: Board,
board: Board,
squares: Vec<GameSquare>,
texture_res: Texture2D,
num_squares: usize,
state: GameState,
debug: bool,
info_square: Rect,
window_height: f32,
window_width: f32,
}
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 {
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,
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),
window_height: screen_height(),
window_width: screen_width(),
}
}
fn update_window_size(&mut self) {
let new_height = screen_height();
let new_width = screen_width();
if new_height == self.window_height && new_width == self.window_width {
return;
}
self.window_height = screen_height();
self.window_width = screen_width();
let square_width = 128.0;
let num_squares = 4;
let x = (self.window_width - (square_width * num_squares as f32)) / 2.0;
let y = (self.window_height - (square_width * num_squares as f32)) / 2.0;
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,
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.squares = rects;
self.info_square = Rect::new(info_x, info_y, info_w, square_width);
}
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;
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,
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,
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_key_released(KeyCode::Q) {
std::process::exit(0);
}
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<String>) {
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,
);
}
}
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),
}
}
}
-26
View File
@@ -1,26 +0,0 @@
*********** 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
-26
View File
@@ -1,26 +0,0 @@
*********** 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
-26
View File
@@ -1,26 +0,0 @@
*********** 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
-26
View File
@@ -1,26 +0,0 @@
*********** 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
-26
View File
@@ -1,26 +0,0 @@
*********** 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
-26
View File
@@ -1,26 +0,0 @@
*********** 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
-26
View File
@@ -1,26 +0,0 @@
*********** 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
-26
View File
@@ -1,26 +0,0 @@
*********** 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
-26
View File
@@ -1,26 +0,0 @@
*********** 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
-26
View File
@@ -1,26 +0,0 @@
*********** 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
-9
View File
@@ -1,9 +0,0 @@
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
+128
View File
@@ -0,0 +1,128 @@
#! /usr/bin/bash
rootd() {
pushd $(git rev-parse --show-toplevel) 2>&1 > /dev/null
}
restored() {
popd 2>&1 > /dev/null
}
get_parameter() {
DEFAULT="${1}"
if [ -z ${2} ]; then
echo "${DEFAULT}"
elif [ ! -d "${2}" ]; then
echo "${DEFAULT}"
else
echo "${2}"
fi
}
# Usage: build_web debug|release target_dir [archive_dir]
# Build profile location to place files location to place compressed archive
build_web() {
rootd
local BINARY_NAME="sol_chess"
local BUILD_PROFILE="debug"
local BUILD_PROFILE_SWITCH=""
if [ -n "${1}" ]; then
if [ "${1}" = "release" ]; then
local BUILD_PROFILE="release"
local BUILD_PROFILE_SWITCH="--release"
fi
fi
local TARGET_DIR=$(get_parameter "./target/dist" ${2})
local ARCHIVE_DIR=$(get_parameter "" ${3})
echo "Build profile: ${BUILD_PROFILE}"
echo "Build profile switch: ${BUILD_PROFILE_SWITCH}"
echo "Target directory: ${TARGET_DIR}"
echo "Archive directory: ${ARCHIVE_DIR}"
set -x
cargo build --target wasm32-unknown-unknown ${BUILD_PROFILE_SWITCH}
set +x
if [ $? -ne 0 ]; then
echo "Wasm build failed"
return 1
fi
rm -rf ${TARGET_DIR} && mkdir -p ${TARGET_DIR} && mv ./target/wasm32-unknown-unknown/${BUILD_PROFILE}/${BINARY_NAME}.wasm ${TARGET_DIR}/${BINARY_NAME}.wasm && cp ./tools/web/index.html ${TARGET_DIR}/index.html
if [ $? -ne 0 ]; then
echo "Failed to assemble the build in ${TARGET_DIR}"
return 1
fi
if [ -n "${ARCHIVE_DIR}" ]; then
local TAR_NAME="${ARCHIVE_DIR}/${BINARY_NAME}.tar.gz"
set -x
tar -czvf ${TAR_NAME} -C ${TARGET_DIR} . && echo "Created ${TAR_NAME}"
set +x
fi
restored
}
run_web() {
rootd
local TARGET_DIR=$(get_parameter "./target/dist" ${1})
echo "Building web app in ${TARGET_DIR}"
build_web "debug" $TARGET_DIR
if [ $? -ne 0 ]; then
echo "Failed to build the web app"
return 1
fi
basic-http-server $TARGET_DIR
restored
}
run_dev() {
rootd
TESTING=1 cargo run
restored
}
deploy() {
rootd
local BINARY_NAME="sol_chess"
if [ $# -ne 1 ]; then
echo "Usage: deploy <serve_root>"
return 1
fi
if [ ! -d $1 ]; then
echo "Directory $1 does not exist"
return 1
fi
local serve_root=$1
build_web "release" "./target/dist" "./target"
if [ $? -ne 0 ]; then
echo "Failed to build the web app"
return 1
fi
sudo mv ./target/${BINARY_NAME}.tar.gz $serve_root/${BINARY_NAME}.tar.gz && \
sudo tar -xzvf $serve_root/${BINARY_NAME}.tar.gz -C $serve_root && \
sudo rm $serve_root/${BINARY_NAME}.tar.gz
echo "Deployment complete"
restored
}
clean() {
rootd
rm -rf ./target
restored
}
+27
View File
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Solitaire Chess</title>
<style>
html,
body,
canvas {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
position: absolute;
background: black;
z-index: 0;
}
</style>
</head>
<body>
<canvas id="glcanvas" tabindex='1'></canvas>
<script src="https://not-fl3.github.io/miniquad-samples/mq_js_bundle.js"></script>
<script>load("sol_chess.wasm");</script> <!-- Your compiled WASM binary -->
</body>
</html>