From dc61d6d9560487ec11fdbe29c12b662504664f55 Mon Sep 17 00:00:00 2001 From: cool-mist Date: Sun, 25 Jan 2026 01:50:52 +0530 Subject: [PATCH] separate logic state and draw state --- gui/src/game.rs | 45 ++-- gui/src/game/draw.rs | 321 +++++------------------ gui/src/game/logic.rs | 321 ++++++++--------------- gui/src/main.rs | 3 +- gui/src/widgets.rs | 3 + gui/src/widgets/board.rs | 422 +++++++++++++++++++++++++++++++ gui/src/widgets/button.rs | 24 +- gui/src/widgets/id_text_input.rs | 5 +- 8 files changed, 633 insertions(+), 511 deletions(-) create mode 100644 gui/src/widgets/board.rs diff --git a/gui/src/game.rs b/gui/src/game.rs index 25d32c6..3cf23fc 100644 --- a/gui/src/game.rs +++ b/gui/src/game.rs @@ -2,21 +2,14 @@ pub mod constants; mod draw; mod logic; -use crate::{ - game::logic::{GameMode, GameState}, - resources::Resources, - widgets::*, -}; +use crate::{resources::Resources, widgets::*}; use macroquad::prelude::*; -use sol_lib::{board::Board, generator::Puzzle}; +use sol_lib::generator::Puzzle; #[derive(Default)] pub struct Game { resources: Resources, - - volume: f32, - - debug: bool, + settings: GameSettings, window_height: f32, window_width: f32, @@ -26,12 +19,7 @@ pub struct Game { reset_btn: ButtonWidget, next_btn: ButtonWidget, - current_board: Board, - square_width: f32, - num_squares: usize, // this is a constant = 4 for now. - board_rect: Rect, - squares: Vec, - state: GameState, + board: BoardWidget, heading_rect: Rect, heading_font_size: f32, @@ -47,12 +35,21 @@ pub struct Game { hard_btn: ButtonWidget, } -struct GameSquare { - rect: Rect, - color: Color, - is_source: bool, - is_target: bool, - is_previous_target: bool, - i: usize, - j: usize, +#[derive(Default)] +pub struct GameSettings { + pub volume: f32, + pub debug: bool, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum GameMode { + Easy, + Medium, + Hard, +} + +impl Default for GameMode { + fn default() -> Self { + GameMode::Medium + } } diff --git a/gui/src/game/draw.rs b/gui/src/game/draw.rs index 2b7eb61..6c82368 100644 --- a/gui/src/game/draw.rs +++ b/gui/src/game/draw.rs @@ -1,48 +1,20 @@ -use super::{Game, GameMode, constants}; +use super::{Game, constants}; use crate::widgets::*; use macroquad::{math, prelude::*}; -use sol_lib::board::piece::Piece; impl Game { - pub fn draw(&mut self) { - self.update_window_size(); - self.draw_heading(); - self.draw_board(); - self.draw_buttons(); - self.draw_id_text(); - self.draw_debug(); - } - - fn update_window_size(&mut self) { - let new_height = math::clamp( - screen_height(), - constants::SCREEN_HEIGHT_MIN, - constants::SCREEN_HEIGHT_MAX, - ); - let new_width = math::clamp( - screen_width(), - constants::SCREEN_WIDTH_MIN, - constants::SCREEN_WIDTH_MAX, - ); - - if new_height == self.window_height && new_width == self.window_width { - return; - } - - self.window_height = new_height; - self.window_width = new_width; - self.initialize_drawables(); - } - fn initialize_drawables(&mut self) { let min_dimension = f32::min(self.window_height, self.window_width); - self.square_width = constants::BOARD_SQUARE_WIDTH_MULTIPLIER * min_dimension; - let board_width = self.square_width * self.num_squares as f32; + 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_y = (self.window_height - board_width) / 2.0; - self.board_rect = Rect::new(board_x, board_y, board_width, board_width); + 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( self.heading_text.as_str(), @@ -52,65 +24,35 @@ impl Game { ); self.heading_rect = Rect::new( board_x + (board_width - dims.width) / 2.0, - board_y - dims.height - constants::TOP_HEADING_OFFSET_MULTIPLIER * self.square_width, + board_y - dims.height - constants::TOP_HEADING_OFFSET_MULTIPLIER * square_width, 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(super::GameSquare { - rect, - color, - i, - j, - is_source: false, - is_target: false, - is_previous_target: false, - }); - } - } - - self.squares = rects; - // 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; // Bottom row - let bottom_row_y = board_width - + board_y - + constants::BOTTOM_BUTTON_ROW_OFFSET_MULTIPLIER * self.square_width; - self.reset_btn = ButtonWidget::new(Rect::new( - board_x + (self.square_width - btn_w_sq) / 2., + 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., bottom_row_y, btn_w_sq, btn_h, )); - let btn_next_x_offset = - (self.board_rect.w - self.square_width) + (self.square_width - btn_w_sq) / 2.; - self.next_btn = ButtonWidget::new(Rect::new( + let btn_next_x_offset = (board_width - square_width) + (square_width - btn_w_sq) / 2.; + self.next_btn.initialize_drawables(Rect::new( board_x + btn_next_x_offset, bottom_row_y, btn_w_sq, btn_h, )); - self.next_btn.is_active = false; let id_text_rect = Rect::new( - board_x + 2. * self.square_width - btn_w, + board_x + 2. * square_width - btn_w, bottom_row_y, btn_w * 2., btn_h, @@ -119,50 +61,51 @@ impl Game { // Left column let left_column_x = - board_x - constants::BOTTOM_RIGHT_ROW_OFFSET_MULTIPLIER * self.square_width - btn_w; - self.rules_btn = ButtonWidget::new(Rect::new( + board_x - constants::BOTTOM_RIGHT_ROW_OFFSET_MULTIPLIER * square_width - btn_w; + self.rules_btn.initialize_drawables(Rect::new( left_column_x, - board_y + self.board_rect.h - self.square_width + (self.square_width - btn_h) / 2., + board_y + board_rect.h - square_width + (square_width - btn_h) / 2., btn_w, btn_h, )); // Right column - let right_column_x = board_x - + board_width - + constants::BOTTOM_RIGHT_ROW_OFFSET_MULTIPLIER * self.square_width; - self.easy_btn = ButtonWidget::new(Rect::new( + 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 + self.square_width + (self.square_width - btn_h) / 2., + board_y + square_width + (square_width - btn_h) / 2., btn_w, btn_h, )); - self.medium_btn = ButtonWidget::new(Rect::new( + self.medium_btn.initialize_drawables(Rect::new( right_column_x, - board_y + 2. * self.square_width + (self.square_width - btn_h) / 2., + board_y + 2. * square_width + (square_width - btn_h) / 2., btn_w, btn_h, )); - self.hard_btn = ButtonWidget::new(Rect::new( + self.hard_btn.initialize_drawables(Rect::new( right_column_x, - board_y + 3. * self.square_width + (self.square_width - btn_h) / 2., + board_y + 3. * square_width + (square_width - btn_h) / 2., btn_w, btn_h, )); + } - match self.game_mode { - GameMode::Easy => { - self.easy_btn.is_active = false; - } - GameMode::Medium => { - self.medium_btn.is_active = false; - } - GameMode::Hard => { - self.hard_btn.is_active = false; - } - } + 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_buttons(); + self.draw_debug(); } fn draw_heading(&self) { @@ -187,187 +130,47 @@ impl Game { ); } - fn draw_board(&self) { - let board_shadow_width = constants::BOARD_SHADOW_MULTIPLIER * self.square_width; - draw_shadow(&self.board_rect, board_shadow_width); - - if self.show_rules { - draw_rectangle( - self.board_rect.x, - self.board_rect.y, - self.board_rect.w, - self.board_rect.h, - UiColor::Yellow.to_bg_color(), - ); - - let font_size = self.heading_font_size * 0.6; - let rules = "\ - Every move should be a \n\ - capture. Win when only \n\ - one piece is left.\n"; - let measurement = - measure_text(rules, Some(&self.resources.font()), font_size as u16, 1.0); - let draw_text_params = TextParams { - font_size: font_size as u16, - font: Some(&self.resources.font()), - color: UiColor::Brown.to_bg_color(), - ..Default::default() - }; - 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) - - 2. * measurement.offset_y, - Some(2.), - draw_text_params, - ); - return; - } - - let sprite_size = constants::BOARD_PIECE_WIDTH_MULTIPLIER * 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.current_board.cells[square.i][square.j] { - let offset = (square.rect.w - sprite_size) / 2.0; - let texture_params = self.piece_draw_texture_params(&p, sprite_size); - if !square.is_source { - draw_texture_ex( - texture_params.texture, - square.rect.x + offset, - square.rect.y + offset, - WHITE, - texture_params.draw_text_params, - ); - } else { - selected_square = Some(square); - } - } - }); - - if let Some(selected_square) = selected_square { - if let Some(p) = self.current_board.cells[selected_square.i][selected_square.j] { - let texture_params = self.piece_draw_texture_params(&p, sprite_size); - draw_texture_ex( - texture_params.texture, - mouse_position().0 - sprite_size / 2.0, - mouse_position().1 - sprite_size / 2.0, - WHITE, - texture_params.draw_text_params, - ); - } - } - } - - fn piece_draw_texture_params<'a>( - &'a self, - piece: &Piece, - sprite_size: f32, - ) -> PieceDrawTextureParams<'a> { - let texture = self.resources.get_piece_texture(&piece); - let dtp = DrawTextureParams { - source: Some(Rect::new( - texture.texture_rect.x, - texture.texture_rect.y, - texture.texture_rect.w, - texture.texture_rect.h, - )), - dest_size: Some(Vec2::new(sprite_size, sprite_size)), - ..DrawTextureParams::default() - }; - - PieceDrawTextureParams { - texture: texture.texture, - draw_text_params: dtp, - } - } - fn draw_buttons(&mut self) { self.reset_btn.draw( constants::RESET_BUTTON_TEXT, &UiColor::Yellow, - &self.resources.font(), + &self.resources, ); + self.next_btn.draw( constants::NEXT_BUTTON_TEXT, &UiColor::Green, - &self.resources.font(), + &self.resources, ); + self.easy_btn.draw( constants::EASY_BUTTON_TEXT, &UiColor::Yellow, - &self.resources.font(), + &self.resources, ); + self.medium_btn.draw( constants::MEDIUM_BUTTON_TEXT, &UiColor::Yellow, - &self.resources.font(), + &self.resources, ); + self.hard_btn.draw( constants::HARD_BUTTON_TEXT, &UiColor::Yellow, - &self.resources.font(), + &self.resources, ); - let rules_btn_text = if self.show_rules { - constants::RULES_BUTTON_ALT_TEXT - } else { - constants::RULES_BUTTON_TEXT - }; self.rules_btn - .draw(rules_btn_text, &UiColor::Brown, &self.resources.font()); - } - - fn draw_id_text(&self) { - self.id_text - .draw(&self.resources.font(), &self.puzzle.board.id); + .draw(&self.rules_text, &UiColor::Brown, &self.resources); } 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.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); - + if self.settings.debug { self.show_fps(); } } - fn add_debug_info(&self, lines: Vec) { - let mut y = 20.0; - for line in lines { - draw_text(&line, 10.0, y, 20.0, BLACK); - y += 25.0; - } - } - fn show_fps(&self) { let fps = get_fps(); draw_text( @@ -378,9 +181,25 @@ impl Game { BLACK, ); } -} -struct PieceDrawTextureParams<'a> { - texture: &'a Texture2D, - draw_text_params: DrawTextureParams, + fn update_window_size(&mut self) { + let new_height = math::clamp( + screen_height(), + constants::SCREEN_HEIGHT_MIN, + constants::SCREEN_HEIGHT_MAX, + ); + let new_width = math::clamp( + screen_width(), + constants::SCREEN_WIDTH_MIN, + constants::SCREEN_WIDTH_MAX, + ); + + if new_height == self.window_height && new_width == self.window_width { + return; + } + + self.window_height = new_height; + self.window_width = new_width; + self.initialize_drawables(); + } } diff --git a/gui/src/game/logic.rs b/gui/src/game/logic.rs index 86c7b9e..aa36606 100644 --- a/gui/src/game/logic.rs +++ b/gui/src/game/logic.rs @@ -1,107 +1,80 @@ -use super::{Game, GameSquare, constants}; -use crate::{resources::*, widgets::*}; -use core::fmt; +use super::{Game, GameMode, GameSettings, constants}; +use crate::{ + resources::{self, *}, + widgets::*, +}; use macroquad::prelude::*; use sol_lib::{ board::BoardState, generator::{self, Puzzle, RandomRange}, }; -use std::fmt::{Display, Formatter}; - -#[derive(Copy, Clone)] -pub enum GameState { - SelectSource(Option<(usize, usize)>), - SelectTarget((usize, usize)), - GameOver((usize, usize)), -} - -impl Default for GameState { - fn default() -> Self { - GameState::SelectSource(None) - } -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] -pub enum GameMode { - Easy, - Medium, - Hard, -} - -impl Default for GameMode { - fn default() -> Self { - GameMode::Medium - } -} - -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), - } - } -} impl Game { - fn get(&mut self, i: usize, j: usize) -> &mut GameSquare { - &mut self.squares[i * self.num_squares + j] + pub async fn initialize_state() -> Self { + let resources = resources::init().await; + let game_mode = GameMode::Medium; + let settings = GameSettings { + volume: constants::VOLUME, + ..Default::default() + }; + let puzzle = Game::generate_puzzle(game_mode); + let board = BoardWidget::initialize_state(puzzle.board.size, puzzle.board.clone()); + let medium_btn = ButtonWidget::initialize_state(false); + let next_btn = ButtonWidget::initialize_state(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, + next_btn, + ..Self::default() + } } - pub fn handle_input(&mut self) { // CORE GAMEPLAY let (mouse_x, mouse_y) = mouse_position(); let mouse_pos = Circle::new(mouse_x, mouse_y, 0.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_pos, previous_target) - } - GameState::SelectTarget(source) => { - let next = self.handle_select_target(mouse_pos, source); - if let GameState::SelectTarget(_) = next { - self.reset_squares(); - GameState::SelectSource(None) - } else { - next - } - } - - GameState::GameOver(previous_target) => GameState::GameOver(previous_target), + let board_interaction = self + .board + .handle_input(mouse_pos, &self.resources, &self.settings); + if board_interaction.interacted { + let Some(board_state) = board_interaction.board_state else { + return; }; - self.state = new_state; - } - - 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_pos, previous_target) + match board_state { + BoardState::Won => { + self.next_btn.is_active = true; } - GameState::SelectTarget(source) => GameState::SelectTarget(source), - GameState::GameOver(previous_target) => GameState::GameOver(previous_target), - }; + _ => {} + } - self.state = new_state; + return; } - if (self + if self .reset_btn - .handle_input(&self.resources.sound(&SoundKind::Button), self.volume)) - .is_clicked + .handle_input( + &self.resources.sound(&SoundKind::Button), + self.settings.volume, + ) + .is_clicked { self.reset(); return; } - if (self + if self .next_btn - .handle_input(&self.resources.sound(&SoundKind::Button), self.volume)) - .is_clicked + .handle_input( + &self.resources.sound(&SoundKind::Button), + self.settings.volume, + ) + .is_clicked { self.next_puzzle(); return; @@ -109,30 +82,39 @@ impl Game { // PRESETS let mut clicked_game_mode_btn: Option<&ButtonWidget> = None; - if (self + if self .easy_btn - .handle_input(&self.resources.sound(&SoundKind::Mode), self.volume)) - .is_clicked + .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 + } else if self .medium_btn - .handle_input(&self.resources.sound(&SoundKind::Mode), self.volume)) - .is_clicked + .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 + } else if self .hard_btn - .handle_input(&self.resources.sound(&SoundKind::Mode), self.volume)) - .is_clicked + .handle_input( + &self.resources.sound(&SoundKind::Mode), + self.settings.volume, + ) + .is_clicked { self.game_mode = GameMode::Hard; self.easy_btn.is_active = true; @@ -146,10 +128,13 @@ impl Game { return; } - if (self + if self .rules_btn - .handle_input(&self.resources.sound(&SoundKind::Button), self.volume)) - .is_clicked + .handle_input( + &self.resources.sound(&SoundKind::Button), + self.settings.volume, + ) + .is_clicked { self.show_rules = !self.show_rules; self.rules_text = match self.show_rules { @@ -163,16 +148,19 @@ impl Game { // KEY INPUTS if is_key_released(KeyCode::Escape) { if self.show_rules { - play_sound_once(&self.resources.sound(&SoundKind::Button), self.volume); + play_sound_once( + &self.resources.sound(&SoundKind::Button), + self.settings.volume, + ); self.show_rules = false; - self.rules_text = constants::RESET_BUTTON_TEXT.to_string(); + self.rules_text = constants::RULES_BUTTON_TEXT.to_string(); } return; } if is_key_released(KeyCode::D) { - self.debug = !self.debug; + self.settings.debug = !self.settings.debug; return; } @@ -181,123 +169,24 @@ impl Game { } } - fn handle_select_source( - &mut self, - mouse_pos: Circle, - previous_target: Option<(usize, usize)>, - ) -> GameState { - self.reset_squares(); - let mut selected = None; - for square in &mut self.squares { - if mouse_pos.overlaps_rect(&square.rect) { - if let Some(_) = self.current_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.current_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_pos: Circle, source: (usize, usize)) -> GameState { - let mut selected = None; - for square in &mut self.squares { - if mouse_pos.overlaps_rect(&square.rect) { - if let Some(_) = self.current_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.current_board.legal_moves.iter().find(|m| { - m.from.file == s_x && m.from.rank == s_y && m.to.file == x && m.to.rank == y - }); - - let m = m.expect("legal move should be found"); - self.current_board.make_move(m.clone()); - - if self.current_board.game_state == BoardState::Won - || self.current_board.game_state == BoardState::Lost - { - self.reset_squares(); - if self.current_board.game_state == BoardState::Won { - self.next_btn.is_active = true; - play_sound_once(&self.resources.sound(&SoundKind::Win), self.volume); - } else { - play_sound_once(&self.resources.sound(&SoundKind::Loss), self.volume); - } - - return GameState::GameOver((x, y)); - } - - self.reset_squares(); - self.get(x, y).is_target = true; - play_sound_once(&self.resources.sound(&SoundKind::Click), self.volume); - return GameState::SelectSource(Some((x, y))); - } - - self.reset_squares(); - return GameState::SelectSource(None); - } - fn reset(&mut self) { - self.current_board = self.puzzle.board.clone(); + self.reset_game(Default::default()); + } + + fn reset_game(&mut self, options: ResetOptions) { + if options.create_new_puzzle { + self.puzzle = Game::generate_puzzle(self.game_mode); + } + self.next_btn.is_active = false; - self.state = GameState::SelectSource(None); - self.reset_squares(); + self.board.reset(&self.puzzle); } fn next_puzzle(&mut self) { - self.reset(); - let puzzle = Game::generate_puzzle(self.game_mode); - self.current_board = puzzle.board.clone(); - self.puzzle = puzzle; - } - - 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; - } - } + self.reset_game(ResetOptions { + create_new_puzzle: true, + ..Default::default() + }); } fn generate_puzzle(mode: GameMode) -> Puzzle { @@ -312,23 +201,11 @@ impl Game { let puzzle = generated.puzzle(); puzzle.expect("No puzzle was generated") } +} - pub fn new_game(resources: Resources) -> Self { - let game_mode = GameMode::Medium; - let puzzle = Game::generate_puzzle(game_mode); - let current_board = puzzle.board.clone(); - let num_squares: usize = current_board.size; - Self { - puzzle, - current_board, - resources, - game_mode, - num_squares, - heading_text: constants::HEADING_TEXT.to_string(), - volume: constants::VOLUME, - ..Self::default() - } - } +#[derive(Default)] +struct ResetOptions { + create_new_puzzle: bool, } struct MacroquadRandAdapter; diff --git a/gui/src/main.rs b/gui/src/main.rs index 3b60ca1..50ba779 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -28,8 +28,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 resources = resources::init().await; - let mut game = Game::new_game(resources); + let mut game = Game::initialize_state().await; loop { clear_background(background_color); game.handle_input(); diff --git a/gui/src/widgets.rs b/gui/src/widgets.rs index 66bea9c..ae45651 100644 --- a/gui/src/widgets.rs +++ b/gui/src/widgets.rs @@ -1,3 +1,6 @@ +mod board; +pub use board::*; + mod button; pub use button::*; diff --git a/gui/src/widgets/board.rs b/gui/src/widgets/board.rs new file mode 100644 index 0000000..1a5cd72 --- /dev/null +++ b/gui/src/widgets/board.rs @@ -0,0 +1,422 @@ +use core::fmt; +use std::fmt::{Display, Formatter}; + +use crate::{ + game::{GameSettings, constants}, + resources::{Resources, SoundKind}, + widgets::*, +}; +use macroquad::prelude::*; +use sol_lib::{ + board::{Board, BoardState, piece::Piece}, + generator::Puzzle, +}; + +#[derive(Default)] +pub struct BoardWidget { + current_board: Board, + square_width: f32, + pub num_squares: usize, // this is a constant = 4 for now. + board_rect: Rect, + squares: Vec, + state: GameState, +} + +pub struct BoardInteraction<'a> { + pub interacted: bool, + pub board_state: Option<&'a BoardState>, +} + +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; + board.current_board = current_board; + board + } + + pub fn initialize_drawables(&mut self, square_width: f32, board_rect: Rect) { + self.square_width = square_width; + self.board_rect = board_rect; + + 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 = self.board_rect.x + (i as f32 * self.square_width); + let y_eff = self.board_rect.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; + } + + pub fn draw(&self, show_rules: bool, rules_font_size: f32, 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 { + draw_rectangle( + self.board_rect.x, + self.board_rect.y, + self.board_rect.w, + self.board_rect.h, + UiColor::Yellow.to_bg_color(), + ); + + let font_size = rules_font_size * 0.6; + let rules = "\ + Every move should be a \n\ + capture. Win when only \n\ + one piece is left.\n"; + 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, + font: Some(resources.font()), + color: UiColor::Brown.to_bg_color(), + ..Default::default() + }; + 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) + - 2. * measurement.offset_y, + Some(2.), + draw_text_params, + ); + return; + } + + let sprite_size = constants::BOARD_PIECE_WIDTH_MULTIPLIER * 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.current_board.cells[square.i][square.j] { + let offset = (square.rect.w - sprite_size) / 2.0; + let texture_params = piece_draw_texture_params(&p, sprite_size, resources); + if !square.is_source { + draw_texture_ex( + texture_params.texture, + square.rect.x + offset, + square.rect.y + offset, + WHITE, + texture_params.draw_text_params, + ); + } else { + selected_square = Some(square); + } + } + }); + + if let Some(selected_square) = selected_square { + if let Some(p) = self.current_board.cells[selected_square.i][selected_square.j] { + let texture_params = piece_draw_texture_params(&p, sprite_size, resources); + draw_texture_ex( + texture_params.texture, + mouse_position().0 - sprite_size / 2.0, + mouse_position().1 - sprite_size / 2.0, + WHITE, + texture_params.draw_text_params, + ); + } + } + + if settings.debug { + self.draw_debug(); + } + } + + 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<'a>( + &'a mut self, + mouse_pos: Circle, + resources: &Resources, + settings: &GameSettings, + ) -> BoardInteraction<'a> { + if mouse_pos.overlaps_rect(&self.board_rect) == false { + return BoardInteraction { + interacted: false, + board_state: None, + }; + } + + 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_pos, previous_target) + } + GameState::SelectTarget(source) => { + let next = self.handle_select_target(mouse_pos, source, resources, settings); + 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; + } + + 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_pos, previous_target) + } + GameState::SelectTarget(source) => GameState::SelectTarget(source), + GameState::GameOver(previous_target) => GameState::GameOver(previous_target), + }; + + self.state = new_state; + } + + let board_state = &self.current_board.game_state; + return BoardInteraction { + interacted: true, + board_state: Some(board_state), + }; + } + + pub fn reset(&mut self, puzzle: &Puzzle) { + self.current_board = puzzle.board.clone(); + self.state = GameState::SelectSource(None); + self.reset_squares(); + } + + fn handle_select_source( + &mut self, + mouse_pos: Circle, + previous_target: Option<(usize, usize)>, + ) -> GameState { + self.reset_squares(); + let mut selected = None; + for square in &mut self.squares { + if mouse_pos.overlaps_rect(&square.rect) { + if let Some(_) = self.current_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.current_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_pos: Circle, + source: (usize, usize), + resources: &Resources, + settings: &GameSettings, + ) -> GameState { + let mut selected = None; + for square in &mut self.squares { + if mouse_pos.overlaps_rect(&square.rect) { + if let Some(_) = self.current_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.current_board.legal_moves.iter().find(|m| { + m.from.file == s_x && m.from.rank == s_y && m.to.file == x && m.to.rank == y + }); + + let m = m.expect("legal move should be found"); + self.current_board.make_move(m.clone()); + + if self.current_board.game_state == BoardState::Won + || self.current_board.game_state == BoardState::Lost + { + self.reset_squares(); + if self.current_board.game_state == BoardState::Won { + play_sound_once(resources.sound(&SoundKind::Win), settings.volume); + } else { + play_sound_once(resources.sound(&SoundKind::Loss), settings.volume); + } + + return GameState::GameOver((x, y)); + } + + self.reset_squares(); + self.get(x, y).is_target = true; + play_sound_once(resources.sound(&SoundKind::Click), settings.volume); + return GameState::SelectSource(Some((x, y))); + } + + self.reset_squares(); + return GameState::SelectSource(None); + } + + 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; + } + } + } +} + +struct GameSquare { + rect: Rect, + color: Color, + is_source: bool, + is_target: bool, + is_previous_target: bool, + i: usize, + j: usize, +} + +#[derive(Copy, Clone)] +pub enum GameState { + SelectSource(Option<(usize, usize)>), + SelectTarget((usize, usize)), + GameOver((usize, usize)), +} + +impl Default for GameState { + fn default() -> Self { + GameState::SelectSource(None) + } +} + +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), + } + } +} + +fn piece_draw_texture_params<'a>( + piece: &Piece, + sprite_size: f32, + resources: &'a Resources, +) -> PieceDrawTextureParams<'a> { + let texture = resources.get_piece_texture(&piece); + let dtp = DrawTextureParams { + source: Some(Rect::new( + texture.texture_rect.x, + texture.texture_rect.y, + texture.texture_rect.w, + texture.texture_rect.h, + )), + dest_size: Some(Vec2::new(sprite_size, sprite_size)), + ..DrawTextureParams::default() + }; + + PieceDrawTextureParams { + texture: texture.texture, + draw_text_params: dtp, + } +} + +struct PieceDrawTextureParams<'a> { + texture: &'a Texture2D, + draw_text_params: DrawTextureParams, +} diff --git a/gui/src/widgets/button.rs b/gui/src/widgets/button.rs index da4d59f..481a4a5 100644 --- a/gui/src/widgets/button.rs +++ b/gui/src/widgets/button.rs @@ -1,4 +1,4 @@ -use crate::widgets::*; +use crate::{resources::Resources, widgets::*}; use macroquad::{audio::Sound, prelude::*}; pub struct ButtonWidget { @@ -13,18 +13,21 @@ pub struct ButtonInteraction { } impl ButtonWidget { - pub fn new(rect: Rect) -> Self { + pub fn initialize_state(is_active: bool) -> Self { Self { - is_down: false, - is_active: true, - rect, - shadow_width: 5.0, + is_active, + ..Default::default() } } - pub fn draw(&self, text: &str, color: &UiColor, font: &Font) { + pub fn initialize_drawables(&mut self, rect: Rect) { + self.rect = rect; + self.shadow_width = 5.0; + } + + pub fn draw(&self, text: &str, color: &UiColor, resources: &Resources) { self.draw_button(color); - self.draw_label(text, font, color); + self.draw_label(text, resources, color); } fn draw_button(&self, color: &UiColor) { @@ -56,12 +59,13 @@ impl ButtonWidget { draw_shadow(&self.rect, self.shadow_width); } - fn draw_label(&self, text: &str, font: &Font, color: &UiColor) { + fn draw_label(&self, text: &str, resources: &Resources, color: &UiColor) { let font_color = match self.is_active { true => color.to_fg_color(), false => Color::from_rgba(100, 100, 100, 255), }; + 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 button_draw_offset = self.get_button_draw_offset(); @@ -124,7 +128,7 @@ impl ButtonWidget { impl Default for ButtonWidget { fn default() -> Self { Self { - is_active: Default::default(), + is_active: true, is_down: Default::default(), rect: Default::default(), shadow_width: Default::default(), diff --git a/gui/src/widgets/id_text_input.rs b/gui/src/widgets/id_text_input.rs index 6333a1f..9946718 100644 --- a/gui/src/widgets/id_text_input.rs +++ b/gui/src/widgets/id_text_input.rs @@ -1,4 +1,4 @@ -use crate::widgets::*; +use crate::{resources::Resources, widgets::*}; use macroquad::prelude::*; pub struct IdTextInput { @@ -38,12 +38,13 @@ impl IdTextInput { } } - pub fn draw(&self, font: &Font, text: &str) { + 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); + 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); draw_text_ex(