diff --git a/gui/assets/caskaydia.ttf b/gui/assets/caskaydia.ttf deleted file mode 100644 index c4b34b5..0000000 Binary files a/gui/assets/caskaydia.ttf and /dev/null differ diff --git a/gui/src/game.rs b/gui/src/game.rs index 8511b0e..2ed57ea 100644 --- a/gui/src/game.rs +++ b/gui/src/game.rs @@ -4,11 +4,7 @@ mod logic; use std::collections::HashMap; -use crate::{ - game, - resources::{self, Resources, SoundKind}, - widgets::*, -}; +use crate::{resources::Resources, widgets::*}; use macroquad::prelude::*; use sol_lib::{board::BoardState, generator::Puzzle}; @@ -16,14 +12,12 @@ use sol_lib::{board::BoardState, generator::Puzzle}; pub struct Game { resources: Resources, - // Game state settings: Settings, puzzle: Puzzle, show_rules: bool, game_mode: GameMode, game_mode_btns: HashMap, - // Render state window_height: f32, window_width: f32, @@ -44,109 +38,6 @@ pub struct Game { btn_generate: WidgetId, } -#[derive(Eq, PartialEq, Copy, Clone, Hash, Default)] -pub struct WidgetId(u64); - -pub enum Widget { - Label(LabelWidget), - Counter(CounterWidget), - Button(ButtonWidget), - Board(BoardWidget), -} - -impl Widget { - fn draw(&mut self, resources: &Resources, settings: &Settings) { - match self { - Widget::Label(label) => label.draw(&resources), - Widget::Counter(counter) => counter.draw(&resources), - Widget::Button(btn) => btn.draw(&resources), - Widget::Board(board) => board.draw(&resources, &settings), - } - } - - fn update( - &mut self, - interaction: &Interaction, - resources: &Resources, - settings: &Settings, - ) -> Command { - match self { - Widget::Label(_) => Command::NoInteraction, - Widget::Counter(c) => handle_counter_input(interaction, c, resources, settings), - Widget::Button(b) => handle_button_input(interaction, b, resources, settings), - Widget::Board(_) => Command::NoInteraction, - } - } -} - -fn handle_counter_input( - interaction: &Interaction, - counter: &mut CounterWidget, - resources: &Resources, - settings: &Settings, -) -> Command { - let counter_interaction = counter.handle_input(interaction, resources, settings); - let Some(interaction_type) = counter_interaction.interaction_type else { - return Command::NoInteraction; - }; - - match interaction_type { - CounterInteractionType::Increment => (counter.on_increment)(counter_interaction), - CounterInteractionType::Decrement => (counter.on_decrement)(counter_interaction), - } -} - -fn handle_button_input( - interaction: &Interaction, - button: &mut ButtonWidget, - resources: &Resources, - settings: &Settings, -) -> Command { - let clicked = button - .handle_input(interaction, resources, settings) - .clicked; - if clicked { - return (button.on_click)(button); - } - - Command::NoInteraction -} - -macro_rules! get_widget_fn { - ($name:ident, $ret:ty, $variant:path) => { - fn $name(widgets: &mut HashMap, id: WidgetId) -> &mut $ret { - match widgets.get_mut(&id) { - Some(w) => match w { - $variant(v) => return v, - _ => panic!("Widget with id {} not found", id.0), - }, - None => panic!("Widget with id {} not found", id.0), - } - } - }; -} - -get_widget_fn!(label, LabelWidget, Widget::Label); -get_widget_fn!(button, ButtonWidget, Widget::Button); -get_widget_fn!(counter, CounterWidget, Widget::Counter); -get_widget_fn!(board, BoardWidget, Widget::Board); - -macro_rules! widget_accessor { - ($name:ident, $fn:ident) => { - macro_rules! $name { - ($this:expr, $widget_id:ident) => { - game::$fn(&mut $this.widgets, $this.$widget_id) - }; - } - pub(crate) use $name; - }; -} - -widget_accessor!(w_button, button); -widget_accessor!(w_label, label); -widget_accessor!(w_counter, counter); -widget_accessor!(w_board, board); - #[derive(Default)] pub struct Settings { pub volume: f32, @@ -169,6 +60,7 @@ impl Default for GameMode { } } +#[derive(Copy, Clone)] pub enum Command { NoInteraction, ResetBoard, @@ -183,169 +75,3 @@ pub enum Command { HideRules, Quit, } - -pub async fn new_game() -> Game { - let resources = resources::init().await; - let game_mode = GameMode::Medium; - let settings = Settings { - volume: constants::VOLUME, - max_age: 3, - max_pieces: 2, - ..Default::default() - }; - let puzzle = logic::generate_new_puzzle(game_mode, &settings); - let game_mode = GameMode::Medium; - let label_heading = LabelWidget::initialize_state(constants::HEADING_TEXT); - let label_pieces = LabelWidget::initialize_state(constants::PIECES_LABEL_TEXT); - let label_max_age = LabelWidget::initialize_state(constants::AGE_LABEL_TEXT); - let show_rules = false; - - let mut widgets = HashMap::new(); - let label_pieces = add_widget(&mut widgets, Widget::Label(label_pieces)); - let label_max_age = add_widget(&mut widgets, Widget::Label(label_max_age)); - let label_heading = add_widget(&mut widgets, Widget::Label(label_heading)); - - let mut w_counter_pieces = - CounterWidget::new(2, 7, settings.max_pieces, false, UiColor::Yellow); - w_counter_pieces.on_increment = |c| Command::UpdateMaxPieceCount(c.current); - w_counter_pieces.on_decrement = |c| Command::UpdateMaxPieceCount(c.current); - let counter_pieces = add_widget(&mut widgets, Widget::Counter(w_counter_pieces)); - - let mut w_counter_moves = CounterWidget::new(1, 7, settings.max_age, false, UiColor::Yellow); - w_counter_moves.on_increment = |c| Command::UpdateMaxAge(c.current); - w_counter_moves.on_decrement = |c| Command::UpdateMaxAge(c.current); - let counter_moves = add_widget(&mut widgets, Widget::Counter(w_counter_moves)); - - let btn_reset = add_widget( - &mut widgets, - Widget::Button(ButtonWidget::new( - true, - UiColor::Yellow, - constants::RESET_BUTTON_TEXT, - SoundKind::Button, - )), - ); - - let btn_next = add_widget( - &mut widgets, - Widget::Button(ButtonWidget::new( - false, - UiColor::Green, - constants::NEXT_BUTTON_TEXT, - SoundKind::Button, - )), - ); - - let btn_rules = add_widget( - &mut widgets, - Widget::Button(ButtonWidget::new( - true, - UiColor::Brown, - constants::RULES_BUTTON_TEXT, - SoundKind::Button, - )), - ); - - let btn_generate = add_widget( - &mut widgets, - Widget::Button(ButtonWidget::new( - false, - UiColor::Pink, - constants::GENERATE_BUTTON_TEXT, - SoundKind::Button, - )), - ); - - let mut game_mode_btns = HashMap::new(); - game_mode_btns.insert( - GameMode::Easy, - add_widget( - &mut widgets, - Widget::Button(ButtonWidget::new( - true, - UiColor::Yellow, - constants::EASY_BUTTON_TEXT, - SoundKind::Mode, - )), - ), - ); - - game_mode_btns.insert( - GameMode::Medium, - add_widget( - &mut widgets, - Widget::Button(ButtonWidget::new( - true, - UiColor::Yellow, - constants::MEDIUM_BUTTON_TEXT, - SoundKind::Mode, - )), - ), - ); - - game_mode_btns.insert( - GameMode::Hard, - add_widget( - &mut widgets, - Widget::Button(ButtonWidget::new( - true, - UiColor::Yellow, - constants::HARD_BUTTON_TEXT, - SoundKind::Mode, - )), - ), - ); - - game_mode_btns.insert( - GameMode::Custom, - add_widget( - &mut widgets, - Widget::Button(ButtonWidget::new( - true, - UiColor::Blue, - constants::CUSTOM_BUTTON_TEXT, - SoundKind::Mode, - )), - ), - ); - - let active_game_mode_btn = game_mode_btns.get(&game_mode).unwrap(); - game::button(&mut widgets, *active_game_mode_btn).is_active = false; - - let board = add_widget( - &mut widgets, - Widget::Board(BoardWidget::initialize_state( - puzzle.board.size, - puzzle.board.clone(), - )), - ); - - Game { - puzzle, - board, - resources, - settings, - game_mode, - game_mode_btns, - btn_reset, - btn_next, - btn_rules, - show_rules, - widgets, - counter_pieces, - counter_moves, - btn_generate, - label_pieces, - label_max_age, - label_heading, - ..Default::default() - } -} - -fn add_widget(widgets: &mut HashMap, w: Widget) -> WidgetId { - let id = rand::rand() as u64; - let widget_id = WidgetId(id); - widgets.insert(widget_id, w); - - widget_id -} diff --git a/gui/src/game/draw.rs b/gui/src/game/draw.rs index 0217276..ca5fc5f 100644 --- a/gui/src/game/draw.rs +++ b/gui/src/game/draw.rs @@ -1,6 +1,6 @@ use super::{Game, constants}; use crate::{ - game::{self, GameMode, w_board, w_button, w_counter, w_label}, + game::{self, GameMode}, widgets::*, }; use macroquad::{math, prelude::*}; @@ -33,92 +33,102 @@ impl Game { heading_text_dims.width, heading_text_dims.height, ); - w_label!(self, label_heading).initialize_drawables( - heading_rect, - heading_font_size, - &self.resources, - ); + w_label!(self, label_heading).place(heading_rect, heading_font_size, &self.resources); let board_rect = Rect::new(board_x, board_y, board_width, board_width); - w_board!(self, board).initialize_drawables( - square_width, - board_rect, - heading_font_size * 0.4, - ); + w_board!(self, board).place(square_width, board_rect, heading_font_size * 0.4); // Buttons let btn_h = constants::BUTTON_HEIGHT_MULTIPLIER * min_dimension; let btn_w = constants::BUTTON_WIDTH_MULTIPLIER * board_width; + let btn_font_size = heading_font_size * 0.3; // Bottom row let bottom_row_y = board_width + board_y + constants::BOTTOM_ROW_OFFSET_MULTIPLIER * square_width; - w_button!(self, btn_rules).place(Rect::new( - board_x + (square_width - btn_w) / 2., - bottom_row_y, - btn_w, - btn_h, - )); + w_button!(self, btn_rules).place( + Rect::new( + board_x + (square_width - btn_w) / 2., + bottom_row_y, + btn_w, + btn_h, + ), + FontSize::Fixed(btn_font_size), + ); - w_button!(self, btn_reset).place(Rect::new( - board_x + square_width + (square_width - btn_w) / 2., - bottom_row_y, - btn_w, - btn_h, - )); + w_button!(self, btn_reset).place( + Rect::new( + board_x + square_width + (square_width - btn_w) / 2., + bottom_row_y, + btn_w, + btn_h, + ), + FontSize::Fixed(btn_font_size), + ); let btn_next_x_offset = (board_width - square_width) + (square_width - btn_w) / 2.; - w_button!(self, btn_next).place(Rect::new( - board_x + btn_next_x_offset, - bottom_row_y, - btn_w, - btn_h, - )); + w_button!(self, btn_next).place( + Rect::new(board_x + btn_next_x_offset, bottom_row_y, btn_w, btn_h), + FontSize::Fixed(btn_font_size), + ); // Right column let right_column_x = board_x + board_width + constants::RIGHT_COLUMN_OFFSET_MULTIPLIER * square_width; - self.get_game_mode_button(&GameMode::Easy).place(Rect::new( - right_column_x, - board_y + (square_width - btn_h) / 2., - btn_w, - btn_h, - )); + self.get_game_mode_button(&GameMode::Easy).place( + Rect::new( + right_column_x, + board_y + (square_width - btn_h) / 2., + btn_w, + btn_h, + ), + FontSize::Fixed(btn_font_size), + ); - self.get_game_mode_button(&GameMode::Medium) - .place(Rect::new( + self.get_game_mode_button(&GameMode::Medium).place( + Rect::new( right_column_x, board_y + square_width + (square_width - btn_h) / 2., btn_w, btn_h, - )); + ), + FontSize::Fixed(btn_font_size), + ); - self.get_game_mode_button(&GameMode::Hard).place(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::Hard).place( + Rect::new( + right_column_x, + board_y + 2. * square_width + (square_width - btn_h) / 2., + btn_w, + btn_h, + ), + FontSize::Fixed(btn_font_size), + ); - self.get_game_mode_button(&GameMode::Custom) - .place(Rect::new( + self.get_game_mode_button(&GameMode::Custom).place( + Rect::new( right_column_x, board_y + 3. * square_width + (square_width - btn_h) / 2., btn_w, btn_h, - )); + ), + FontSize::Fixed(btn_font_size), + ); // Right column 2 let right_column_2_x = right_column_x + btn_w + constants::RIGHT_COLUMN_OFFSET_MULTIPLIER * square_width; - w_button!(self, btn_generate).place(Rect::new( - right_column_2_x, - board_y + board_width - square_width + (square_width - btn_h) / 2., - 2. * btn_w, - btn_h, - )); + w_button!(self, btn_generate).place( + Rect::new( + right_column_2_x, + board_y + board_width - square_width + (square_width - btn_h) / 2., + 2. * btn_w, + btn_h, + ), + FontSize::Fixed(btn_font_size), + ); - w_label!(self, label_pieces).initialize_drawables( + w_label!(self, label_pieces).place( Rect::new( right_column_2_x, board_y + (square_width + btn_h) / 2., @@ -129,20 +139,20 @@ impl Game { &self.resources, ); - w_counter!(self, counter_pieces).initialize_drawables( + w_counter!(self, counter_pieces).place( Rect::new( right_column_2_x, board_y + square_width + (2. * square_width - 2. * btn_h) / 2., btn_w, 2. * btn_h, ), - 30., + 0.50 * btn_h, &self.resources, ); // Right column 3 let right_column_3_x = right_column_2_x + btn_w; - w_label!(self, label_max_age).initialize_drawables( + w_label!(self, label_max_age).place( Rect::new( right_column_2_x + btn_w, board_y + (square_width + btn_h) / 2., @@ -152,14 +162,14 @@ impl Game { 0.30 * btn_h, &self.resources, ); - w_counter!(self, counter_moves).initialize_drawables( + w_counter!(self, counter_moves).place( Rect::new( right_column_3_x, board_y + square_width + (2. * square_width - 2. * btn_h) / 2., btn_w, 2. * btn_h, ), - 30., + 0.50 * btn_h, &self.resources, ); } diff --git a/gui/src/game/logic.rs b/gui/src/game/logic.rs index 40ea8aa..50faad5 100644 --- a/gui/src/game/logic.rs +++ b/gui/src/game/logic.rs @@ -1,7 +1,9 @@ +use std::collections::HashMap; + use super::{Game, GameMode, Settings, constants}; use crate::{ - game::{self, Command, w_board, w_button, w_counter}, - resources::*, + game::{self, Command}, + resources::{self, *}, widgets::*, }; use macroquad::prelude::*; @@ -11,80 +13,175 @@ use sol_lib::{ }; impl Game { + pub async fn new() -> Self { + let resources = resources::init().await; + let game_mode = GameMode::Medium; + let settings = Settings { + volume: constants::VOLUME, + max_age: 3, + max_pieces: 2, + ..Default::default() + }; + let puzzle = generate_new_puzzle(game_mode, &settings); + let game_mode = GameMode::Medium; + let show_rules = false; + + let mut widgets = HashMap::new(); + + let w_label_heading = LabelWidget::new(constants::HEADING_TEXT); + let label_heading = add_widget(&mut widgets, Widget::Label(w_label_heading)); + + let w_label_pieces = LabelWidget::new(constants::PIECES_LABEL_TEXT); + let label_pieces = add_widget(&mut widgets, Widget::Label(w_label_pieces)); + + let w_label_max_age = LabelWidget::new(constants::AGE_LABEL_TEXT); + let label_max_age = add_widget(&mut widgets, Widget::Label(w_label_max_age)); + + let mut w_counter_pieces = + CounterWidget::new(2, 7, settings.max_pieces, false, UiColor::Yellow); + w_counter_pieces.on_increment = |c| Command::UpdateMaxPieceCount(c.current); + w_counter_pieces.on_decrement = |c| Command::UpdateMaxPieceCount(c.current); + let counter_pieces = add_widget(&mut widgets, Widget::Counter(w_counter_pieces)); + + let mut w_counter_moves = + CounterWidget::new(1, 7, settings.max_age, false, UiColor::Yellow); + w_counter_moves.on_increment = |c| Command::UpdateMaxAge(c.current); + w_counter_moves.on_decrement = |c| Command::UpdateMaxAge(c.current); + let counter_moves = add_widget(&mut widgets, Widget::Counter(w_counter_moves)); + + let mut w_btn_reset = ButtonWidget::new( + true, + UiColor::Yellow, + constants::RESET_BUTTON_TEXT, + SoundKind::Button, + ); + w_btn_reset.on_click = |_| Command::ResetBoard; + let btn_reset = add_widget(&mut widgets, Widget::Button(w_btn_reset)); + + let mut w_btn_next = ButtonWidget::new( + false, + UiColor::Green, + constants::NEXT_BUTTON_TEXT, + SoundKind::Button, + ); + w_btn_next.on_click = |_| Command::NextPuzzle; + let btn_next = add_widget(&mut widgets, Widget::Button(w_btn_next)); + + let mut w_btn_rules = ButtonWidget::new( + true, + UiColor::Brown, + constants::RULES_BUTTON_TEXT, + SoundKind::Button, + ); + w_btn_rules.on_click = |_| Command::ToggleRules; + let btn_rules = add_widget(&mut widgets, Widget::Button(w_btn_rules)); + + let mut w_btn_generate = ButtonWidget::new( + false, + UiColor::Pink, + constants::GENERATE_BUTTON_TEXT, + SoundKind::Button, + ); + w_btn_generate.on_click = |_| Command::NewCustomPuzzle; + let btn_generate = add_widget(&mut widgets, Widget::Button(w_btn_generate)); + + let mut game_mode_btns = HashMap::new(); + + let mut w_game_mode_easy_btn = ButtonWidget::new( + true, + UiColor::Yellow, + constants::EASY_BUTTON_TEXT, + SoundKind::Mode, + ); + w_game_mode_easy_btn.on_click = |_| Command::SwitchGameMode(GameMode::Easy); + game_mode_btns.insert( + GameMode::Easy, + add_widget(&mut widgets, Widget::Button(w_game_mode_easy_btn)), + ); + + let mut w_game_mode_medium_btn = ButtonWidget::new( + true, + UiColor::Yellow, + constants::MEDIUM_BUTTON_TEXT, + SoundKind::Mode, + ); + w_game_mode_medium_btn.on_click = |_| Command::SwitchGameMode(GameMode::Medium); + game_mode_btns.insert( + GameMode::Medium, + add_widget(&mut widgets, Widget::Button(w_game_mode_medium_btn)), + ); + + let mut w_game_mode_hard_btn = ButtonWidget::new( + true, + UiColor::Yellow, + constants::HARD_BUTTON_TEXT, + SoundKind::Mode, + ); + w_game_mode_hard_btn.on_click = |_| Command::SwitchGameMode(GameMode::Hard); + game_mode_btns.insert( + GameMode::Hard, + add_widget(&mut widgets, Widget::Button(w_game_mode_hard_btn)), + ); + + let mut w_game_mode_custom_btn = ButtonWidget::new( + true, + UiColor::Blue, + constants::CUSTOM_BUTTON_TEXT, + SoundKind::Mode, + ); + w_game_mode_custom_btn.on_click = |_| Command::SwitchGameMode(GameMode::Custom); + game_mode_btns.insert( + GameMode::Custom, + add_widget(&mut widgets, Widget::Button(w_game_mode_custom_btn)), + ); + + let active_game_mode_btn = game_mode_btns.get(&game_mode).unwrap(); + game::button(&mut widgets, *active_game_mode_btn).is_active = false; + + let mut w_board = BoardWidget::new(puzzle.board.size, puzzle.board.clone()); + w_board.on_board_state_changed = |s| Command::UpdateBoard(s.new_board_state); + let board = add_widget(&mut widgets, Widget::Board(w_board)); + + let mut w_controls = ControlsWidget::new(); + w_controls.add(KeyCode::Q, Command::Quit); + w_controls.add(KeyCode::Escape, Command::HideRules); + w_controls.add(KeyCode::D, Command::ToggleDebug); + add_widget(&mut widgets, Widget::Controls(w_controls)); + + Game { + puzzle, + board, + resources, + settings, + game_mode, + game_mode_btns, + btn_reset, + btn_next, + btn_rules, + show_rules, + widgets, + counter_pieces, + counter_moves, + btn_generate, + label_pieces, + label_max_age, + label_heading, + ..Default::default() + } + } + pub fn update(&mut self, interaction: Interaction) { - let command = self.get_command(interaction); + let command = self.map_to_command(interaction); self.update_state(command); } - fn get_command(&mut self, interaction: Interaction) -> Command { - + fn map_to_command(&mut self, interaction: Interaction) -> Command { for (_, widget) in &mut self.widgets { - let cmd = widget.update(&interaction, &self.resources, &self.settings); - let ret_cmd = match cmd { - Command::NoInteraction => false, - _ => true, - }; - - if ret_cmd { - return cmd; + if let Some(command) = widget.update(&interaction, &self.resources, &self.settings) { + return command; } } - let board_interaction = - w_board!(self, board).handle_input(&interaction, &self.resources, &self.settings); - if let Some(new_board_state) = board_interaction.new_board_state { - return Command::UpdateBoard(new_board_state); - } - - if w_button!(self, btn_reset) - .handle_input(&interaction, &self.resources, &self.settings) - .clicked - { - return Command::ResetBoard; - } - - if w_button!(self, btn_next) - .handle_input(&interaction, &self.resources, &self.settings) - .clicked - { - return Command::NextPuzzle; - } - - for (mode, btn) in &mut self.game_mode_btns { - if game::button(&mut self.widgets, *btn) - .handle_input(&interaction, &self.resources, &self.settings) - .clicked - { - return Command::SwitchGameMode(*mode); - } - } - - if w_button!(self, btn_rules) - .handle_input(&interaction, &self.resources, &self.settings) - .clicked - { - return Command::ToggleRules; - } - - if w_button!(self, btn_generate) - .handle_input(&interaction, &self.resources, &self.settings) - .clicked - { - return Command::NewCustomPuzzle; - } - - if interaction.keys_released.contains(&KeyCode::Escape) { - return Command::HideRules; - } - - if interaction.keys_released.contains(&KeyCode::D) { - return Command::ToggleDebug; - } - - if interaction.keys_released.contains(&KeyCode::Q) { - return Command::Quit; - } - return Command::NoInteraction; } @@ -177,7 +274,15 @@ impl Game { } } -pub fn generate_new_puzzle(mode: GameMode, settings: &Settings) -> Puzzle { +fn add_widget(widgets: &mut HashMap, w: Widget) -> WidgetId { + let id = rand::rand() as u64; + let widget_id = WidgetId(id); + widgets.insert(widget_id, w); + + widget_id +} + +fn generate_new_puzzle(mode: GameMode, settings: &Settings) -> Puzzle { let piece_count = match mode { GameMode::Easy => 3, GameMode::Medium => 5, diff --git a/gui/src/main.rs b/gui/src/main.rs index e6104d1..16f4d9f 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -9,7 +9,7 @@ use miniquad::date; use game::constants; -use crate::widgets::Interaction; +use crate::{game::Game, widgets::Interaction}; fn window_conf() -> Conf { let window_title = { @@ -31,7 +31,7 @@ fn window_conf() -> Conf { async fn main() { rand::srand(date::now() as u64); let background_color = Color::from_rgba(196, 195, 208, 255); - let mut game = game::new_game().await; + let mut game = Game::new().await; loop { clear_background(background_color); diff --git a/gui/src/widgets.rs b/gui/src/widgets.rs index 38bcf0b..d5a8aff 100644 --- a/gui/src/widgets.rs +++ b/gui/src/widgets.rs @@ -1,16 +1,16 @@ +use std::collections::{HashMap, HashSet}; + mod board; -use std::collections::HashSet; +mod button; +mod controls; +mod counter; +mod label; pub use board::*; - -mod button; pub use button::*; - -mod label; -pub use label::*; - -mod counter; +pub use controls::*; pub use counter::*; +pub use label::*; use macroquad::{ audio::{self, Sound}, @@ -18,6 +18,11 @@ use macroquad::{ }; use quad_snd::PlaySoundParams; +use crate::{ + game::{Command, Settings}, + resources::Resources, +}; + pub struct Interaction { pub mouse_pos: Circle, pub keys_released: HashSet, @@ -25,6 +30,127 @@ pub struct Interaction { pub mouse_released: HashSet, } +#[derive(Eq, PartialEq, Copy, Clone, Hash, Default)] +pub struct WidgetId(pub u64); + +pub enum Widget { + Label(LabelWidget), + Counter(CounterWidget), + Button(ButtonWidget), + Board(BoardWidget), + Controls(ControlsWidget), +} + +impl Widget { + pub fn draw(&mut self, resources: &Resources, settings: &Settings) { + match self { + Widget::Label(label) => label.draw(&resources), + Widget::Counter(counter) => counter.draw(&resources), + Widget::Button(btn) => btn.draw(&resources), + Widget::Board(board) => board.draw(&resources, &settings), + Widget::Controls(_) => (), + } + } + + pub fn update( + &mut self, + interaction: &Interaction, + resources: &Resources, + settings: &Settings, + ) -> Option { + match self { + Widget::Label(_) => None, + Widget::Counter(c) => handle_counter_input(interaction, c, resources, settings), + Widget::Button(b) => handle_button_input(interaction, b, resources, settings), + Widget::Board(b) => handle_board_input(interaction, b, resources, settings), + Widget::Controls(c) => c.handle_input(interaction), + } + } +} + +fn handle_board_input( + interaction: &Interaction, + b: &mut BoardWidget, + resources: &Resources, + settings: &Settings, +) -> Option { + let board_interaction = b.handle_input(interaction, resources, settings); + let Some(board_interaction) = board_interaction else { + return None; + }; + + Some((b.on_board_state_changed)(board_interaction)) +} + +fn handle_counter_input( + interaction: &Interaction, + counter: &mut CounterWidget, + resources: &Resources, + settings: &Settings, +) -> Option { + let counter_interaction = counter.handle_input(interaction, resources, settings); + let Some(interaction_type) = counter_interaction.interaction_type else { + return None; + }; + + let command = match interaction_type { + CounterInteractionType::Increment => (counter.on_increment)(counter_interaction), + CounterInteractionType::Decrement => (counter.on_decrement)(counter_interaction), + }; + + Some(command) +} + +fn handle_button_input( + interaction: &Interaction, + button: &mut ButtonWidget, + resources: &Resources, + settings: &Settings, +) -> Option { + let Some(ButtonInteraction { clicked: true }) = + button.handle_input(interaction, resources, settings) + else { + return None; + }; + + return Some((button.on_click)(button)); +} + +macro_rules! get_widget_fn { + ($name:ident, $ret:ty, $variant:path) => { + pub fn $name(widgets: &mut HashMap, id: WidgetId) -> &mut $ret { + match widgets.get_mut(&id) { + Some(w) => match w { + $variant(v) => return v, + _ => panic!("Widget with id {} not found", id.0), + }, + None => panic!("Widget with id {} not found", id.0), + } + } + }; +} + +get_widget_fn!(label, LabelWidget, Widget::Label); +get_widget_fn!(button, ButtonWidget, Widget::Button); +get_widget_fn!(counter, CounterWidget, Widget::Counter); +get_widget_fn!(board, BoardWidget, Widget::Board); + +macro_rules! widget_accessor { + ($name:ident, $fn:ident) => { + macro_rules! $name { + ($this:expr, $widget_id:ident) => { + game::$fn(&mut $this.widgets, $this.$widget_id) + }; + } + pub(crate) use $name; + }; +} + +widget_accessor!(w_button, button); +widget_accessor!(w_label, label); +widget_accessor!(w_counter, counter); +widget_accessor!(w_board, board); + #[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 a21a478..71e10bf 100644 --- a/gui/src/widgets/board.rs +++ b/gui/src/widgets/board.rs @@ -2,7 +2,7 @@ use core::fmt; use std::fmt::{Display, Formatter}; use crate::{ - game::{Settings, constants}, + game::{Command, Settings, constants}, resources::{Resources, SoundKind}, widgets::*, }; @@ -11,12 +11,12 @@ use sol_lib::{ generator::Puzzle, }; -#[derive(Default)] pub struct BoardWidget { - current_board: Board, - square_width: f32, pub num_squares: usize, // this is a constant = 4 for now. pub show_rules: bool, + pub on_board_state_changed: fn(BoardInteraction) -> Command, + current_board: Board, + square_width: f32, rules_font_size: f32, board_rect: Rect, squares: Vec, @@ -24,23 +24,18 @@ pub struct BoardWidget { } pub struct BoardInteraction { - pub new_board_state: Option, + pub new_board_state: BoardState, } impl BoardWidget { - pub fn initialize_state(num_squares: usize, current_board: Board) -> BoardWidget { + pub fn new(num_squares: usize, current_board: Board) -> BoardWidget { let mut board = BoardWidget::default(); board.num_squares = num_squares; board.current_board = current_board; board } - pub fn initialize_drawables( - &mut self, - square_width: f32, - board_rect: Rect, - rules_font_size: f32, - ) { + pub fn place(&mut self, square_width: f32, board_rect: Rect, rules_font_size: f32) { self.square_width = square_width; self.board_rect = board_rect; self.rules_font_size = rules_font_size; @@ -169,49 +164,18 @@ 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(); - 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.current_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); - } - - fn add_debug_info(&self, lines: Vec) { - let mut y = 20.0; - for line in lines { - draw_text(&line, 10.0, y, 20.0, BLACK); - y += 25.0; - } - } - pub fn handle_input( &mut self, interaction: &Interaction, resources: &Resources, settings: &Settings, - ) -> BoardInteraction { + ) -> Option { let mouse_pos = interaction.mouse_pos; if mouse_pos.overlaps_rect(&self.board_rect) == false { - return BoardInteraction { - new_board_state: None, - }; + return None; } + let old_board_state = self.current_board.state; if is_mouse_button_released(MouseButton::Left) { let current_state = self.state.clone(); let new_state = match current_state { @@ -247,10 +211,44 @@ impl BoardWidget { self.state = new_state; } - let board_state = &self.current_board.game_state; - return BoardInteraction { - new_board_state: Some(*board_state), - }; + if old_board_state == self.current_board.state { + return None; + } + + let board_state = self.current_board.state; + return Some(BoardInteraction { + new_board_state: board_state, + }); + } + + 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(); + 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.current_board.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); + } + + fn add_debug_info(&self, lines: Vec) { + let mut y = 20.0; + for line in lines { + draw_text(&line, 10.0, y, 20.0, BLACK); + y += 25.0; + } } pub fn reset(&mut self, puzzle: &Puzzle) { @@ -337,11 +335,11 @@ impl BoardWidget { let m = m.expect("legal move should be found"); self.current_board.make_move(m.clone()); - if self.current_board.game_state == BoardState::Won - || self.current_board.game_state == BoardState::Lost + if self.current_board.state == BoardState::Won + || self.current_board.state == BoardState::Lost { self.reset_squares(); - if self.current_board.game_state == BoardState::Won { + if self.current_board.state == BoardState::Won { play_sound_once(resources.sound(&SoundKind::Win), settings.volume); } else { play_sound_once(resources.sound(&SoundKind::Loss), settings.volume); @@ -431,3 +429,19 @@ struct PieceDrawTextureParams<'a> { texture: &'a Texture2D, draw_text_params: DrawTextureParams, } + +impl Default for BoardWidget { + fn default() -> Self { + Self { + on_board_state_changed: |_| Command::NoInteraction, + num_squares: Default::default(), + show_rules: Default::default(), + current_board: Default::default(), + square_width: Default::default(), + rules_font_size: Default::default(), + board_rect: Default::default(), + squares: Default::default(), + state: Default::default(), + } + } +} diff --git a/gui/src/widgets/button.rs b/gui/src/widgets/button.rs index d351f45..44ca8e5 100644 --- a/gui/src/widgets/button.rs +++ b/gui/src/widgets/button.rs @@ -12,6 +12,7 @@ pub struct ButtonWidget { rect: Rect, shadow_width: f32, sound: SoundKind, + font_size: FontSize, pub on_click: fn(&mut ButtonWidget) -> Command, } @@ -20,15 +21,17 @@ pub struct ButtonInteraction { } impl ButtonInteraction { - fn not_clicked() -> Self { - Self { clicked: false } - } - fn clicked() -> Self { Self { clicked: true } } } +#[derive(Copy, Clone)] +pub enum FontSize { + Fixed(f32), + Fill, +} + impl ButtonWidget { pub fn new(is_active: bool, color: UiColor, text: impl Into, sound: SoundKind) -> Self { Self { @@ -41,8 +44,9 @@ impl ButtonWidget { } } - pub fn place(&mut self, rect: Rect) { + pub fn place(&mut self, rect: Rect, font_size: FontSize) { self.rect = rect; + self.font_size = font_size; } pub fn draw(&self, resources: &Resources) { @@ -86,19 +90,26 @@ impl ButtonWidget { }; let font = resources.font(); - let target_w = self.rect.w * 0.9; - let target_h = self.rect.h; - let mut font_size = 20; - let mut dims = measure_text(&text, Some(&font), font_size, 1.0); - loop { - if dims.width <= target_w && dims.height <= target_h { - break; + let font_size = match self.font_size { + FontSize::Fixed(s) => s as u16, + FontSize::Fill => { + let target_w = self.rect.w * 0.9; + let target_h = self.rect.h; + let mut font_size = 22; + loop { + font_size -= 2; + let dims = measure_text(&text, Some(&font), font_size as u16, 1.0); + + if dims.width <= target_w && dims.height <= target_h { + break; + } + } + + font_size } + }; - font_size -= 2; - dims = measure_text(&text, Some(&font), font_size as u16, 1.0); - } - + let dims = measure_text(&text, Some(&font), font_size, 1.0); let button_draw_offset = self.get_button_draw_offset(); let text_params = TextParams { font_size, @@ -130,17 +141,17 @@ impl ButtonWidget { interaction: &Interaction, resources: &Resources, settings: &Settings, - ) -> ButtonInteraction { + ) -> Option { if !self.is_active { self.is_down = false; - return ButtonInteraction::not_clicked(); + return None; } let mouse = interaction.mouse_pos; if interaction.mouse_pressed.contains(&MouseButton::Left) { if mouse.overlaps_rect(&self.rect) { self.is_down = true; - return ButtonInteraction::not_clicked(); + return None; } } @@ -150,13 +161,13 @@ impl ButtonWidget { let volume = settings.volume; play_sound_once(&click_sound, volume); self.is_down = false; - return ButtonInteraction::clicked(); + return Some(ButtonInteraction::clicked()); } self.is_down = false; } - ButtonInteraction::not_clicked() + None } } @@ -170,6 +181,7 @@ impl Default for ButtonWidget { color: UiColor::Grey, text: String::new(), sound: SoundKind::Click, + font_size: FontSize::Fill, on_click: |_| Command::NoInteraction, } } diff --git a/gui/src/widgets/controls.rs b/gui/src/widgets/controls.rs new file mode 100644 index 0000000..8c99ead --- /dev/null +++ b/gui/src/widgets/controls.rs @@ -0,0 +1,31 @@ +use std::collections::HashMap; + +use macroquad::input::KeyCode; + +use crate::{game::Command, widgets::Interaction}; + +pub struct ControlsWidget { + key_release_bindings: HashMap, +} + +impl ControlsWidget { + pub fn new() -> Self { + Self { + key_release_bindings: HashMap::new(), + } + } + + pub fn add(&mut self, key: KeyCode, command: Command) { + self.key_release_bindings.insert(key, command); + } + + pub fn handle_input(&self, interaction: &Interaction) -> Option { + for (key, command) in &self.key_release_bindings { + if interaction.keys_released.contains(&key) { + return Some(*command); + } + } + + None + } +} diff --git a/gui/src/widgets/counter.rs b/gui/src/widgets/counter.rs index 60adc21..615c233 100644 --- a/gui/src/widgets/counter.rs +++ b/gui/src/widgets/counter.rs @@ -77,12 +77,7 @@ impl CounterWidget { self.decrement.is_active = is_active; } - pub fn initialize_drawables( - &mut self, - total_area: Rect, - font_size: f32, - resources: &Resources, - ) { + pub fn place(&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; @@ -94,10 +89,10 @@ impl CounterWidget { 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.place(increment_rect); + self.increment.place(increment_rect, FontSize::Fill); let decrement_rect = Rect::new(btn_x, decrement_y, btn_w, btn_h); - self.decrement.place(decrement_rect); + self.decrement.place(decrement_rect, FontSize::Fill); 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); @@ -112,25 +107,27 @@ impl CounterWidget { resources: &Resources, settings: &Settings, ) -> CounterWidgetInteraction { - if self + if let Some(interaction) = self .increment .handle_input(&interaction, &resources, &settings) - .clicked { - if self.current < self.max { - self.current += 1; - return CounterWidgetInteraction::increment(self.current); + if interaction.clicked { + if self.current < self.max { + self.current += 1; + return CounterWidgetInteraction::increment(self.current); + } } } - if self + if let Some(interaction) = self .decrement .handle_input(&interaction, &resources, &settings) - .clicked { - if self.current > self.min { - self.current -= 1; - return CounterWidgetInteraction::decrement(self.current); + if interaction.clicked { + if self.current > self.min { + self.current -= 1; + return CounterWidgetInteraction::decrement(self.current); + } } } @@ -141,24 +138,7 @@ impl CounterWidget { self.increment.draw(resources); self.decrement.draw(resources); - let font = resources.font(); - let mut min_dim = self.visual_rect.w.min(self.visual_rect.h); - let mut font_size = self.font_size as u16; - loop { - if min_dim <= self.visual_rect.w * 0.5 { - break; - } - - let dims = measure_text( - &self.current.to_string(), - Some(&font), - font_size as u16, - 1.0, - ); - min_dim = dims.width.min(dims.height); - font_size -= 1; - } - + let font_size = self.font_size as u16; draw_text_ex( &self.current.to_string(), self.visual_rect.x + (self.visual_rect.w - self.visual_dims.width) / 2., diff --git a/gui/src/widgets/label.rs b/gui/src/widgets/label.rs index 2643714..f122d92 100644 --- a/gui/src/widgets/label.rs +++ b/gui/src/widgets/label.rs @@ -11,14 +11,14 @@ pub struct LabelWidget { } impl LabelWidget { - pub fn initialize_state(text: &str) -> Self { + pub fn new(text: &str) -> Self { Self { text: text.to_string(), ..Default::default() } } - pub fn initialize_drawables(&mut self, rect: Rect, font_size: f32, resources: &Resources) { + pub fn place(&mut self, rect: Rect, font_size: f32, resources: &Resources) { self.rect = rect; self.font_size = font_size; self.text_dimensions = diff --git a/lib/src/board.rs b/lib/src/board.rs index 87aeff1..369ec9c 100644 --- a/lib/src/board.rs +++ b/lib/src/board.rs @@ -23,7 +23,7 @@ use crate::{board::piece::Piece, generator::Puzzle}; pub struct Board { pub cells: [[Option; BOARD_SIZE]; BOARD_SIZE], pub legal_moves: HashSet, - pub game_state: BoardState, + pub state: BoardState, pub id: String, pub size: usize, pieces_remaining: u8, @@ -64,7 +64,7 @@ impl Board { legal_moves: HashSet::new(), id, pieces_remaining: 0, - game_state: BoardState::NotStarted, + state: BoardState::NotStarted, size: BOARD_SIZE, max_moves_per_piece: options.max_moves_per_piece, } @@ -190,7 +190,7 @@ impl Board { next_move: CMove, } - if let BoardState::Won = self.game_state { + if let BoardState::Won = self.state { return Puzzle { board: self.clone(), solutions: vec![vec![]], @@ -223,7 +223,7 @@ impl Board { let (mut board, mut moves_so_far, next) = (top.board, top.moves_so_far, top.next_move); board.make_move(next.clone()); - match board.game_state { + match board.state { BoardState::Won => { moves_so_far.push(next); solutions.push(moves_so_far); @@ -374,7 +374,7 @@ impl Board { } fn calc_game_state(&mut self) { - self.game_state = if self.pieces_remaining == 0 { + self.state = if self.pieces_remaining == 0 { BoardState::NotStarted } else if self.pieces_remaining == 1 { BoardState::Won @@ -674,7 +674,7 @@ mod tests { #[test] fn test_smoke_puzzle() { let mut board = Board::new(); - assert_eq!(BoardState::NotStarted, board.game_state); + assert_eq!(BoardState::NotStarted, board.state); assert_eq!(0, board.pieces_remaining); // K . . . @@ -682,22 +682,22 @@ mod tests { // . . R . // N . . . board.set(sq!("Ka4")); - assert_eq!(BoardState::Won, board.game_state); + assert_eq!(BoardState::Won, board.state); board.set(sq!("Pb3")); board.set(sq!("Rc2")); board.set(sq!("Na1")); - assert_eq!(BoardState::InProgress, board.game_state); + assert_eq!(BoardState::InProgress, board.state); assert_eq!(4, board.pieces_remaining); assert!(board.make_move(mv!("Na1", "Rc2")).is_some()); assert_eq!(3, board.pieces_remaining); - assert_eq!(BoardState::InProgress, board.game_state); + assert_eq!(BoardState::InProgress, board.state); assert!(board.make_move(mv!("Pb3", "Ka4")).is_some()); assert_eq!(2, board.pieces_remaining); - assert_eq!(BoardState::Lost, board.game_state); + assert_eq!(BoardState::Lost, board.state); // P . . . // . . . . @@ -712,12 +712,12 @@ mod tests { // . . N . // P . . . assert_eq!(4, board.pieces_remaining); - assert_eq!(BoardState::InProgress, board.game_state); + assert_eq!(BoardState::InProgress, board.state); board.make_move(mv!("Qa3", "Pa4")); board.make_move(mv!("Nc2", "Pa1")); assert_eq!(2, board.pieces_remaining); - assert_eq!(BoardState::InProgress, board.game_state); + assert_eq!(BoardState::InProgress, board.state); // Q . . . // . . . . @@ -725,7 +725,7 @@ mod tests { // N . . . board.make_move(mv!("Qa4", "Na1")); assert_eq!(1, board.pieces_remaining); - assert_eq!(BoardState::Won, board.game_state); + assert_eq!(BoardState::Won, board.state); } #[test] @@ -775,7 +775,7 @@ mod tests { solution .into_iter() .for_each(|m| assert!(board.make_move(m).is_some())); - assert_eq!(BoardState::Won, board.game_state); + assert_eq!(BoardState::Won, board.state); } }