From 280150b0b0ca54ccf8062f27dffa0fef28af30f1 Mon Sep 17 00:00:00 2001 From: cool-mist Date: Mon, 26 Jan 2026 20:00:50 +0530 Subject: [PATCH] add age --- cli/src/main.rs | 3 +- gui/src/game.rs | 25 ++- gui/src/game/constants.rs | 15 +- gui/src/game/draw.rs | 257 +++++++++++++++++++++---------- gui/src/game/logic.rs | 185 +++++++++++++++------- gui/src/main.rs | 1 + gui/src/resources.rs | 54 ++++--- gui/src/widgets.rs | 11 +- gui/src/widgets/board.rs | 25 +-- gui/src/widgets/button.rs | 15 +- gui/src/widgets/counter.rs | 127 +++++++++++++++ gui/src/widgets/id_text_input.rs | 92 ++++++----- gui/src/widgets/label.rs | 42 +++++ lib/src/board.rs | 100 +++++++----- lib/src/board/cmove.rs | 8 +- lib/src/board/piece.rs | 74 +++++---- lib/src/board/square.rs | 13 +- lib/src/generator.rs | 57 ++++--- 18 files changed, 779 insertions(+), 325 deletions(-) create mode 100644 gui/src/widgets/counter.rs create mode 100644 gui/src/widgets/label.rs diff --git a/cli/src/main.rs b/cli/src/main.rs index 0731106..b94bb0f 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -72,7 +72,8 @@ fn generate_puzzle(num_pieces: Option, num_solutions: Option) -> Optio "Generating a puzzle with {} pieces with a maximum of {} solutions", num_pieces, num_solutions ); - let gen_result = generator::generate_weighted_random(num_pieces, num_solutions, &RandRngImpl); + let gen_result = + generator::generate_weighted_random(num_pieces, num_solutions, num_pieces, &RandRngImpl); gen_result.print_stats(); let Some(puzzle) = gen_result.puzzle() else { diff --git a/gui/src/game.rs b/gui/src/game.rs index 3cf23fc..7ef6a52 100644 --- a/gui/src/game.rs +++ b/gui/src/game.rs @@ -2,6 +2,8 @@ pub mod constants; mod draw; mod logic; +use std::collections::HashMap; + use crate::{resources::Resources, widgets::*}; use macroquad::prelude::*; use sol_lib::generator::Puzzle; @@ -15,30 +17,36 @@ pub struct Game { window_width: f32, puzzle: Puzzle, - id_text: IdTextInput, + id_text_btn: ButtonWidget, reset_btn: ButtonWidget, next_btn: ButtonWidget, + show_rules: bool, + rules_font_size: f32, board: BoardWidget, - heading_rect: Rect, - heading_font_size: f32, heading_text: String, + heading: LabelWidget, - show_rules: bool, - rules_text: String, + rules_btn_text: String, rules_btn: ButtonWidget, game_mode: GameMode, - easy_btn: ButtonWidget, - medium_btn: ButtonWidget, - hard_btn: ButtonWidget, + game_mode_btns: HashMap, + + pieces_label: LabelWidget, + max_age_label: LabelWidget, + pieces_counter: CounterWidget, + max_age_counter: CounterWidget, + generate_btn: ButtonWidget, } #[derive(Default)] pub struct GameSettings { pub volume: f32, + pub max_moves_per_piece: u32, pub debug: bool, + pub num_pieces: u32, } #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] @@ -46,6 +54,7 @@ pub enum GameMode { Easy, Medium, Hard, + Custom, } impl Default for GameMode { diff --git a/gui/src/game/constants.rs b/gui/src/game/constants.rs index 1d38eef..0a180f7 100644 --- a/gui/src/game/constants.rs +++ b/gui/src/game/constants.rs @@ -6,7 +6,7 @@ pub const SCREEN_WIDTH_MIN: f32 = 200.0; pub const SCREEN_WIDTH_MAX: f32 = 10000.0; // How big the square should be relative to the screen size, between 0 and 1 -pub const BOARD_SQUARE_WIDTH_MULTIPLIER: f32 = 0.15; +pub const BOARD_SQUARE_WIDTH_MULTIPLIER: f32 = 0.11; pub const BOARD_SHADOW_MULTIPLIER: f32 = 0.1; @@ -22,19 +22,24 @@ pub const BUTTON_HEIGHT_MULTIPLIER: f32 = 0.08; pub const BUTTON_WIDTH_MULTIPLIER: f32 = 0.20; // Gap between the bottom of the board and the first row of buttons -pub const BOTTOM_BUTTON_ROW_OFFSET_MULTIPLIER: f32 = 0.3; +pub const BOTTOM_ROW_OFFSET_MULTIPLIER: f32 = 0.3; // Gap between the right of the board and the first row of buttons -pub const BOTTOM_RIGHT_ROW_OFFSET_MULTIPLIER: f32 = 0.3; +pub const RIGHT_COLUMN_OFFSET_MULTIPLIER: f32 = 0.3; // Gap between the top of the board and the heading pub const TOP_HEADING_OFFSET_MULTIPLIER: f32 = 0.5; pub const HEADING_TEXT: &str = "Solitaire Chess"; -pub const RESET_BUTTON_TEXT: &str = "R"; -pub const NEXT_BUTTON_TEXT: &str = "N"; +pub const RESET_BUTTON_TEXT: &str = "RESET"; +pub const NEXT_BUTTON_TEXT: &str = "NEXT"; pub const RULES_BUTTON_TEXT: &str = "RULES"; pub const RULES_BUTTON_ALT_TEXT: &str = "CLOSE"; pub const EASY_BUTTON_TEXT: &str = "EASY"; pub const MEDIUM_BUTTON_TEXT: &str = "MEDIUM"; pub const HARD_BUTTON_TEXT: &str = "HARD"; +pub const CUSTOM_BUTTON_TEXT: &str = "CUSTOM"; +pub const GENERATE_BUTTON_TEXT: &str = "GENERATE"; +pub const PIECES_LABEL_TEXT: &str = "Pieces"; +pub const AGE_LABEL_TEXT: &str = " Age"; + diff --git a/gui/src/game/draw.rs b/gui/src/game/draw.rs index 6c82368..9a08fdb 100644 --- a/gui/src/game/draw.rs +++ b/gui/src/game/draw.rs @@ -1,53 +1,66 @@ use super::{Game, constants}; -use crate::widgets::*; +use crate::{game::GameMode, widgets::*}; use macroquad::{math, prelude::*}; impl Game { - fn initialize_drawables(&mut self) { + fn initialize_drawables(&mut self, new_width: f32, new_height: f32) { + self.window_height = new_height; + self.window_width = new_width; + let min_dimension = f32::min(self.window_height, self.window_width); let square_width = constants::BOARD_SQUARE_WIDTH_MULTIPLIER * min_dimension; let num_squares = self.board.num_squares; let board_width = square_width * num_squares as f32; - let board_x = (self.window_width - board_width) / 2.0; + let board_x = (self.window_width * 0.6 - board_width) / 2.0; let board_y = (self.window_height - board_width) / 2.0; let board_rect = Rect::new(board_x, board_y, board_width, board_width); self.board.initialize_drawables(square_width, board_rect); - self.heading_font_size = constants::HEADING_FONT_SIZE_MULTIPLIER * min_dimension; - - let f = self.heading_font_size.floor() as u16; - let dims = measure_text( + let heading_font_size = constants::HEADING_FONT_SIZE_MULTIPLIER * min_dimension; + let f = heading_font_size.floor() as u16; + let heading_text_dims = measure_text( self.heading_text.as_str(), Some(&self.resources.font()), f, 1.0, ); - self.heading_rect = Rect::new( - board_x + (board_width - dims.width) / 2.0, - board_y - dims.height - constants::TOP_HEADING_OFFSET_MULTIPLIER * square_width, - dims.width, - dims.height, + let heading_rect = Rect::new( + (self.window_width - heading_text_dims.width) / 2.0, + board_y + - heading_text_dims.height + - constants::TOP_HEADING_OFFSET_MULTIPLIER * square_width, + heading_text_dims.width, + heading_text_dims.height, ); + self.heading + .initialize_drawables(heading_rect, heading_font_size, &self.resources); // Buttons let btn_h = constants::BUTTON_HEIGHT_MULTIPLIER * min_dimension; - let btn_w = board_width * constants::BUTTON_WIDTH_MULTIPLIER; - let btn_w_sq = btn_h; + let btn_w = constants::BUTTON_WIDTH_MULTIPLIER * board_width; // Bottom row let bottom_row_y = - board_width + board_y + constants::BOTTOM_BUTTON_ROW_OFFSET_MULTIPLIER * square_width; - self.reset_btn.initialize_drawables(Rect::new( - board_x + (square_width - btn_w_sq) / 2., + board_width + board_y + constants::BOTTOM_ROW_OFFSET_MULTIPLIER * square_width; + self.rules_btn.initialize_drawables(Rect::new( + board_x + (square_width - btn_w) / 2., bottom_row_y, - btn_w_sq, + btn_w, btn_h, )); - let btn_next_x_offset = (board_width - square_width) + (square_width - btn_w_sq) / 2.; + + self.reset_btn.initialize_drawables(Rect::new( + board_x + square_width + (square_width - btn_w) / 2., + bottom_row_y, + btn_w, + btn_h, + )); + + let btn_next_x_offset = (board_width - square_width) + (square_width - btn_w) / 2.; self.next_btn.initialize_drawables(Rect::new( board_x + btn_next_x_offset, bottom_row_y, - btn_w_sq, + btn_w, btn_h, )); @@ -57,77 +70,131 @@ impl Game { btn_w * 2., btn_h, ); - self.id_text = IdTextInput::new(id_text_rect); + self.id_text_btn.initialize_drawables(id_text_rect); - // Left column - let left_column_x = - board_x - constants::BOTTOM_RIGHT_ROW_OFFSET_MULTIPLIER * square_width - btn_w; - self.rules_btn.initialize_drawables(Rect::new( - left_column_x, - board_y + board_rect.h - square_width + (square_width - btn_h) / 2., - btn_w, - btn_h, - )); + self.rules_font_size = heading_font_size; // Right column let right_column_x = - board_x + board_width + constants::BOTTOM_RIGHT_ROW_OFFSET_MULTIPLIER * square_width; - self.easy_btn.initialize_drawables(Rect::new( - right_column_x, - board_y + square_width + (square_width - btn_h) / 2., - btn_w, + board_x + board_width + constants::RIGHT_COLUMN_OFFSET_MULTIPLIER * square_width; + self.get_game_mode_button(&GameMode::Easy) + .initialize_drawables(Rect::new( + right_column_x, + board_y + (square_width - btn_h) / 2., + btn_w, + btn_h, + )); + + self.get_game_mode_button(&GameMode::Medium) + .initialize_drawables(Rect::new( + right_column_x, + board_y + square_width + (square_width - btn_h) / 2., + btn_w, + btn_h, + )); + + self.get_game_mode_button(&GameMode::Hard) + .initialize_drawables(Rect::new( + right_column_x, + board_y + 2. * square_width + (square_width - btn_h) / 2., + btn_w, + btn_h, + )); + + self.get_game_mode_button(&GameMode::Custom) + .initialize_drawables(Rect::new( + right_column_x, + board_y + 3. * square_width + (square_width - btn_h) / 2., + btn_w, + btn_h, + )); + + // Right column 2 + let right_column_2_x = + right_column_x + btn_w + constants::RIGHT_COLUMN_OFFSET_MULTIPLIER * square_width; + self.generate_btn.initialize_drawables(Rect::new( + right_column_2_x, + board_y + board_width - square_width + (square_width - btn_h) / 2., + 2. * btn_w, btn_h, )); - self.medium_btn.initialize_drawables(Rect::new( - right_column_x, - board_y + 2. * square_width + (square_width - btn_h) / 2., - btn_w, - btn_h, - )); + self.pieces_label.initialize_drawables( + Rect::new( + right_column_2_x, + board_y + (square_width + btn_h) / 2., + btn_w, + btn_h, + ), + 0.30 * btn_h, + &self.resources, + ); + self.pieces_counter.initialize_drawables( + Rect::new( + right_column_2_x, + board_y + square_width + (2. * square_width - 2. * btn_h) / 2., + btn_w, + 2. * btn_h, + ), + 30., + &self.resources, + ); - self.hard_btn.initialize_drawables(Rect::new( - right_column_x, - board_y + 3. * square_width + (square_width - btn_h) / 2., - btn_w, - btn_h, - )); + // Right column 3 + let right_column_3_x = right_column_2_x + btn_w; + self.max_age_label.initialize_drawables( + Rect::new( + right_column_2_x + btn_w, + board_y + (square_width + btn_h) / 2., + btn_w, + btn_h, + ), + 0.30 * btn_h, + &self.resources, + ); + self.max_age_counter.initialize_drawables( + Rect::new( + right_column_3_x, + board_y + square_width + (2. * square_width - 2. * btn_h) / 2., + btn_w, + 2. * btn_h, + ), + 30., + &self.resources, + ); } pub fn draw(&mut self) { self.update_window_size(); + self.draw_heading(); - self.board.draw( - self.show_rules, - self.heading_font_size, - &self.resources, - &self.settings, - ); - self.id_text.draw(&self.resources, &self.puzzle.board.id); + self.draw_board(); + self.draw_id_text_button(); self.draw_buttons(); + self.draw_setting_controls(); self.draw_debug(); } fn draw_heading(&self) { - let f = self.heading_font_size.floor() as u16; - let dims = measure_text( - self.heading_text.as_str(), - Some(&self.resources.font()), - f, - 1.0, - ); - let draw_text_params = TextParams { - font_size: f, - font: Some(&self.resources.font()), - color: BLACK, - ..Default::default() + self.heading.draw(&self.resources); + } + + fn draw_board(&mut self) { + let params = BoardDrawParams { + show_rules: self.show_rules, + rules_font_size: self.rules_font_size, }; - draw_text_ex( - self.heading_text.as_str(), - self.heading_rect.x, - self.heading_rect.y + dims.offset_y, - draw_text_params, - ); + + self.board.draw(¶ms, &self.resources, &self.settings); + } + + fn get_game_mode_button<'a>(&'a mut self, mode: &GameMode) -> &'a mut ButtonWidget { + self.game_mode_btns.get_mut(mode).unwrap() + } + + fn draw_game_mode_button(&mut self, mode: &GameMode, color: &UiColor, text: &str) { + let btn = self.game_mode_btns.get_mut(mode).unwrap(); + btn.draw(text, color, &self.resources); } fn draw_buttons(&mut self) { @@ -143,26 +210,50 @@ impl Game { &self.resources, ); - self.easy_btn.draw( + self.draw_game_mode_button( + &GameMode::Easy, + &UiColor::Yellow, constants::EASY_BUTTON_TEXT, - &UiColor::Yellow, - &self.resources, ); - self.medium_btn.draw( + self.draw_game_mode_button( + &GameMode::Medium, + &UiColor::Yellow, constants::MEDIUM_BUTTON_TEXT, - &UiColor::Yellow, - &self.resources, ); - self.hard_btn.draw( - constants::HARD_BUTTON_TEXT, + self.draw_game_mode_button( + &GameMode::Hard, &UiColor::Yellow, + constants::HARD_BUTTON_TEXT, + ); + + self.draw_game_mode_button( + &GameMode::Custom, + &UiColor::Blue, + constants::CUSTOM_BUTTON_TEXT, + ); + + self.generate_btn.draw( + constants::GENERATE_BUTTON_TEXT, + &UiColor::Pink, &self.resources, ); self.rules_btn - .draw(&self.rules_text, &UiColor::Brown, &self.resources); + .draw(&self.rules_btn_text, &UiColor::Brown, &self.resources); + } + + fn draw_setting_controls(&mut self) { + self.pieces_label.draw(&self.resources); + self.pieces_counter.draw(&self.resources); + self.max_age_label.draw(&self.resources); + self.max_age_counter.draw(&self.resources); + } + + fn draw_id_text_button(&self) { + // self.id_text_btn + // .draw(&self.puzzle.board.id, &UiColor::Yellow, &self.resources); } fn draw_debug(&self) { @@ -198,8 +289,6 @@ impl Game { return; } - self.window_height = new_height; - self.window_width = new_width; - self.initialize_drawables(); + self.initialize_drawables(new_width, new_height); } } diff --git a/gui/src/game/logic.rs b/gui/src/game/logic.rs index aa36606..ad75cc1 100644 --- a/gui/src/game/logic.rs +++ b/gui/src/game/logic.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use super::{Game, GameMode, GameSettings, constants}; use crate::{ resources::{self, *}, @@ -15,26 +17,61 @@ impl Game { let game_mode = GameMode::Medium; let settings = GameSettings { volume: constants::VOLUME, + max_moves_per_piece: 3, + num_pieces: 2, ..Default::default() }; - let puzzle = Game::generate_puzzle(game_mode); + let puzzle = Game::generate_puzzle(game_mode, &settings); let board = BoardWidget::initialize_state(puzzle.board.size, puzzle.board.clone()); - let medium_btn = ButtonWidget::initialize_state(false); + let mut game_mode_btns = HashMap::new(); + let game_mode = GameMode::Medium; + game_mode_btns.insert(GameMode::Easy, ButtonWidget::initialize_state(true)); + game_mode_btns.insert(GameMode::Medium, ButtonWidget::initialize_state(true)); + game_mode_btns.insert(GameMode::Hard, ButtonWidget::initialize_state(true)); + game_mode_btns.insert(GameMode::Custom, ButtonWidget::initialize_state(true)); + let reset_btn = ButtonWidget::initialize_state(true); let next_btn = ButtonWidget::initialize_state(false); + let rules_btn = ButtonWidget::initialize_state(true); + let generate_btn = ButtonWidget::initialize_state(false); + let id_text_btn = ButtonWidget::initialize_state(true); + let heading = LabelWidget::initialize_state(constants::HEADING_TEXT); + let pieces_label = LabelWidget::initialize_state(constants::PIECES_LABEL_TEXT); + let max_age_label = LabelWidget::initialize_state(constants::AGE_LABEL_TEXT); + let show_rules = false; + let pieces_counter = CounterWidget::initialize_state(2, 7, settings.num_pieces, false); + let moves_counter = CounterWidget::initialize_state(1, 7, settings.max_moves_per_piece, false); + game_mode_btns.get_mut(&game_mode).unwrap().is_active = false; + Self { puzzle, board, resources, - game_mode, settings, - heading_text: constants::HEADING_TEXT.to_string(), - rules_text: constants::RULES_BUTTON_TEXT.to_string(), - medium_btn, + game_mode, + game_mode_btns, + rules_btn_text: constants::RULES_BUTTON_TEXT.to_string(), + reset_btn, next_btn, - ..Self::default() + rules_btn, + id_text_btn, + show_rules, + heading_text: constants::HEADING_TEXT.to_string(), + heading, + pieces_counter, + max_age_counter: moves_counter, + generate_btn, + pieces_label, + max_age_label, + ..Default::default() } } + pub fn handle_input(&mut self) { + let (mx, my) = mouse_position(); + let winput = WidgetInput { + mouse_pos: Circle::new(mx, my, 0.0), + }; + // CORE GAMEPLAY let (mouse_x, mouse_y) = mouse_position(); let mouse_pos = Circle::new(mouse_x, mouse_y, 0.0); @@ -56,6 +93,11 @@ impl Game { return; } + self.id_text_btn.handle_input( + &self.resources.sound(&SoundKind::Click), + self.settings.volume, + ); + if self .reset_btn .handle_input( @@ -81,50 +123,41 @@ impl Game { } // PRESETS - let mut clicked_game_mode_btn: Option<&ButtonWidget> = None; - if self - .easy_btn - .handle_input( - &self.resources.sound(&SoundKind::Mode), - self.settings.volume, - ) - .is_clicked - { - self.game_mode = GameMode::Easy; - self.easy_btn.is_active = false; - self.medium_btn.is_active = true; - self.hard_btn.is_active = true; - clicked_game_mode_btn = Some(&self.easy_btn); - } else if self - .medium_btn - .handle_input( - &self.resources.sound(&SoundKind::Mode), - self.settings.volume, - ) - .is_clicked - { - self.game_mode = GameMode::Medium; - self.easy_btn.is_active = true; - self.medium_btn.is_active = false; - self.hard_btn.is_active = true; - clicked_game_mode_btn = Some(&self.medium_btn); - } else if self - .hard_btn - .handle_input( - &self.resources.sound(&SoundKind::Mode), - self.settings.volume, - ) - .is_clicked - { - self.game_mode = GameMode::Hard; - self.easy_btn.is_active = true; - self.medium_btn.is_active = true; - self.hard_btn.is_active = false; - clicked_game_mode_btn = Some(&self.hard_btn); + let mut game_mode_changed = false; + for (mode, btn) in &mut self.game_mode_btns { + if btn + .handle_input( + &self.resources.sound(&SoundKind::Mode), + self.settings.volume, + ) + .is_clicked + { + self.game_mode = *mode; + game_mode_changed = true; + break; + } } - if let Some(_) = clicked_game_mode_btn { - self.next_puzzle(); + if game_mode_changed { + for (mode, btn) in &mut self.game_mode_btns { + if *mode == self.game_mode { + btn.is_active = false; + } else { + btn.is_active = true; + } + } + + if self.game_mode != GameMode::Custom { + self.generate_btn.is_active = false; + self.pieces_counter.set_active(false); + self.max_age_counter.set_active(false); + self.next_puzzle(); + } else { + self.generate_btn.is_active = true; + self.pieces_counter.set_active(true); + self.max_age_counter.set_active(true); + } + return; } @@ -137,7 +170,7 @@ impl Game { .is_clicked { self.show_rules = !self.show_rules; - self.rules_text = match self.show_rules { + self.rules_btn_text = match self.show_rules { true => constants::RULES_BUTTON_ALT_TEXT.to_string(), false => constants::RULES_BUTTON_TEXT.to_string(), }; @@ -145,6 +178,36 @@ impl Game { return; } + let pieces_counter_interaction = + self.pieces_counter + .handle_input(&winput, &self.resources, &self.settings); + if pieces_counter_interaction.interacted { + self.settings.num_pieces = pieces_counter_interaction.current; + return; + } + + let moves_counter_interaction = + self.max_age_counter + .handle_input(&winput, &self.resources, &self.settings); + if moves_counter_interaction.interacted { + self.settings.max_moves_per_piece = moves_counter_interaction.current; + return; + } + + if self + .generate_btn + .handle_input( + &self.resources.sound(&SoundKind::Button), + self.settings.volume, + ) + .is_clicked + { + if self.game_mode == GameMode::Custom { + self.next_puzzle(); + } + return; + } + // KEY INPUTS if is_key_released(KeyCode::Escape) { if self.show_rules { @@ -153,7 +216,7 @@ impl Game { self.settings.volume, ); self.show_rules = false; - self.rules_text = constants::RULES_BUTTON_TEXT.to_string(); + self.rules_btn_text = constants::RULES_BUTTON_TEXT.to_string(); } return; @@ -175,7 +238,7 @@ impl Game { fn reset_game(&mut self, options: ResetOptions) { if options.create_new_puzzle { - self.puzzle = Game::generate_puzzle(self.game_mode); + self.puzzle = Game::generate_puzzle(self.game_mode, &self.settings); } self.next_btn.is_active = false; @@ -189,15 +252,27 @@ impl Game { }); } - fn generate_puzzle(mode: GameMode) -> Puzzle { + fn generate_puzzle(mode: GameMode, settings: &GameSettings) -> Puzzle { let piece_count = match mode { GameMode::Easy => 3, GameMode::Medium => 5, GameMode::Hard => 7, + GameMode::Custom => settings.num_pieces, }; - let generated = - generator::generate_weighted_random(piece_count, 100, &MacroquadRandAdapter); + let max_moves_per_piece = match mode { + GameMode::Easy => 2, + GameMode::Medium => 2, + GameMode::Hard => 2, + GameMode::Custom => settings.max_moves_per_piece, + }; + + let generated = generator::generate_weighted_random( + piece_count, + 100, + max_moves_per_piece, + &MacroquadRandAdapter, + ); let puzzle = generated.puzzle(); puzzle.expect("No puzzle was generated") } diff --git a/gui/src/main.rs b/gui/src/main.rs index 50ba779..a0bedac 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -36,3 +36,4 @@ async fn main() { next_frame().await } } + diff --git a/gui/src/resources.rs b/gui/src/resources.rs index b54677b..cdf9615 100644 --- a/gui/src/resources.rs +++ b/gui/src/resources.rs @@ -2,13 +2,14 @@ use macroquad::{ audio::{self, Sound}, prelude::*, }; -use sol_lib::board::piece::Piece; +use sol_lib::board::piece::{Piece, PieceKind}; use std::collections::HashMap; #[derive(Default)] pub struct Resources { texture_pieces: Option, - texture_rects_pieces: HashMap, + texture_rects_pieces: HashMap, + texture_rects_inactive_pieces: HashMap, sounds: HashMap, @@ -52,32 +53,43 @@ pub async fn init() -> Resources { texture_pieces.set_filter(FilterMode::Nearest); build_textures_atlas(); - let mut texture_rects_pieces = HashMap::new(); - texture_rects_pieces.insert(Piece::Pawn, piece_texture_rect(Piece::Pawn)); - texture_rects_pieces.insert(Piece::Knight, piece_texture_rect(Piece::Knight)); - texture_rects_pieces.insert(Piece::Bishop, piece_texture_rect(Piece::Bishop)); - texture_rects_pieces.insert(Piece::Rook, piece_texture_rect(Piece::Rook)); - texture_rects_pieces.insert(Piece::Queen, piece_texture_rect(Piece::Queen)); - texture_rects_pieces.insert(Piece::King, piece_texture_rect(Piece::King)); + let texture_rects_pieces = generate_texture_rects_pieces(true); + let texture_rects_inactive_pieces = generate_texture_rects_pieces(false); Resources { texture_pieces: Some(texture_pieces), texture_rects_pieces, + texture_rects_inactive_pieces, sounds, font: Some(font), } } -fn piece_texture_rect(piece: Piece) -> Rect { +fn generate_texture_rects_pieces(active: bool) -> HashMap { + let mut texture_rects_pieces = HashMap::new(); + texture_rects_pieces.insert(PieceKind::Pawn, piece_texture_rect(PieceKind::Pawn, active)); + texture_rects_pieces.insert(PieceKind::Knight, piece_texture_rect(PieceKind::Knight, active)); + texture_rects_pieces.insert(PieceKind::Bishop, piece_texture_rect(PieceKind::Bishop, active)); + texture_rects_pieces.insert(PieceKind::Rook, piece_texture_rect(PieceKind::Rook, active)); + texture_rects_pieces.insert(PieceKind::Queen, piece_texture_rect(PieceKind::Queen, active)); + texture_rects_pieces.insert(PieceKind::King, piece_texture_rect(PieceKind::King, active)); + + texture_rects_pieces +} + +fn piece_texture_rect(piece: PieceKind, active: bool) -> Rect { let index = match piece { - Piece::Pawn => 0, - Piece::Knight => 1, - Piece::Bishop => 2, - Piece::Rook => 3, - Piece::Queen => 4, - Piece::King => 5, + PieceKind::Pawn => 0, + PieceKind::Knight => 1, + PieceKind::Bishop => 2, + PieceKind::Rook => 3, + PieceKind::Queen => 4, + PieceKind::King => 5, + }; + let color = match active { + true => 1, + false => 0, }; - let color = 0; Rect::new(index as f32 * 128.0, color as f32 * 128.0, 128.0, 128.0) } @@ -93,7 +105,13 @@ pub enum SoundKind { impl Resources { pub fn get_piece_texture<'a>(&'a self, piece: &Piece) -> Texture<'a> { let texture = self.texture_pieces.as_ref().unwrap(); - let texture_rect = self.texture_rects_pieces.get(piece).unwrap(); + let texture_rects_lookup = if piece.active { + &self.texture_rects_pieces + } else { + &self.texture_rects_inactive_pieces + }; + + let texture_rect = texture_rects_lookup.get(&piece.kind).unwrap(); Texture { texture: texture, texture_rect: *texture_rect, diff --git a/gui/src/widgets.rs b/gui/src/widgets.rs index ae45651..3fa3f6e 100644 --- a/gui/src/widgets.rs +++ b/gui/src/widgets.rs @@ -5,7 +5,12 @@ mod button; pub use button::*; mod id_text_input; -pub use id_text_input::*; + +mod label; +pub use label::*; + +mod counter; +pub use counter::*; use macroquad::{ audio::{self, Sound}, @@ -13,6 +18,10 @@ use macroquad::{ }; use quad_snd::PlaySoundParams; +pub struct WidgetInput { + pub mouse_pos: Circle, +} + #[allow(unused)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UiColor { diff --git a/gui/src/widgets/board.rs b/gui/src/widgets/board.rs index 1a5cd72..26cc99e 100644 --- a/gui/src/widgets/board.rs +++ b/gui/src/widgets/board.rs @@ -28,10 +28,6 @@ pub struct BoardInteraction<'a> { } impl BoardWidget { - fn get(&mut self, i: usize, j: usize) -> &mut GameSquare { - &mut self.squares[i * self.num_squares + j] - } - pub fn initialize_state(num_squares: usize, current_board: Board) -> BoardWidget { let mut board = BoardWidget::default(); board.num_squares = num_squares; @@ -71,11 +67,11 @@ impl BoardWidget { self.squares = rects; } - pub fn draw(&self, show_rules: bool, rules_font_size: f32, resources: &Resources, settings: &GameSettings) { + pub fn draw(&self, params: &BoardDrawParams, resources: &Resources, settings: &GameSettings) { let board_shadow_width = constants::BOARD_SHADOW_MULTIPLIER * self.square_width; draw_shadow(&self.board_rect, board_shadow_width); - if show_rules { + if params.show_rules { draw_rectangle( self.board_rect.x, self.board_rect.y, @@ -84,11 +80,13 @@ impl BoardWidget { UiColor::Yellow.to_bg_color(), ); - let font_size = rules_font_size * 0.6; + let font_size = params.rules_font_size * 0.4; let rules = "\ Every move should be a \n\ capture. Win when only \n\ - one piece is left.\n"; + one piece is left.\n\ + Age: Each piece can only \n\ + move 'age' times"; let measurement = measure_text(rules, Some(resources.font()), font_size as u16, 1.0); let draw_text_params = TextParams { font_size: font_size as u16, @@ -99,7 +97,7 @@ impl BoardWidget { draw_multiline_text_ex( rules, self.board_rect.x + 0.5 * self.square_width, - self.board_rect.y + 0.5 * (self.board_rect.h - measurement.height) + self.board_rect.y + 0.4 * (self.board_rect.h - measurement.height) - 2. * measurement.offset_y, Some(2.), draw_text_params, @@ -161,6 +159,10 @@ impl BoardWidget { } } + fn get(&mut self, i: usize, j: usize) -> &mut GameSquare { + &mut self.squares[i * self.num_squares + j] + } + fn draw_debug(&self) { let mut debug_lines = vec![]; let (mx, my) = mouse_position(); @@ -420,3 +422,8 @@ struct PieceDrawTextureParams<'a> { texture: &'a Texture2D, draw_text_params: DrawTextureParams, } + +pub struct BoardDrawParams { + pub show_rules: bool, + pub rules_font_size: f32, +} diff --git a/gui/src/widgets/button.rs b/gui/src/widgets/button.rs index 481a4a5..584a352 100644 --- a/gui/src/widgets/button.rs +++ b/gui/src/widgets/button.rs @@ -66,8 +66,19 @@ impl ButtonWidget { }; let font = resources.font(); - let font_size = (0.2 * self.rect.w) as u16; - let dims = measure_text(&text, Some(&font), font_size, 1.0); + let mut dim_width = self.rect.w; + let mut font_size = 30.0; + let mut dims = measure_text(&text, Some(&font), font_size as u16, 1.0); + loop { + if dim_width <= self.rect.w * 0.8 { + break; + } + + dims = measure_text(&text, Some(&font), font_size as u16, 1.0); + dim_width = dims.width; + font_size -= 2.0; + } + let button_draw_offset = self.get_button_draw_offset(); let text_params = TextParams { diff --git a/gui/src/widgets/counter.rs b/gui/src/widgets/counter.rs new file mode 100644 index 0000000..6a0819e --- /dev/null +++ b/gui/src/widgets/counter.rs @@ -0,0 +1,127 @@ +use crate::{ + game::GameSettings, + resources::{Resources, SoundKind}, + widgets::*, +}; +use macroquad::prelude::*; + +#[derive(Default)] +pub struct CounterWidget { + visual_rect: Rect, + increment: ButtonWidget, + decrement: ButtonWidget, + min: u32, + max: u32, + current: u32, + font_size: f32, + visual_dims: TextDimensions, + increment_dims: TextDimensions, + decrement_dims: TextDimensions, + is_active: bool, +} + +pub struct CounterWidgetInteraction { + pub current: u32, + pub interacted: bool, +} + +impl CounterWidget { + pub fn initialize_state(min: u32, max: u32, current: u32, is_active: bool) -> Self { + Self { + min, + max, + current, + is_active, + ..Default::default() + } + } + + pub fn set_active(&mut self, is_active: bool) { + self.is_active = is_active; + self.increment.is_active = is_active; + self.decrement.is_active = is_active; + } + + pub fn initialize_drawables( + &mut self, + total_area: Rect, + font_size: f32, + resources: &Resources, + ) { + let margin_y = 0.05 * total_area.h; + let btn_h = total_area.h * 0.3; + let btn_w = total_area.w * 0.9; + let btn_x = total_area.x + (total_area.w - btn_w) / 2.; + let increment_y = total_area.y + margin_y; + let visual_y = increment_y + btn_h + margin_y; + let decrement_y = visual_y + btn_h + margin_y; + + self.visual_rect = Rect::new(btn_x, visual_y, btn_w, btn_h); + + let increment_rect = Rect::new(btn_x, increment_y, btn_w, btn_h); + self.increment = ButtonWidget::initialize_state(self.is_active); + self.increment.initialize_drawables(increment_rect); + + let decrement_rect = Rect::new(btn_x, decrement_y, btn_w, btn_h); + self.decrement = ButtonWidget::initialize_state(self.is_active); + self.decrement.initialize_drawables(decrement_rect); + + self.increment_dims = measure_text("+", Some(resources.font()), font_size as u16, 1.0); + self.decrement_dims = measure_text("-", Some(resources.font()), font_size as u16, 1.0); + + self.visual_dims = measure_text("0", Some(resources.font()), font_size as u16, 1.0); + self.font_size = font_size; + } + + pub fn handle_input( + &mut self, + _: &WidgetInput, + resources: &Resources, + settings: &GameSettings, + ) -> CounterWidgetInteraction { + let mut interacted = false; + if self + .increment + .handle_input(resources.sound(&SoundKind::Click), settings.volume) + .is_clicked + { + if self.current < self.max { + self.current += 1; + interacted = true; + } + } + + if self + .decrement + .handle_input(resources.sound(&SoundKind::Click), settings.volume) + .is_clicked + { + if self.current > self.min { + self.current -= 1; + interacted = true; + } + } + + return CounterWidgetInteraction { + current: self.current, + interacted, + }; + } + + pub fn draw(&mut self, resources: &Resources) { + self.increment.draw("+", &UiColor::Yellow, resources); + self.decrement.draw("-", &UiColor::Yellow, resources); + + draw_text_ex( + &self.current.to_string(), + self.visual_rect.x + (self.visual_rect.w - self.visual_dims.width) / 2., + self.visual_rect.y + (self.visual_rect.h + self.font_size) / 2., + TextParams { + font: Some(resources.font()), + font_size: self.font_size as u16, + color: UiColor::Yellow.to_fg_color(), + ..Default::default() + }, + ); + } +} diff --git a/gui/src/widgets/id_text_input.rs b/gui/src/widgets/id_text_input.rs index 9946718..1b06818 100644 --- a/gui/src/widgets/id_text_input.rs +++ b/gui/src/widgets/id_text_input.rs @@ -1,77 +1,73 @@ +#[allow(dead_code)] + use crate::{resources::Resources, widgets::*}; use macroquad::prelude::*; +#[derive(Default)] pub struct IdTextInput { rect: Rect, text_rect: Rect, copy_box_rect: Rect, paste_box_rect: Rect, - shadow_width: f32, - color: UiColor, - copy_color: UiColor, - paste_color: UiColor, +} + +pub struct IdTextInputDrawParams<'a> { + pub text: &'a str, + pub color: UiColor, + pub copy_color: UiColor, + pub paste_color: UiColor, + pub shadow_width: f32, } impl IdTextInput { - pub fn new(rect: Rect) -> Self { - let copy_button_w = 0.25 * rect.w; - let paste_button_h = 0.5 * rect.h; - Self { - rect, - text_rect: Rect::new(rect.x, rect.y, rect.w - copy_button_w, rect.h), - copy_box_rect: Rect::new( - rect.x + (rect.w - copy_button_w), - rect.y, - copy_button_w, - paste_button_h, - ), - paste_box_rect: Rect::new( - rect.x + (rect.w - copy_button_w), - rect.y + paste_button_h, - copy_button_w, - paste_button_h, - ), - shadow_width: 3.0, - color: UiColor::Yellow, - copy_color: UiColor::Blue, - paste_color: UiColor::Pink, - } + pub fn initialize_state() -> Self { + Default::default() } - pub fn draw(&self, resources: &Resources, text: &str) { - draw_rect(&self.text_rect, self.color); - draw_rect(&self.copy_box_rect, self.copy_color); - draw_rect(&self.paste_box_rect, self.paste_color); - draw_shadow(&self.rect, self.shadow_width); + pub fn initialize_drawables(&mut self, rect: Rect) { + let copy_button_w = 0.25 * rect.w; + let paste_button_h = 0.5 * rect.h; + self.rect = rect; + self.text_rect = Rect::new(rect.x, rect.y, rect.w - copy_button_w, rect.h); + self.copy_box_rect = Rect::new( + rect.x + (rect.w - copy_button_w), + rect.y, + copy_button_w, + paste_button_h, + ); + self.paste_box_rect = Rect::new( + rect.x + (rect.w - copy_button_w), + rect.y + paste_button_h, + copy_button_w, + paste_button_h, + ); + } + + pub fn draw(&self, params: &IdTextInputDrawParams, resources: &Resources) { + draw_rect(&self.text_rect, params.color); + draw_rect(&self.copy_box_rect, params.copy_color); + draw_rect(&self.paste_box_rect, params.paste_color); + draw_shadow(&self.rect, params.shadow_width); let font = resources.font(); let font_size = (0.15 * self.text_rect.w) as u16; - let text_dims = measure_text(text, Some(font), font_size, 1.0); + let text_dims = measure_text(params.text, Some(font), font_size, 1.0); draw_text_ex( - &text, + params.text, self.rect.x + (self.text_rect.w - text_dims.width) / 2., self.rect.y + (self.text_rect.h - text_dims.height) / 2. + text_dims.offset_y, TextParams { font: Some(font), font_size: font_size, - color: self.color.to_fg_color(), + color: params.color.to_fg_color(), ..Default::default() }, ); } -} -impl Default for IdTextInput { - fn default() -> Self { - Self { - rect: Default::default(), - text_rect: Default::default(), - copy_box_rect: Default::default(), - paste_box_rect: Default::default(), - shadow_width: Default::default(), - color: UiColor::Grey, - copy_color: UiColor::Grey, - paste_color: UiColor::Grey, - } + pub fn handle_input(&self, _: &WidgetInput) -> IdTextInputInteraction { + IdTextInputInteraction {} } } + +pub struct IdTextInputInteraction {} diff --git a/gui/src/widgets/label.rs b/gui/src/widgets/label.rs new file mode 100644 index 0000000..428e7cf --- /dev/null +++ b/gui/src/widgets/label.rs @@ -0,0 +1,42 @@ +use macroquad::prelude::*; + +use crate::resources::Resources; + +#[derive(Default)] +pub struct LabelWidget { + rect: Rect, + font_size: f32, + text_dimensions: TextDimensions, + text: String, +} + +impl LabelWidget { + pub fn initialize_state(text: &str) -> Self { + Self { + text: text.to_string(), + ..Default::default() + } + } + + pub fn initialize_drawables(&mut self, rect: Rect, font_size: f32, resources: &Resources) { + self.rect = rect; + self.font_size = font_size; + self.text_dimensions = measure_text(&self.text, Some(resources.font()), font_size as u16, 1.0); + } + + pub fn draw(&self, resources: &Resources) { + let draw_text_params = TextParams { + font: Some(&resources.font()), + font_size: self.font_size as u16, + color: BLACK, + ..Default::default() + }; + + draw_text_ex( + &self.text, + self.rect.x, + self.rect.y + self.text_dimensions.offset_y, + draw_text_params, + ); + } +} diff --git a/lib/src/board.rs b/lib/src/board.rs index 73f6ce4..78146e4 100644 --- a/lib/src/board.rs +++ b/lib/src/board.rs @@ -14,10 +14,10 @@ use std::{ use cmove::CMove; use constants::BOARD_SIZE; use errors::SError; -use piece::Piece; +use piece::PieceKind; use square::{Square, SquarePair}; -use crate::generator::Puzzle; +use crate::{board::piece::Piece, generator::Puzzle}; #[derive(Clone, Default)] pub struct Board { @@ -27,6 +27,7 @@ pub struct Board { pub id: String, pub size: usize, pieces_remaining: u8, + max_moves_per_piece: u32, } #[derive(PartialEq, Eq, Debug, Clone)] @@ -43,8 +44,19 @@ impl Default for BoardState { } } +pub struct BoardOptions { + pub max_moves_per_piece: u32, +} + impl Board { pub fn new() -> Self { + let options = BoardOptions { + max_moves_per_piece: 2, + }; + Board::create(options) + } + + pub fn create(options: BoardOptions) -> Self { let cells = [[None; BOARD_SIZE]; BOARD_SIZE]; let id = Board::encode(cells); Board { @@ -54,6 +66,7 @@ impl Board { pieces_remaining: 0, game_state: BoardState::NotStarted, size: BOARD_SIZE, + max_moves_per_piece: options.max_moves_per_piece, } } @@ -72,10 +85,10 @@ impl Board { let mask = 0b111; for i in (0..BOARD_SIZE).rev() { for j in (0..BOARD_SIZE).rev() { - let piece = Board::get_piece_from_encoding((working & mask) as u8); + let piece_kind = Board::get_piece_from_encoding((working & mask) as u8); working = working >> 3; - let piece = piece?; - board.set(Square::new(i, j, piece)); + let piece_kind = piece_kind?; + board.set(Square::new(i, j, Piece::from_kind(piece_kind))); } } Ok(board) @@ -91,18 +104,18 @@ impl Board { for r in 0..BOARD_SIZE { for f in 0..BOARD_SIZE { let c = chars.next().unwrap(); - let piece = match c { - 'K' | 'k' => Piece::King, - 'Q' | 'q' => Piece::Queen, - 'B' | 'b' => Piece::Bishop, - 'N' | 'n' => Piece::Knight, - 'R' | 'r' => Piece::Rook, - 'P' | 'p' => Piece::Pawn, + let piece_kind = match c { + 'K' | 'k' => PieceKind::King, + 'Q' | 'q' => PieceKind::Queen, + 'B' | 'b' => PieceKind::Bishop, + 'N' | 'n' => PieceKind::Knight, + 'R' | 'r' => PieceKind::Rook, + 'P' | 'p' => PieceKind::Pawn, '.' => continue, _ => return Err(SError::InvalidBoard), }; - let square = Square::new(f, r, Some(piece)); + let square = Square::new(f, r, Some(Piece::new(piece_kind))); board.set(square); } } @@ -137,7 +150,14 @@ impl Board { return None; } - let from_piece = mem::replace(&mut self.cells[mv.from.file][mv.from.rank], None); + let mut from_piece = mem::replace(&mut self.cells[mv.from.file][mv.from.rank], None); + if let Some(p) = &mut from_piece { + p.moves_made += 1; + if p.moves_made >= self.max_moves_per_piece { + p.active = false; + } + } + self.cells[mv.to.file][mv.to.rank] = from_piece; self.pieces_remaining -= 1; @@ -278,13 +298,17 @@ impl Board { return None; }; - let legal = match piece { - Piece::King => self.is_king_legal(&pair), - Piece::Queen => self.is_queen_legal(&pair), - Piece::Bishop => self.is_bishop_legal(&pair), - Piece::Knight => self.is_knight_legal(&pair), - Piece::Rook => self.is_rook_legal(&pair), - Piece::Pawn => self.is_pawn_legal(&pair), + if piece.moves_made >= self.max_moves_per_piece { + return None; + } + + let legal = match piece.kind { + PieceKind::King => self.is_king_legal(&pair), + PieceKind::Queen => self.is_queen_legal(&pair), + PieceKind::Bishop => self.is_bishop_legal(&pair), + PieceKind::Knight => self.is_knight_legal(&pair), + PieceKind::Rook => self.is_rook_legal(&pair), + PieceKind::Pawn => self.is_pawn_legal(&pair), }; if legal { @@ -400,26 +424,26 @@ impl Board { fn get_piece_encoding(piece: Option) -> u8 { match piece { - Some(p) => match p { - Piece::King => 0b001, - Piece::Queen => 0b010, - Piece::Rook => 0b011, - Piece::Bishop => 0b100, - Piece::Knight => 0b101, - Piece::Pawn => 0b110, + Some(p) => match p.kind { + PieceKind::King => 0b001, + PieceKind::Queen => 0b010, + PieceKind::Rook => 0b011, + PieceKind::Bishop => 0b100, + PieceKind::Knight => 0b101, + PieceKind::Pawn => 0b110, }, None => 0b000, } } - fn get_piece_from_encoding(encoding: u8) -> Result, SError> { + fn get_piece_from_encoding(encoding: u8) -> Result, SError> { match encoding { - 0b001 => Ok(Some(Piece::King)), - 0b010 => Ok(Some(Piece::Queen)), - 0b011 => Ok(Some(Piece::Rook)), - 0b100 => Ok(Some(Piece::Bishop)), - 0b101 => Ok(Some(Piece::Knight)), - 0b110 => Ok(Some(Piece::Pawn)), + 0b001 => Ok(Some(PieceKind::King)), + 0b010 => Ok(Some(PieceKind::Queen)), + 0b011 => Ok(Some(PieceKind::Rook)), + 0b100 => Ok(Some(PieceKind::Bishop)), + 0b101 => Ok(Some(PieceKind::Knight)), + 0b110 => Ok(Some(PieceKind::Pawn)), 0b000 => Ok(None), _ => Err(SError::InvalidBoard), } @@ -433,9 +457,9 @@ impl Board { fn get_square_for_display(piece: &Option, pretty: bool) -> String { let contents = if let Some(piece) = piece { if pretty { - piece.pretty() + piece.kind.pretty() } else { - piece.notation() + piece.kind.notation() } } else { ".".to_string() @@ -557,7 +581,7 @@ mod tests { assert!(board.set(sq!("Nb2")).is_none()); let existing = board.set(sq!("Pc4")); assert!(existing.is_some()); - assert_eq!(Piece::Knight, existing.unwrap()); + assert_eq!(PieceKind::Knight, existing.unwrap().kind); validate_board!(board, "..PP", "..B.", "QN..", "K..R"); } diff --git a/lib/src/board/cmove.rs b/lib/src/board/cmove.rs index aa366e5..e395af2 100644 --- a/lib/src/board/cmove.rs +++ b/lib/src/board/cmove.rs @@ -1,4 +1,6 @@ -use super::{piece::Piece, square::Square}; +use crate::board::piece::Piece; + +use super::{piece::PieceKind, square::Square}; #[derive(PartialEq, Hash, Eq, Clone)] pub struct CMove { @@ -26,8 +28,8 @@ impl CMove { } pub fn notation(&self) -> String { - let piece_qualifier = match &self.from_piece { - Piece::Pawn => self.from.file_notation(), + let piece_qualifier = match &self.from_piece.kind { + PieceKind::Pawn => self.from.file_notation(), p => p.notation(), }; format!( diff --git a/lib/src/board/piece.rs b/lib/src/board/piece.rs index baa395f..f172c41 100644 --- a/lib/src/board/piece.rs +++ b/lib/src/board/piece.rs @@ -1,5 +1,25 @@ #[derive(Clone, Eq, Hash, Copy, Debug, PartialEq)] -pub enum Piece { +pub struct Piece { + pub kind: PieceKind, + pub moves_made: u32, + pub active: bool, +} +impl Piece { + pub fn new(kind: PieceKind) -> Self { + Self { + kind, + moves_made: 0, + active: true, + } + } + + pub fn from_kind(kind: Option) -> Option { + kind.map(|k| Piece::new(k)) + } +} + +#[derive(Clone, Eq, Hash, Copy, Debug, PartialEq)] +pub enum PieceKind { King, Queen, Bishop, @@ -8,15 +28,15 @@ pub enum Piece { Pawn, } -impl Piece { +impl PieceKind { pub fn parse(piece: &str) -> Option { match piece { - "K" => Some(Piece::King), - "Q" => Some(Piece::Queen), - "B" => Some(Piece::Bishop), - "N" => Some(Piece::Knight), - "R" => Some(Piece::Rook), - "P" => Some(Piece::Pawn), + "K" => Some(PieceKind::King), + "Q" => Some(PieceKind::Queen), + "B" => Some(PieceKind::Bishop), + "N" => Some(PieceKind::Knight), + "R" => Some(PieceKind::Rook), + "P" => Some(PieceKind::Pawn), "." => None, p => panic!("Invalid piece {}", p), } @@ -24,12 +44,12 @@ impl Piece { pub fn notation(&self) -> String { let n = match self { - Piece::King => "K", - Piece::Queen => "Q", - Piece::Bishop => "B", - Piece::Knight => "N", - Piece::Rook => "R", - Piece::Pawn => "P", + PieceKind::King => "K", + PieceKind::Queen => "Q", + PieceKind::Bishop => "B", + PieceKind::Knight => "N", + PieceKind::Rook => "R", + PieceKind::Pawn => "P", }; n.to_string() @@ -37,12 +57,12 @@ impl Piece { pub fn pretty(&self) -> String { let n = match self { - Piece::King => "♔", - Piece::Queen => "♕", - Piece::Bishop => "♗", - Piece::Knight => "♘", - Piece::Rook => "♖", - Piece::Pawn => "♙", + PieceKind::King => "♔", + PieceKind::Queen => "♕", + PieceKind::Bishop => "♗", + PieceKind::Knight => "♘", + PieceKind::Rook => "♖", + PieceKind::Pawn => "♙", }; n.to_string() @@ -55,17 +75,17 @@ mod tests { macro_rules! p { ($piece:literal) => { - Piece::parse($piece) + PieceKind::parse($piece) }; } #[test] fn test_piece_parse() { - assert_eq!(p!("K"), Some(Piece::King)); - assert_eq!(p!("Q"), Some(Piece::Queen)); - assert_eq!(p!("B"), Some(Piece::Bishop)); - assert_eq!(p!("N"), Some(Piece::Knight)); - assert_eq!(p!("R"), Some(Piece::Rook)); - assert_eq!(p!("P"), Some(Piece::Pawn)); + assert_eq!(p!("K"), Some(PieceKind::King)); + assert_eq!(p!("Q"), Some(PieceKind::Queen)); + assert_eq!(p!("B"), Some(PieceKind::Bishop)); + assert_eq!(p!("N"), Some(PieceKind::Knight)); + assert_eq!(p!("R"), Some(PieceKind::Rook)); + assert_eq!(p!("P"), Some(PieceKind::Pawn)); } } diff --git a/lib/src/board/square.rs b/lib/src/board/square.rs index 0984645..8d83049 100644 --- a/lib/src/board/square.rs +++ b/lib/src/board/square.rs @@ -1,5 +1,7 @@ +use crate::board::piece::Piece; + use super::constants::BOARD_SIZE; -use super::piece::Piece; +use super::piece::PieceKind; use core::fmt; #[derive(Clone, Eq, Hash, PartialEq)] @@ -29,8 +31,8 @@ impl Square { pub fn parse(notation: &str) -> Self { let mut chars = notation.chars(); - let piece = chars.next().expect("Piece missing"); - let piece = Piece::parse(&piece.to_string()); + let piece_kind = chars.next().expect("Piece missing"); + let piece_kind = PieceKind::parse(&piece_kind.to_string()); let file = chars.next().expect("File missing"); let file = match file { 'a' => 0, @@ -45,6 +47,7 @@ impl Square { panic!("rank should be between 1-{}", BOARD_SIZE); } let rank = BOARD_SIZE - rank; + let piece = piece_kind.map(|kind| Piece::new(kind)); Square::new(file, rank, piece) } @@ -73,7 +76,7 @@ impl Square { if self.piece.is_none() { "".to_string() } else { - self.piece.unwrap().notation() + self.piece.unwrap().kind.notation() } } } @@ -131,7 +134,7 @@ mod tests { let square = Square::parse(¬ation); assert_eq!(square.file, $file); assert_eq!(square.rank, $rank); - assert_eq!(square.piece, Some(Piece::King)); + assert_eq!(square.piece.unwrap().kind, PieceKind::King); assert_eq!(square.notation(), notation); }; } diff --git a/lib/src/generator.rs b/lib/src/generator.rs index c091ecd..0b49da7 100644 --- a/lib/src/generator.rs +++ b/lib/src/generator.rs @@ -1,6 +1,10 @@ use std::fmt::Display; -use crate::board::{Board, cmove::CMove, piece::Piece}; +use crate::board::{ + Board, BoardOptions, + cmove::CMove, + piece::{Piece, PieceKind}, +}; pub trait RandomRange { fn gen_range(&self, min: usize, max: usize) -> usize; @@ -24,23 +28,24 @@ pub struct GenerateStats { pub fn generate_weighted_random( num_pieces: u32, num_solutions: u32, + max_moves_per_piece: u32, rand: &impl RandomRange, ) -> GenerateStats { let candidate_pieces = vec![ - Piece::Pawn, - Piece::Pawn, - Piece::Pawn, - Piece::Pawn, - Piece::Bishop, - Piece::Bishop, - Piece::Bishop, - Piece::Bishop, - Piece::Knight, - Piece::Knight, - Piece::Knight, - Piece::Queen, - Piece::Rook, - Piece::Rook, + PieceKind::Pawn, + PieceKind::Pawn, + PieceKind::Pawn, + PieceKind::Pawn, + PieceKind::Bishop, + PieceKind::Bishop, + PieceKind::Bishop, + PieceKind::Bishop, + PieceKind::Knight, + PieceKind::Knight, + PieceKind::Knight, + PieceKind::Queen, + PieceKind::Rook, + PieceKind::Rook, ]; if num_pieces > candidate_pieces.len().try_into().unwrap() { @@ -53,7 +58,13 @@ pub fn generate_weighted_random( let attempts: u32 = 1000; 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()); + let stats = try_generate( + num_pieces, + num_solutions, + max_moves_per_piece, + rand, + candidate_pieces.clone(), + ); overall_stats.piece_total += stats.piece_total; overall_stats.piece_success += stats.piece_success; overall_stats.total += stats.total; @@ -117,10 +128,14 @@ where fn try_generate( num_pieces: u32, num_solutions: u32, + max_moves_per_piece: u32, rand: &impl RandomRange, - mut candidate_pieces: Vec, + mut candidate_pieces: Vec, ) -> GenerateStats { - let mut board = Board::new(); + let mut board = Board::create(BoardOptions { + max_moves_per_piece, + }); + let mut piece_total = 0; let mut piece_success = 0; for _ in 0..num_pieces { @@ -136,10 +151,10 @@ fn try_generate( piece_total += 1; let index = rand.gen_range(0, candidate_pieces.len()); - let piece = candidate_pieces[index]; + let piece_kind = candidate_pieces[index]; let square_index = rand.gen_range(0, empty_squares.len()); let mut random_square = empty_squares[square_index].clone(); - random_square.piece = Some(piece); + random_square.piece = Some(Piece::new(piece_kind)); board.set(random_square.clone()); let puzzle = board.solve(); if puzzle.solutions.len() > 0 { @@ -186,7 +201,7 @@ mod tests { #[test] fn generator_smoke() { for _ in 0..10 { - let gen_stats = generate_weighted_random(5, 5, &TestRandom); + let gen_stats = generate_weighted_random(5, 5, 10, &TestRandom); let board = gen_stats.board.expect("No puzzle was generated"); assert_eq!(board.game_state, BoardState::InProgress);