diff --git a/.gitignore b/.gitignore index 3eb2474..7bd455d 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,9 @@ Makefile compile_commands.json cmake_install.cmake CMakeCache.txt + +# Documentation artifacts +sphinx_docs/_build/ +sphinx_docs/_doxygen/ +__pycache__/ +*.pyc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a6904b8..aac737b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: - id: clang-tidy args: ["-p=build"] - id: cppcheck - args: ["--enable=all", "-Iinclude/", "--quiet", "src"] + args: ["--enable=all", "-Iinclude/", "--quiet", "src", "--suppress=normalCheckLevelMaxBranches", "--suppress=checkersReport", "--suppress=missingIncludeSystem", "--suppress=useStlAlgorithm", "--suppress=unusedFunction", "--suppress=unusedStructMember", "--suppress=unmatchedSuppression", "--inline-suppr"] - repo: https://github.com/crate-ci/typos rev: v1.40.0 @@ -53,7 +53,7 @@ repos: hooks: - id: check-added-large-files name: "๐Ÿ“ filesystem/๐Ÿค size ยท Prevent giant files from being committed" - args: [ '--maxkb=200' ] + args: [ '--maxkb=2000' ] - id: check-executables-have-shebangs name: "๐Ÿ“ filesystem/โš™๏ธ exec ยท Verify shebang presence" - id: check-shebang-scripts-are-executable diff --git a/CMakeLists.txt b/CMakeLists.txt index d6b0e3e..a67b639 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,10 +7,33 @@ project(MicroMouse-Simulator LANGUAGES CXX) # Sets the C++ standard for the entire project to C++17. # This ensures consistency across all source files and compilers. -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) set(CXX_EXPORT_BUILD_COMMANDS ON) +# --- SFML Integration using FetchContent (Source Code) --- +include(FetchContent) +FetchContent_Declare( + sfml + GIT_REPOSITORY https://github.com/SFML/SFML.git + GIT_TAG 2.6.1 +) + +# Configure SFML options to build only what we need, statically +set(SFML_BUILD_WINDOW ON CACHE BOOL "" FORCE) +set(SFML_BUILD_GRAPHICS ON CACHE BOOL "" FORCE) +set(SFML_BUILD_SYSTEM ON CACHE BOOL "" FORCE) +set(SFML_BUILD_AUDIO OFF CACHE BOOL "" FORCE) +set(SFML_BUILD_NETWORK OFF CACHE BOOL "" FORCE) +set(SFML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(SFML_BUILD_DOC OFF CACHE BOOL "" FORCE) +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) # Force static linking + +FetchContent_MakeAvailable(sfml) + +# Note: FetchContent_MakeAvailable(sfml) provides the targets SFML::Graphics, SFML::Window, etc. +# directly, so find_package is not needed. The targets will handle include directories automatically. + # --- GoogleTest Integration (Conditional) --- # This block executes only if the user configures CMake with -DBUILD_TESTING=ON. @@ -23,7 +46,7 @@ if(BUILD_TESTING) GIT_TAG 52eb8108c5bdec04579160ae17225d66034bd723 # Recommended: Add DOWNLOAD_EXTRACT_TIMESTAMP TRUE for build robustness (CMP0135). ) - + # Makes the googletest targets (GTest::gtest, GTest::gtest_main, etc.) available # for linking in subsequent targets (like mms-test). FetchContent_MakeAvailable(googletest) @@ -38,18 +61,38 @@ endif() # --- Core Logic Library Definition --- # Define the source files that contain the reusable application logic. -set(CORE_SOURCES src/MazeGen.cpp) +set(CORE_SOURCES + src/model/generators/RecursiveBacktracker.cpp + src/model/generators/EllersGenerator.cpp + src/model/MazeUtils.cpp + src/view/UI.cpp + src/view/UIComponents.cpp + src/controller/Simulator.cpp + src/model/solvers/BFSSolver.cpp + src/model/solvers/DFSSolver.cpp + src/model/solvers/AStarSolver.cpp + src/model/solvers/FloodFillSolver.cpp + src/model/solvers/WallFollowerSolver.cpp +) # Create a Static Library target. This bundles the core logic into a reusable # library that can be linked by both the main executable and the test executable. add_library(mms-core STATIC ${CORE_SOURCES}) -# Configure Include Directories +# Configure Include Directories for mms-core # Set the project's header directory as PUBLIC for the 'mms-core' library. target_include_directories(mms-core PUBLIC ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/include/model + ${CMAKE_SOURCE_DIR}/include/view + ${CMAKE_SOURCE_DIR}/include/controller ) +target_link_libraries(mms-core PUBLIC sfml-graphics sfml-window sfml-system) +if(WIN32) + target_link_libraries(mms-core PUBLIC comdlg32) +endif() +target_compile_definitions(mms-core PUBLIC SFML_STATIC) # The PUBLIC keyword ensures that any target that links to 'mms-core' (like 'mms' # and 'mms-test') automatically inherits this include path. @@ -61,7 +104,19 @@ set(APP_SOURCES src/Main.cpp) # Create the main executable target add_executable(mms ${APP_SOURCES}) -# Link the core logic library to the main executable. +# Link the core logic library and SFML to the main executable. +# PRIVATE linkage means the executable uses the library internally, +# but downstream targets don't need to know about it (standard best practice). +# Link the core logic library and SFML to the main executable. # PRIVATE linkage means the executable uses the library internally, # but downstream targets don't need to know about it (standard best practice). -target_link_libraries(mms PRIVATE mms-core) +target_link_libraries(mms PRIVATE mms-core sfml-graphics sfml-window sfml-system) + +# Define SFML_STATIC to ensure headers use static linkage conventions +target_compile_definitions(mms PRIVATE SFML_STATIC) + +# --- Assets Management --- +# Copy the assets directory to the build directory so the executable can find them +file(COPY "${CMAKE_SOURCE_DIR}/assets" DESTINATION "${CMAKE_BINARY_DIR}") + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f83a99b..d8329fa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,10 @@ -# ๐Ÿค Contributing to MicroMouse Simulator +# Contributing to MicroMouse Simulator Thank you for your interest in contributing to the MicroMouse Simulator project! This guide will help you get started with contributing code, reporting issues, and following our development practices. --- -## ๐Ÿ“‹ Table of Contents +## Table of Contents - [Getting Started](#getting-started) - [Development Workflow](#development-workflow) @@ -19,7 +19,7 @@ Thank you for your interest in contributing to the MicroMouse Simulator project! --- -## ๐Ÿš€ Getting Started +## Getting Started ### Prerequisites @@ -84,7 +84,7 @@ Before contributing, ensure you have: --- -## ๐Ÿ”„ Development Workflow +## Development Workflow ### Branching Model @@ -165,19 +165,19 @@ git rebase dev --- -## ๐Ÿ’ป Coding Standards +## Coding Standards ### Naming Conventions #### 1. File Names Use **PascalCase** for all source files: ``` -โœ… Good: +Correct: - MazeGen.cpp / MazeGen.hpp - PathFinder.cpp / PathFinder.hpp - RobotController.cpp / RobotController.hpp -โŒ Bad: +Incorrect: - maze_gen.cpp - pathfinder.cpp - robot-controller.cpp @@ -190,13 +190,13 @@ Use **PascalCase** for all source files: #### 2. Variable Names Use **snake_case** for variables. Names should be descriptive and self-explanatory: ```cpp -โœ… Good: +Correct: int cell_count; double path_length; bool is_visited; std::vector current_path; -โŒ Bad: +Incorrect: int cc; double pl; bool v; @@ -206,12 +206,12 @@ std::vector cp; #### 3. Class Names Use **PascalCase** for class names: ```cpp -โœ… Good: +Correct: class MazeGenerator; class PathFinder; class RobotController; -โŒ Bad: +Incorrect: class maze_generator; class pathfinder; class robot_controller; @@ -220,30 +220,44 @@ class robot_controller; #### 4. Function Names Use **snake_case** for function names: ```cpp -โœ… Good: +Correct: void generate_maze(); int calculate_distance(); bool is_wall_present(); -โŒ Bad: +Incorrect: void generateMaze(); int calculateDistance(); bool isWallPresent(); ``` #### 5. Constants and Macros -Use **UPPER_SNAKE_CASE** for constants: +Use **UPPER_SNAKE_CASE** for all constants, including global constants, class constants, and local `static constexpr` values: ```cpp -โœ… Good: +Correct: const int MAX_MAZE_SIZE = 16; +static constexpr float K_SIDEBAR_WIDTH = 280.0F; -โŒ Bad: +Incorrect: const int maxMazeSize = 16; ``` +#### 6. Global Variables and State +Avoid non-const global variables. If global state is necessary, encapsulate it in a `struct` within an anonymous namespace to maintain modularity and satisfy static analysis (`cppcoreguidelines-avoid-non-const-global-variables`): + +```cpp +namespace { + struct SimulationState { + int maze_rows = 16; + bool is_solving = false; + }; + SimulationState G_STATE; +} +``` + --- -## ๐Ÿ› ๏ธ Development Tools Setup +## Development Tools Setup ### Clang-Format Configuration @@ -332,7 +346,7 @@ clang-tidy src/filename.cpp -p build/ --- -## ๐Ÿ”จ Build System +## Build System ### CMake Configuration @@ -374,19 +388,14 @@ ctest --test-dir build --output-on-failure ``` MicroMouse-Simulator/ โ”œโ”€โ”€ CMakeLists.txt # Main build configuration -โ”œโ”€โ”€ include/ # Header files -โ”‚ โ””โ”€โ”€ header files (.hpp) -โ”œโ”€โ”€ src/ # Source files -โ”‚ โ”œโ”€โ”€ Main.cpp -โ”‚ โ””โ”€โ”€ other .cpp files -โ””โ”€โ”€ tests/ # Test files - โ”œโ”€โ”€ CMakeLists.txt # Test build configuration - โ””โ”€โ”€ test files (.cpp) +โ”œโ”€โ”€ include/ # Header files (.hpp) +โ”œโ”€โ”€ src/ # Source files (.cpp) +โ”œโ”€โ”€ tests/ # Test files ``` --- -## ๐Ÿ”’ Pre-commit Hooks +## Pre-commit Hooks The project uses pre-commit hooks to automatically check code quality before commits. Configuration is in `.pre-commit-config.yaml`. @@ -465,7 +474,7 @@ git commit --no-verify -m "WIP: temporary commit" --- -## ๐Ÿ“ Commitizen Configuration +## Commitizen Configuration The project uses Commitizen (configured in `cz.yaml`) to enforce consistent commit message formatting. @@ -525,12 +534,12 @@ When using `git commit` directly (not recommended, but validated by pre-commit), **Examples:** ```bash -โœ… Good: +Correct: git commit -m "feat(pathfinding): add Dijkstra algorithm implementation" git commit -m "fix(maze): resolve wall generation bug in corner cases" git commit -m "docs(readme): update build instructions for Windows" -โŒ Bad: +Incorrect: git commit -m "fixed stuff" git commit -m "updates" git commit -m "WIP" @@ -538,7 +547,7 @@ git commit -m "WIP" --- -## ๐Ÿงช Testing Guidelines +## Testing Guidelines ### Prerequisites @@ -583,9 +592,24 @@ TEST(MazeGenTest, BasicGeneration) { } ``` +## Documentation Workflow + +All public classes, methods, and structures must be documented using Doxygen-style comments. + +**Rule: Document in Headers ONLY** +To avoid redundancy and maintain a single source of truth, all Doxygen comments must reside in the header files (`.hpp`). Do not add Doxygen comments to implementation files (`.cpp`) for functions already documented in the header. + +```cpp +/** + * @brief Brief description. + * @param name Description of parameter. + * @return Description of return value. + */ +``` + --- -## ๐Ÿ“ค Submitting Changes +## Submitting Changes ### Pull Request Checklist @@ -618,7 +642,7 @@ Closes #123 --- -## ๐Ÿ› Reporting Issues +## Reporting Issues ### Before Opening an Issue @@ -668,7 +692,7 @@ For feature requests, include: --- -## ๐ŸŽฏ Good First Issues +## Good First Issues Looking for a place to start? Check out issues labeled: @@ -678,7 +702,7 @@ Looking for a place to start? Check out issues labeled: --- -## ๐Ÿ“š Additional Resources +## Additional Resources - [Git Workflow Guide](https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow) - [Conventional Commits](https://www.conventionalcommits.org/) @@ -689,7 +713,7 @@ Looking for a place to start? Check out issues labeled: --- -## ๐Ÿ’ฌ Questions? +## Questions? If you have questions about contributing: diff --git a/README.md b/README.md index 2baf7f4..5b9df18 100644 --- a/README.md +++ b/README.md @@ -6,18 +6,37 @@ A sophisticated micromouse maze simulator featuring pathfinding algorithms and i ## Table of Contents +- [Features](#features) - [Prerequisites](#prerequisites) - [Quick Start](#quick-start) - [Building the Project](#building-the-project) - - [Step 1: Clone the Repository](#step-1-clone-the-repository) - - [Step 2: Configure the Build](#step-2-configure-the-build) - - [Step 3: Compile the Project](#step-3-compile-the-project) - [Running the Simulator](#running-the-simulator) - [Testing](#testing) - [Troubleshooting](#troubleshooting) --- +## Features + +### Maze Generators +- **Recursive Backtracker**: A classic randomized depth-first search algorithm that creates complex, perfect mazes. +- **Eller's Algorithm**: A memory-efficient algorithm that generates mazes row-by-row, allowing for potentially infinite maze generation. + +### Pathfinding Solvers +- **Breadth-First Search (BFS)**: Guarantees the shortest path in an unweighted grid. +- **Depth-First Search (DFS)**: Explores as far as possible along each branch before backtracking. +- **A* Search**: Uses heuristics (Manhattan distance) to find the most efficient path. +- **Flood Fill**: A popular algorithm for micromouse that calculates distances from the goal. +- **Wall Follower**: Implements the classic left-hand rule for maze navigation. + +### Interactive UI +- Real-time visualization of generation and solving processes. +- Adjustable simulation speed. +- Manual wall toggling and maze editing. +- Zoom and pan support for large mazes. + +--- + ## Prerequisites Before building, ensure you have: @@ -26,10 +45,12 @@ Before building, ensure you have: - **C++ Compiler** (GCC, Clang, MSVC, or MinGW) - **Git** (for cloning the repository) - **Build System** (Make, Ninja, or MSBuild) +- **Python 3** (for pre-commit hooks) --- ## Quick Start + ```bash # Clone the repository git clone https://github.com/kr8457/MicroMouse-Simulator @@ -52,90 +73,35 @@ ctest --test-dir build --output-on-failure ## Building the Project -### Step 1: Clone the Repository - -Clone the project from GitHub: -```bash -git clone https://github.com/kr8457/MicroMouse-Simulator -``` - -> **Tip:** The latest features are on the `dev` branch: -> ```bash -> git checkout dev -> ``` - -Navigate to the project directory: -```bash -cd MicroMouse-Simulator -``` - -### Step 2: Configure the Build - -#### Basic Configuration +### Basic Configuration For a standard build without testing: ```bash cmake -B build ``` -#### Configuration with Testing +### Configuration with Testing To enable GoogleTest for unit testing: ```bash cmake -B build -DBUILD_TESTING=ON ``` -This will automatically fetch and build GoogleTest from source, integrating it with the project. +This will automatically fetch and build GoogleTest from source. -#### Choosing a Generator - -You can specify different CMake generators using the `-G` flag: -```bash -# For MinGW Makefiles -cmake -B build -G "MinGW Makefiles" - -# For Visual Studio 2022 -cmake -B build -G "Visual Studio 17 2022" - -# For Ninja -cmake -B build -G "Ninja" -``` - -**What are generators?** Generators produce build system configurations that tools like Make, Ninja, or MSBuild use to compile your project. - -> [!NOTE] -> If no generator is specified, CMake automatically selects the default for your platform. - -### Step 3: Compile the Project +### Compile the Project Build the configured project: ```bash cmake --build build ``` -For multi-configuration generators (like Visual Studio), specify the build type: -```bash -# Release build (optimized) -cmake --build build --config Release - -# Debug build (with debug symbols) -cmake --build build --config Debug -``` - --- ## Running the Simulator -The executable location depends on your generator and configuration: - -| Generator | Executable Location | -|-----------|---------------------| -| MinGW Makefiles / Unix Makefiles | `build/mms` or `build/mms.exe` | -| Visual Studio (Debug) | `build/Debug/mms.exe` | -| Visual Studio (Release) | `build/Release/mms.exe` | -| Ninja | `build/mms` or `build/mms.exe` | +Run the simulator based on your system: -Run the simulator: ```bash # For Unix-like systems ./build/mms @@ -152,22 +118,9 @@ Run the simulator: ## Testing If you configured the project with `-DBUILD_TESTING=ON`, run the test suite using CTest: -```bash -ctest --test-dir build --output-on-failure -``` -**Options:** -- `--output-on-failure`: Displays test output only when tests fail -- `-V` or `--verbose`: Shows detailed output for all tests -- `-R `: Run only tests matching the regular expression - -### Running Specific Tests ```bash -# Run tests matching a pattern -ctest --test-dir build -R TrivialEquality --output-on-failure - -# Run tests verbosely -ctest --test-dir build -V +ctest --test-dir build --output-on-failure ``` --- @@ -175,33 +128,19 @@ ctest --test-dir build -V ## Troubleshooting ### CMake Configuration Fails - - Verify CMake is installed: `cmake --version` - Ensure a C++ compiler is in your PATH - Try specifying a generator explicitly with `-G` ### Build Errors - - Check that all prerequisites are installed - Ensure your compiler supports C++11 or higher - Try cleaning and reconfiguring: -```bash + ```bash rm -rf build/ cmake -B build cmake --build build -``` - -### Cannot Find Executable - -- Check the executable location table above -- Verify the build completed without errors -- Look in `build/Debug/` or `build/Release/` for Visual Studio builds - -### Test Failures - -- Ensure you configured with `-DBUILD_TESTING=ON` -- Check that GoogleTest was successfully downloaded and built -- Run tests with `-V` flag for detailed output + ``` --- @@ -209,3 +148,5 @@ ctest --test-dir build -V - [CMake Documentation](https://cmake.org/documentation/) - [GoogleTest GitHub](https://github.com/google/googletest) +- [Doxygen Documentation](https://www.doxygen.nl/manual/index.html) +- [Sphinx Documentation](https://www.sphinx-doc.org/) diff --git a/assets/fonts/OpenSans-Regular.ttf b/assets/fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000..a135120 Binary files /dev/null and b/assets/fonts/OpenSans-Regular.ttf differ diff --git a/assets/fonts/Pixel-Regular.ttf b/assets/fonts/Pixel-Regular.ttf new file mode 100644 index 0000000..7f4fb35 Binary files /dev/null and b/assets/fonts/Pixel-Regular.ttf differ diff --git a/assets/fonts/PixelOperatorMono-Bold.ttf b/assets/fonts/PixelOperatorMono-Bold.ttf new file mode 100644 index 0000000..215d4a2 Binary files /dev/null and b/assets/fonts/PixelOperatorMono-Bold.ttf differ diff --git a/assets/fonts/pixel.ttf b/assets/fonts/pixel.ttf new file mode 100644 index 0000000..fe4328b Binary files /dev/null and b/assets/fonts/pixel.ttf differ diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..0f265be Binary files /dev/null and b/assets/icon.png differ diff --git a/include/MazeGen.hpp b/include/MazeGen.hpp deleted file mode 100644 index 557bf25..0000000 --- a/include/MazeGen.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once -#include -#include -#include - -class MazeGen { -public: - std::string generate(size_t rows, size_t columns); - static size_t exampleFunc(); -}; \ No newline at end of file diff --git a/include/controller/Simulator.hpp b/include/controller/Simulator.hpp new file mode 100644 index 0000000..01c833e --- /dev/null +++ b/include/controller/Simulator.hpp @@ -0,0 +1,171 @@ +/** + * @file Simulator.hpp + * @brief Header for the Simulator class, managing the MicroMouse simulation + * state and logic. + */ + +#pragma once + +#include "generators/GeneratorInterface.hpp" +#include "UI.hpp" +#include "solvers/SolverInterface.hpp" +#include +#include +#include +#include +#include +#include + +/** + * @enum SolverType + * @brief Identifies the available maze-solving algorithms. + */ +enum class SolverType : std::uint8_t { + BFS = 0, + DFS = 1, + ASTAR = 2, + FLOODFILL = 3, + WALL = 4 +}; + +/** + * @enum GeneratorType + * @brief Identifies the available maze-generation algorithms. + */ +enum class GeneratorType : std::uint8_t { + RECURSIVE = 0, + ELLERS = 1 +}; + +/** + * @class Simulator + * @brief Manages the complete simulation state and orchestrates maze + * generation, solving, and animation. + */ +class Simulator { + public: + /** + * @brief Constructs a new Simulator with default settings. + */ + Simulator(); + + /** + * @brief Initializes the simulator and loads required resources. + * @return True if initialization succeeded, false otherwise. + */ + auto initialize() -> bool; + + /** + * @brief Processes SFML events and updates simulation state. + * @param window The render window to handle events for. + * @param event The event to process (mouse clicks, key presses, etc.). + */ + auto handle_event(sf::RenderWindow &window, const sf::Event &event) -> void; + + /** + * @brief Updates the simulation state based on elapsed time. + * + * Handles animation timing for both maze generation and solving. + * @param delta_time Time elapsed since last update in seconds. + */ + auto update(float delta_time) -> void; + + /** + * @brief Renders the current simulation state to the window. + * + * Draws the maze grid, paths, and UI sidebar. + * @param window The render window to draw to. + */ + auto render(sf::RenderWindow &window) -> void; + + /** + * @brief Updates the stored mouse position relative to the render window. + * @param window The render window. + */ + auto update_mouse_position(sf::RenderWindow &window) -> void; + + private: + // Simulation constants + static constexpr size_t DEFAULT_ROWS = 16; ///< Default number of rows for a new maze. + static constexpr size_t DEFAULT_COLS = 16; ///< Default number of columns for a new maze. + static constexpr int DEFAULT_ANIM_SPEED = 100; ///< Default animation speed in milliseconds per step. + + // Simulation state + size_t maze_rows_ = DEFAULT_ROWS; ///< Current number of maze rows. + size_t maze_cols_ = DEFAULT_COLS; ///< Current number of maze columns. + size_t display_rows_ = DEFAULT_ROWS; ///< Target number of rows for the next generation. + size_t display_cols_ = DEFAULT_COLS; ///< Target number of columns for the next generation. + + std::unique_ptr maze_gen_; ///< Instance of the current maze generator. + std::vector maze_grid_; ///< The 2D grid of maze cells. + Graph maze_graph_; ///< Graph representation of the maze for solving. + std::vector solved_path_; ///< Path found by the solver. + std::vector exploration_path_; ///< List of nodes explored by the solver. + + bool is_generating_ = false; ///< Whether a maze is currently being generated. + bool is_solving_ = false; ///< Whether a solver is currently running. + bool is_paused_ = true; ///< Whether the solving animation is paused. + size_t anim_step_idx_ = 0; ///< Current step in the solver animation. + int anim_speed_ = DEFAULT_ANIM_SPEED; ///< Current animation speed. + + SolverType solver_type_ = SolverType::BFS; ///< Currently selected solver algorithm. + GeneratorType gen_type_ = GeneratorType::RECURSIVE; ///< Currently selected generator algorithm. + std::unique_ptr solver_instance_; ///< Instance of the current maze solver. + + sf::Clock anim_clock_; ///< Clock used for animation timing. + sf::Vector2f mouse_pos_; ///< Current mouse position in window coordinates. + UI ui_; ///< UI manager instance. + float time_accumulator_ = 0.0F; ///< Accumulates time for animation stepping. + + // Zoom and Pan state + float zoom_factor_ = 1.0f; ///< Current camera zoom level. + sf::Vector2f camera_offset_ = {0.0f, 0.0f}; ///< Current camera pan offset. + bool is_panning_ = false; ///< Whether the user is currently panning the camera. + sf::Vector2i last_mouse_pos_; ///< Last recorded mouse position during panning. + + /** + * @brief Initiates the selected maze-solving algorithm. + */ + auto run_solver() -> void; + + /** + * @brief Initiates the selected maze-generation algorithm. + * @param step_by_step If true, enables animated generation. + */ + auto start_generation(bool step_by_step) -> void; + + /** + * @brief Resets the simulation to its initial state. + */ + auto reset_simulator() -> void; + + /** + * @brief Handles mouse clicks for instant maze solving. + * @param mouse_pos The mouse position in world coordinates. + */ + auto handle_instant_solve_click(const sf::Vector2f &mouse_pos) -> void; + + /** + * @brief Performs a single step in the solver animation. + */ + auto step_solver_animation() -> void; + + /** + * @brief Toggles a wall at the specified cell boundary. + * @param row Cell row index. + * @param col Cell column index. + * @param wall_side The side of the cell where the wall is located. + */ + auto toggle_wall(size_t row, size_t col, int wall_side) -> void; + + /** + * @brief Saves the current maze configuration to a file. + */ + auto save_maze() -> void; + + /** + * @brief Loads a maze configuration from a file. + */ + auto load_maze() -> void; +}; + diff --git a/include/model/MazeUtils.hpp b/include/model/MazeUtils.hpp new file mode 100644 index 0000000..bb6a00e --- /dev/null +++ b/include/model/MazeUtils.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include + +/** + * @struct Cell + * @brief Represents a single cell in the maze grid. + * Stores wall states and visited status. + */ +struct Cell { + bool top = true; + bool right = true; + bool bottom = true; + bool left = true; + bool visited = false; +}; + +/** + * @brief Adjacency list representation of the maze graph. + */ +using Graph = std::vector>; + +/** + * @class MazeUtils + * @brief Provides static utility methods for coordinate conversion and graph transformation. + */ +class MazeUtils { +public: + /** + * @brief Converts 2D (row, col) coordinates to a 1D index for linear storage. + * + * Uses the formula: (row * columns) + col. + * @param row The row index (0-indexed). + * @param col The column index (0-indexed). + * @param columns The total number of columns in the grid. + * @return The 1D index corresponding to the 2D coordinates. + */ + static inline auto get_1d_index(size_t row, size_t col, size_t columns) -> size_t { + return (row * columns) + col; + } + + /** + * @brief Converts a 1D index back to 2D (row, col) coordinates. + * @param node_idx The linear index to convert. + * @param columns The total number of columns in the grid. + * @return A pair containing {row, col}. + */ + static inline auto get_2d_coords(size_t node_idx, size_t columns) -> std::pair { + return {node_idx / columns, node_idx % columns}; + } + + /** + * @brief Converts the 2D maze grid (vector of Cells) into an adjacency list graph. + * + * Analyzes wall boundaries for each cell to determine traversable neighbors. + * @param grid The linear vector of maze cells. + * @param rows Total number of rows in the maze. + * @param columns Total number of columns in the maze. + * @return An adjacency list where each entry contains a list of reachable neighbor indices. + */ + static auto convert_to_graph(const std::vector& grid, size_t rows, size_t columns) -> Graph; +}; + diff --git a/include/model/generators/EllersGenerator.hpp b/include/model/generators/EllersGenerator.hpp new file mode 100644 index 0000000..2a3698c --- /dev/null +++ b/include/model/generators/EllersGenerator.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include "GeneratorInterface.hpp" +#include +#include + +/** + * @class EllersGenerator + * @brief Implements Eller's algorithm for row-by-row maze generation. + * This algorithm is efficient (O(N) space) and allows for infinitely tall mazes. + */ +class EllersGenerator : public GeneratorInterface { +public: + EllersGenerator() = default; + + /** + * @brief Initializes the generator state for a new maze. + * @param rows Total rows in the maze. + * @param cols Total columns in the maze. + */ + void initialize(size_t rows, size_t cols) override; + + /** + * @brief Advances the algorithm by one step (processing horizontal/vertical connections). + * @return True if more steps are required, false if the maze is finished. + */ + auto step() -> bool override; + + /** + * @brief Checks if the maze generation is complete. + * @return True if done, false otherwise. + */ + auto is_done() const -> bool override { return done_; } + + /** + * @brief Gets a constant reference to the current grid state. + * @return Reference to the vector of Cells. + */ + auto get_grid() const -> const std::vector& override { return grid_; } + + /** + * @brief Gets the cells in the current row being processed. + * @return Reference to a vector of (row, col) pairs. + */ + auto get_path() const -> const std::vector>& override { return current_row_cells_; } + +private: + size_t rows_ = 0; + size_t cols_ = 0; + std::vector grid_; + bool done_ = true; + bool initialized_ = false; + + // Algorithm state + size_t current_row_ = 0; + std::vector sets_; // Set ID for each cell in the current row + int next_set_id_ = 1; + std::mt19937 rng_; + + enum class State { + HORIZONTAL, + VERTICAL, + FINAL_ROW, + DONE + }; + State state_ = State::HORIZONTAL; + + // For visualization + std::vector> current_row_cells_; + + void assign_sets(); + void horizontal_connections(); + void vertical_connections(); + void finalize_last_row(); + void prepare_next_row(); +}; diff --git a/include/model/generators/GeneratorInterface.hpp b/include/model/generators/GeneratorInterface.hpp new file mode 100644 index 0000000..f6e8a34 --- /dev/null +++ b/include/model/generators/GeneratorInterface.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include "MazeUtils.hpp" + +/** + * @class GeneratorInterface + * @brief Pure virtual interface for all maze generation algorithms. + */ +class GeneratorInterface { +public: + virtual ~GeneratorInterface() = default; + + /** + * @brief Initializes the maze generator state for a new grid. + * + * Resets all internal structures and prepares the generator for a new run. + * @param rows The number of rows in the maze grid. + * @param cols The number of columns in the maze grid. + */ + virtual void initialize(size_t rows, size_t cols) = 0; + + /** + * @brief Performs a single step of the generation algorithm. + * + * Used for real-time visualization of the generation process. + * @return True if generation is still in progress, false if completed. + */ + virtual auto step() -> bool = 0; + + /** + * @brief Checks if the maze generation process has finished. + * @return True if the generation is complete, false otherwise. + */ + virtual auto is_done() const -> bool = 0; + + /** + * @brief Gets the current state of the maze cells. + * @return A constant reference to the grid of Cells. + */ + virtual auto get_grid() const -> const std::vector& = 0; + + /** + * @brief Gets the current algorithm path or stack for visualization. + * + * For Recursive Backtracker, this is the current recursion stack. + * For Eller's, this might include the currently processed row's cells. + * @return A vector of (row, col) pairs representing the active path. + */ + virtual auto get_path() const -> const std::vector>& = 0; +}; + diff --git a/include/model/generators/RecursiveBacktracker.hpp b/include/model/generators/RecursiveBacktracker.hpp new file mode 100644 index 0000000..237442c --- /dev/null +++ b/include/model/generators/RecursiveBacktracker.hpp @@ -0,0 +1,90 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "GeneratorInterface.hpp" +#include "MazeUtils.hpp" + +/** + * @class RecursiveBacktracker + * @brief Implements the Recursive Backtracker algorithm for perfect maze + * generation. + */ +class RecursiveBacktracker : public GeneratorInterface { + public: + RecursiveBacktracker() = default; + + /** + * @brief Initializes the maze generator state. + * @param rows Number of rows in the maze. + * @param columns Number of columns in the maze. + */ + void initialize(size_t rows, size_t columns) override; + + /** + * @brief Performs a single step of the maze generation algorithm. + * + * Uses a stack-based depth-first search to carve paths through the grid. + * @return True if the generation is still in progress, False if completed. + */ + auto step() -> bool override; + + /** + * @brief Checks if the maze generation is finished. + * @return True if finished, False otherwise. + */ + auto is_done() const -> bool override; + + /** + * @brief Gets a constant reference to the current grid state. + * @return Reference to the vector of Cells. + */ + auto get_grid() const -> const std::vector & override { return grid_; } + + /** + * @brief Gets a constant reference to the active recursion stack. + * @return Reference to the vector of (row, col) pairs representing the current path. + */ + auto get_path() const -> const std::vector> & override { + return path_stack_; + } + + /** + * @brief Static helper to generate a complete perfect maze instantly. + * @param rows Number of rows. + * @param columns Number of columns. + * @return A vector of Cells representing the finished maze. + */ + static auto generate(size_t rows, size_t columns) -> std::vector; + + /** + * @brief Utility to render the maze in ASCII format for debugging. + * @param grid The maze grid to render. + * @param rows Total rows. + * @param columns Total columns. + * @return A string representation of the maze. + */ + static auto render_ascii(const std::vector &grid, size_t rows, + size_t columns) -> std::string; + + private: + size_t rows_ = 0; + size_t cols_ = 0; + std::vector grid_; + std::vector> path_stack_; + bool initialized_ = false; + bool done_ = true; + std::mt19937 rng_; + + // Helper to collect unvisited neighbors (internal use) + auto get_neighbors(int row, int col) const + -> std::vector>; + // Helper to remove walls (internal use) + void remove_walls(int row, int col, int nrow, int ncol); +}; + diff --git a/include/model/solvers/AStarSolver.hpp b/include/model/solvers/AStarSolver.hpp new file mode 100644 index 0000000..e6ac630 --- /dev/null +++ b/include/model/solvers/AStarSolver.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include "SolverInterface.hpp" +#include +#include +#include +#include + +class AStarSolver : public SolverInterface { +public: + /** + * @brief Initializes the A* solver state. + * @param grid Maze grid. + * @param graph Adjacency list for pathfinding. + * @param start 1D start index. + * @param end 1D goal index. + * @param rows Number of rows. + * @param cols Number of columns. + */ + void initialize(const std::vector& grid, + const Graph& graph, + size_t start, + size_t end, + size_t rows, + size_t cols) override; + + /** + * @brief Performs one iteration of the A* algorithm (extracting and expanding one node). + * @return True if search is ongoing, false if target found or queue empty. + */ + bool step() override; + + /** + * @brief Reconstructs and returns the optimal path found by A*. + * @return Vector of node indices. + */ + std::vector get_path() const override; + + /** + * @brief Returns the order of nodes explored during search. + * @return Vector of node indices. + */ + std::vector get_visited_order() const override; + + /** + * @brief Checks if the target cell was reached. + * @return True if path found, false otherwise. + */ + bool is_solved() const override; + +private: + const Graph* graph_ = nullptr; + size_t start_node_ = 0; + size_t end_node_ = 0; + size_t rows_ = 0; + size_t cols_ = 0; + size_t total_nodes_ = 0; + + using NodeDist = std::pair; + std::priority_queue, std::greater<>> priority_q_; + + std::vector g_score_; + std::vector predecessors_; + + std::vector visited_order_; + bool solved_ = false; + bool finished_ = false; +}; + diff --git a/include/model/solvers/BFSSolver.hpp b/include/model/solvers/BFSSolver.hpp new file mode 100644 index 0000000..8b28b46 --- /dev/null +++ b/include/model/solvers/BFSSolver.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include "SolverInterface.hpp" +#include +#include + +class BFSSolver : public SolverInterface { +public: + /** + * @brief Initializes the BFS solver with the maze graph. + * @param grid Maze grid. + * @param graph Adjacency list. + * @param start 1D start index. + * @param end 1D goal index. + * @param rows Total rows. + * @param cols Total columns. + */ + void initialize(const std::vector& grid, + const Graph& graph, + size_t start, + size_t end, + size_t rows, + size_t cols) override; + + /** + * @brief Performs one step of the Breadth-First Search. + * @return True if search continues, false if target found or queue empty. + */ + bool step() override; + + /** + * @brief Reconstructs the shortest path found by BFS. + * @return Vector of node indices. + */ + std::vector get_path() const override; + + /** + * @brief Returns the order in which nodes were visited during BFS. + * @return Vector of node indices. + */ + std::vector get_visited_order() const override; + + /** + * @brief Checks if the target has been reached. + * @return True if solved, false otherwise. + */ + bool is_solved() const override; + +private: + const Graph* graph_ = nullptr; + size_t start_node_ = 0; + size_t end_node_ = 0; + size_t total_nodes_ = 0; + + std::queue work_queue_; + std::vector visited_; + std::vector predecessors_; + + // For visualization + std::vector visited_order_; + + bool solved_ = false; + bool finished_ = false; // True if queue empty or target found +}; + diff --git a/include/model/solvers/DFSSolver.hpp b/include/model/solvers/DFSSolver.hpp new file mode 100644 index 0000000..71b7c4d --- /dev/null +++ b/include/model/solvers/DFSSolver.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "SolverInterface.hpp" +#include +#include + +class DFSSolver : public SolverInterface { +public: + /** + * @brief Initializes the DFS solver state. + * @param grid Maze grid. + * @param graph Adjacency list. + * @param start 1D start node. + * @param end 1D end node. + * @param rows Total rows. + * @param cols Total columns. + */ + void initialize(const std::vector& grid, + const Graph& graph, + size_t start, + size_t end, + size_t rows, + size_t cols) override; + + /** + * @brief Performs one step of Depth-First Search. + * @return True if continuing, false if finished. + */ + bool step() override; + + /** + * @brief Returns the path found by DFS. + * @return Vector of node indices. + */ + std::vector get_path() const override; + + /** + * @brief Returns the order of node visits. + * @return Vector of node indices. + */ + std::vector get_visited_order() const override; + + /** + * @brief Checks if the target was found. + * @return True if solved, false otherwise. + */ + bool is_solved() const override; + +private: + const Graph* graph_ = nullptr; + size_t start_node_ = 0; + size_t end_node_ = 0; + size_t total_nodes_ = 0; + + std::stack work_stack_; + std::vector visited_; + std::vector predecessors_; + + std::vector visited_order_; + + bool solved_ = false; + bool finished_ = false; +}; + diff --git a/include/model/solvers/FloodFillSolver.hpp b/include/model/solvers/FloodFillSolver.hpp new file mode 100644 index 0000000..647d4e8 --- /dev/null +++ b/include/model/solvers/FloodFillSolver.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include "SolverInterface.hpp" +#include +#include + +class FloodFillSolver : public SolverInterface { +public: + /** + * @brief Initializes the Flood Fill solver state. + * @param grid Maze grid. + * @param graph Adjacency list. + * @param start 1D target start node (where the agent starts). + * @param end 1D goal node (distance 0). + * @param rows Total rows. + * @param cols Total columns. + */ + void initialize(const std::vector& grid, + const Graph& graph, + size_t start, + size_t end, + size_t rows, + size_t cols) override; + + /** + * @brief Advances the solver (either flooding distances or following the gradient). + * @return True if solver animation should continue. + */ + bool step() override; + + /** + * @brief Returns the gradient path from start to end. + * @return Vector of node indices. + */ + std::vector get_path() const override; + + /** + * @brief Returns visited order during the flooding phase. + * @return Vector of node indices. + */ + std::vector get_visited_order() const override; + + /** + * @brief Checks if the agent reached the goal. + * @return True if solved. + */ + bool is_solved() const override; + + /** + * @brief Returns the calculated distance field for visualization. + * @return Vector of integers mapping one-to-one with grid cells. + */ + std::vector get_grid_values() const override { return distances_; } + +private: + const std::vector* grid_ = nullptr; + const Graph* graph_ = nullptr; + size_t start_node_ = 0; + size_t end_node_ = 0; + size_t rows_ = 0; + size_t cols_ = 0; + size_t total_nodes_ = 0; + + std::queue work_queue_; + std::vector distances_; + + std::vector visited_order_; + bool solved_ = false; + bool finished_ = false; + bool flooding_phase_ = true; +}; + diff --git a/include/model/solvers/SolverInterface.hpp b/include/model/solvers/SolverInterface.hpp new file mode 100644 index 0000000..0106ef6 --- /dev/null +++ b/include/model/solvers/SolverInterface.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include "MazeUtils.hpp" + +class SolverInterface { +public: + virtual ~SolverInterface() = default; + + /** + * @brief Initializes the solver state with necessary data. + * + * @param grid The collection of maze cells (wall configuration). + * @param graph Adjacency list representation of traversable paths. + * @param start 1D index of the starting cell. + * @param end 1D index of the target destination cell. + * @param rows Number of rows in the maze. + * @param cols Number of columns in the maze. + */ + virtual void initialize(const std::vector& grid, + const Graph& graph, + size_t start, + size_t end, + size_t rows, + size_t cols) = 0; + + /** + * @brief Advances the solving algorithm by exactly one step. + * + * This method is intended for visualization of the search process. + * @return True if the algorithm is still actively searching, + * false if it has finished (either found the path or exhausted all options). + */ + virtual bool step() = 0; + + /** + * @brief Runs the algorithm to completion immediately without visualization. + */ + virtual void solve_instant() { + while(step()); + } + + /** + * @brief Retrieves the path found by the solver. + * + * If the solver is still running, this returns the path found so far. + * @return Vector of 1D node indices representing the path. + */ + virtual std::vector get_path() const = 0; + + /** + * @brief Retrieves the chronological order of nodes explored by the solver. + * @return Vector of 1D node indices in the order they were visited. + */ + virtual std::vector get_visited_order() const = 0; + + /** + * @brief Checks if the target cell has been successfully reached. + * @return True if a path to the goal exists and was found. + */ + virtual bool is_solved() const = 0; + + /** + * @brief Provides algorithm-specific values for cell visualization. + * + * For example, Flood Fill uses this to return distances from the goal. + * @return Vector of integers mapped one-to-one with grid cells. Empty if not used. + */ + virtual std::vector get_grid_values() const { return {}; } + + /** + * @brief Gets the current direction the solver agent is facing. + * @return An integer representing direction (0: North, 1: East, 2: South, 3: West, -1: Undefined). + */ + virtual int get_current_heading() const { return -1; } +}; + diff --git a/include/model/solvers/WallFollowerSolver.hpp b/include/model/solvers/WallFollowerSolver.hpp new file mode 100644 index 0000000..b0df324 --- /dev/null +++ b/include/model/solvers/WallFollowerSolver.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include "SolverInterface.hpp" +#include + +class WallFollowerSolver : public SolverInterface { +public: + /** + * @brief Solves the maze instantly by following the right-hand rule until the exit is reached. + */ + virtual void solve_instant() override; + + /** + * @brief Initializes the solver with the maze grid and start/end points. + * @param grid Grid of cells for wall checks. + * @param graph Adjacency list (not primary for wall follower, but kept for interface). + * @param start 1D start index. + * @param end 1D goal index. + * @param rows Number of rows. + * @param cols Number of columns. + */ + void initialize(const std::vector& grid, + const Graph& graph, + size_t start, + size_t end, + size_t rows, + size_t cols) override; + + /** + * @brief Performs one step of the wall-following algorithm. + * @return True if still searching, false if solved or failed. + */ + bool step() override; + + /** + * @brief Gets the path taken by the agent. + * @return Vector of node indices. + */ + std::vector get_path() const override; + + /** + * @brief Gets the order in which nodes were visited. + * @return Vector of node indices. + */ + std::vector get_visited_order() const override; + + /** + * @brief Checks if the solver has reached the target. + * @return True if solved, false otherwise. + */ + bool is_solved() const override; + + int get_current_heading() const override { return facing_; } + +private: + const std::vector* grid_ = nullptr; + size_t start_node_ = 0; + size_t end_node_ = 0; + size_t rows_ = 0; + size_t cols_ = 0; + + size_t current_node_ = 0; + int facing_ = 0; // 0:Up, 1:Right, 2:Down, 3:Left + + std::vector path_; // Stores the path taken + std::vector visited_order_; + + bool solved_ = false; + bool finished_ = false; + + // Cycle detection: tracks {node, facing} states + std::vector visited_states_; +}; + diff --git a/include/view/UI.hpp b/include/view/UI.hpp new file mode 100644 index 0000000..42919fd --- /dev/null +++ b/include/view/UI.hpp @@ -0,0 +1,203 @@ +/** + * @file UI.hpp + * @brief Header for the UI class, managing rendering and user interaction for + * the MicroMouse simulator. + */ + +#pragma once + +#include "generators/GeneratorInterface.hpp" +#include "view/UITheme.hpp" +#include "view/UIComponents.hpp" +#include +#include +#include +#include + +/** + * @struct UILayout + * @brief Stores the bounding boxes (sf::FloatRect) for all interactive UI + * elements. Used to detect clicks and hover states. + */ +struct UILayout { + sf::FloatRect gen_inst_btn; ///< Quick Generate button + sf::FloatRect gen_step_btn; ///< Step-by-step Generate button + sf::FloatRect recursive_gen_btn; ///< Recursive Backtracker selection + sf::FloatRect ellers_gen_btn; ///< Eller's algorithm selection + sf::FloatRect play_btn; ///< Resume/Pause button + sf::FloatRect step_btn; ///< Simulation step button + sf::FloatRect bfs_btn; ///< BFS solver selection + sf::FloatRect dfs_btn; ///< DFS solver selection + sf::FloatRect astar_btn; ///< A* solver selection + sf::FloatRect flood_btn; ///< Flood Fill solver selection + sf::FloatRect wall_btn; ///< Wall Follower solver selection + sf::FloatRect solve_btn; ///< Animate Solve button + sf::FloatRect solve_inst_btn; ///< Instant Solve button + sf::FloatRect reset_btn; ///< Reset Simulator button + sf::FloatRect save_btn; ///< Save Maze button + sf::FloatRect load_btn; ///< Load Maze button + + sf::FloatRect row_input_box; ///< Input box for Rows + sf::FloatRect col_input_box; ///< Input box for Columns + sf::FloatRect speed_input_box; ///< Input box for Speed + sf::FloatRect apply_btn; ///< Apply Dimensions button +}; + +/** + * @class UI + * @brief Orchestrates the simulation's GUI, including the sidebar, maze + * rendering, and event dispatching. + */ +class UI { + public: + /** @brief Default constructor. */ + UI(); + + /** + * @brief Loads required assets (fonts) from system or local paths. + * @return True if at least one font was loaded, false otherwise. + */ + auto load_resources() -> bool; + + /** + * @brief Renders the entire application interface. + * + * Orchestrates the drawing of the sidebar and the maze area. + * @param window The render window to draw to. + * @param maze_rows Total rows in the maze grid. + * @param maze_cols Total columns in the maze grid. + * @param display_rows Rows to display in dimension inputs. + * @param display_cols Columns to display in dimension inputs. + * @param grid The collection of maze cells to render. + * @param exploration_path Sequence of cells explored by the solver. + * @param solved_path Final path found by the solver. + * @param is_generating True if a generation animation is active. + * @param is_solving True if a solver animation is active. + * @param is_paused True if the simulation is currently paused. + * @param solver_type Enum value of the active solver. + * @param gen_type Enum value of the active generator. + * @param animation_speed Current speed of the simulation in ms. + * @param maze_gen Reference to the active generator for step-by-step visuals. + * @param mouse_pos Current mouse position for hover effects. + * @param zoom_factor Current camera zoom level. + * @param camera_offset Current camera pan offset. + * @param grid_values Optional numeric values to display in cells (e.g., for Flood Fill). + * @param heading Optional direction for the mouse/robot (0=N, 1=E, 2=S, 3=W). + */ + auto draw(sf::RenderWindow &window, size_t maze_rows, size_t maze_cols, + size_t display_rows, size_t display_cols, + const std::vector &grid, + const std::vector &exploration_path, + const std::vector &solved_path, + bool is_generating, bool is_solving, + bool is_paused, int solver_type, int gen_type, + int animation_speed, const GeneratorInterface &maze_gen, + const sf::Vector2f &mouse_pos, + float zoom_factor, const sf::Vector2f &camera_offset, + const std::vector& grid_values = {}, + int heading = -1) -> void; + + /** + * @brief Handles SFML events and dispatches simulation state updates. + * + * Manages text input focus, button clicks, and camera manipulation. + * @param window The render window. + * @param event The event to process. + * @param maze_rows [in,out] Current maze rows. + * @param maze_cols [in,out] Current maze columns. + * @param display_rows [in,out] Maze rows shown in UI text box. + * @param display_cols [in,out] Maze columns shown in UI text box. + * @param is_generating [in,out] Generation state. + * @param is_solving [in,out] Solver state. + * @param is_paused [in,out] Pause state. + * @param solver_type [in,out] Selected solver index. + * @param gen_type [in,out] Selected generator index. + * @param animation_speed [in,out] Animation speed in ms. + * @param start_gen Callback to initiate maze generation. + * @param start_sol Callback to initiate maze solving. + * @param step_fn Callback for simulation stepping. + * @param reset_fn Callback to reset simulation. + * @param toggle_wall_fn Callback to manually toggle cell walls. + * @param save_fn Callback to save maze. + * @param load_fn Callback to load maze. + * @param zoom_factor [in,out] Camera zoom factor. + * @param camera_offset [in,out] Camera pan offset. + */ + auto handle_event(sf::RenderWindow &window, const sf::Event &event, + size_t &maze_rows, size_t &maze_cols, + size_t &display_rows, size_t &display_cols, + bool &is_generating, bool &is_solving, bool &is_paused, + int &solver_type, int &gen_type, int &animation_speed, + const std::function &start_gen, + const std::function &start_sol, + const std::function &step_fn, + const std::function &reset_fn, + const std::function &toggle_wall_fn, + const std::function &save_fn, + const std::function &load_fn, + float &zoom_factor, sf::Vector2f &camera_offset) -> void; + + /** @brief Gets the current UI layout bounding boxes. */ + auto get_layout() const -> const UILayout & { return layout_; } + + private: + sf::Font font_; ///< The font used for all UI text elements. + UILayout layout_; ///< Stores bounding boxes for all interactive UI elements. + + std::string row_input_buffer_; ///< Text buffer for the row dimension input field. + std::string col_input_buffer_; ///< Text buffer for the column dimension input field. + std::string speed_input_buffer_; ///< Text buffer for the animation speed input field. + bool row_focused_ = false; ///< Whether the row input field has focus. + bool col_focused_ = false; ///< Whether the column input field has focus. + bool speed_focused_ = false; ///< Whether the speed input field has focus. + + /** + * @brief Renders the UI sidebar containing controls and statistics. + * @param window The render window. + * @param display_rows Current rows value in text box. + * @param display_cols Current columns value in text box. + * @param is_generating If generation is active. + * @param is_solving If solving is active. + * @param is_paused If simulation is paused. + * @param solver_type Index of current solver. + * @param gen_type Index of current generator. + * @param exploration_count Number of nodes explored. + * @param solved_count Number of nodes in final path. + * @param animation_speed Current speed in ms. + * @param mouse_pos Current mouse position. + */ + auto draw_sidebar(sf::RenderWindow &window, size_t display_rows, + size_t display_cols, bool is_generating, bool is_solving, + bool is_paused, int solver_type, int gen_type, + size_t exploration_count, size_t solved_count, + int animation_speed, + const sf::Vector2f &mouse_pos) -> void; + + /** + * @brief Renders the 2D maze grid with optional paths and values. + * + * Handles centering, zoom, and panning logic for the maze viewport. + * @param window The render window. + * @param rows Total grid rows. + * @param cols Total grid columns. + * @param grid Grid of cells to draw. + * @param exploration_path Path segments explored by the solver. + * @param solved_path Final path segments found by the solver. + * @param is_generating If generation is active. + * @param maze_gen Reference to active generator for specific status. + * @param grid_values Numeric values to display for certain solvers. + * @param heading Direction indicator. + * @param zoom_factor Current camera scale. + * @param camera_offset Current camera pan. + */ + auto draw_maze(sf::RenderWindow &window, size_t rows, size_t cols, + const std::vector &grid, + const std::vector &exploration_path, + const std::vector &solved_path, + bool is_generating, + const GeneratorInterface &maze_gen, + const std::vector& grid_values, + int heading, + float zoom_factor, const sf::Vector2f &camera_offset) -> void; +}; + diff --git a/include/view/UIComponents.hpp b/include/view/UIComponents.hpp new file mode 100644 index 0000000..90b1ec0 --- /dev/null +++ b/include/view/UIComponents.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +/** + * @namespace UIComponents + * @brief Reusable UI drawing functions. + */ +namespace UIComponents { + + /** + * @brief Draws a reusable button element with hover and active states. + * @param window The render window to draw to. + * @param font The font to use for the button label. + * @param label The text to display on the button. + * @param bounds [out] The bounding box of the calculated button area. + * @param x The X-coordinate of the button's top-left corner. + * @param y The Y-coordinate of the button's top-left corner. + * @param width The target width of the button. + * @param active Whether the button is currently selected/active. + * @param mouse_pos Current mouse position for hover detection. + */ + void draw_btn(sf::RenderWindow &window, const sf::Font &font, + const std::string &label, sf::FloatRect &bounds, + float x, float y, float width, + bool active, const sf::Vector2f &mouse_pos); + + /** + * @brief Draws a stylized text input box with focus indicators. + * @param window The render window to draw to. + * @param font The font to use for labels and input text. + * @param label The descriptive label above or beside the input. + * @param value The current string value to display inside the box. + * @param bounds [out] The bounding box of the interactive input area. + * @param x The X-coordinate of the input box's top-left corner. + * @param y The Y-coordinate of the input box's top-left corner. + * @param width The target width of the input box. + * @param focused Whether the input box currently has keyboard focus. + */ + void draw_input_box(sf::RenderWindow &window, const sf::Font &font, + const std::string &label, const std::string &value, + sf::FloatRect &bounds, float x, float y, + float width, bool focused); + +} // namespace UIComponents + diff --git a/include/view/UITheme.hpp b/include/view/UITheme.hpp new file mode 100644 index 0000000..8b9663e --- /dev/null +++ b/include/view/UITheme.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +/** + * @namespace Theme + * @brief Defines the visual style and color palette of the application. + * + * Implements a "Sunset" theme using deep purples for surfaces and + * vibrant oranges and roses for interactive elements and highlights. + */ +namespace Theme { + // Deep Twilight/Purple base + const sf::Color BACKGROUND = sf::Color(30, 27, 46); ///< Deep Midnight Purple for the main window background. + const sf::Color SIDEBAR = sf::Color(45, 35, 66); ///< Muted Plum color for the sidebar container. + const sf::Color SURFACE = sf::Color(68, 56, 100); ///< Lighter Purple for buttons and panel surfaces. + + // The "Sun" Colors (Warm Tones) + const sf::Color PRIMARY = sf::Color(251, 146, 60); ///< Bright Sunset Orange for primary actions and highlights. + const sf::Color PRIMARY_HOVER = sf::Color(249, 115, 22); ///< Deep Burnt Orange for primary buttons in hovered state. + const sf::Color ACCENT = sf::Color(244, 63, 112); ///< Vivid Rose/Pink for accentuating specific UI elements. + + // Typography + const sf::Color TEXT_MAIN = sf::Color(255, 247, 237); ///< Warm Cream White for main body and heading text. + const sf::Color TEXT_DIM = sf::Color(167, 139, 192); ///< Soft Lavender Gray for secondary or disabled text. + + // State Indicators + const sf::Color SUCCESS = sf::Color(52, 211, 153); ///< Seafoam Green for successful operations or status indicators. + const sf::Color PATH_COLOR = sf::Color(253, 224, 71); ///< Golden Hour Yellow for rendering the final solved path. + const sf::Color EXPLORATION_COLOR = sf::Color(56, 189, 248); ///< Light Sky Blue for rendering explored areas/nodes. + const sf::Color DOT_COLOR = sf::Color(255, 255, 255); ///< Pure White for marker dots and interactive indicators. + const sf::Color WALL_COLOR = sf::Color(91, 76, 125); ///< Dusty Purple for maze walls. + + // Visited cells use a subtle warm glow + const sf::Color VISITED_CELL = sf::Color(251, 146, 60, 40); ///< Transparent Orange Glow for marking visited cells during generation. +} // namespace Theme + diff --git a/src/Main.cpp b/src/Main.cpp index 1ee2173..0847f68 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -1,9 +1,84 @@ -#include "MazeGen.hpp" +/** + * @file Main.cpp + * @brief Entry point for the MicroMouse Simulator Pro. + * @details Initializes the SFML environment and runs the main application loop. + */ + +#include "Simulator.hpp" +#include +#include +#include #include -int main() { - MazeGen gen; - auto maze = gen.generate(5, 10); - std::cout << "Hello Maze!\n"; - std::cout << maze << "\n"; -} \ No newline at end of file +// Application constants +static constexpr int WINDOW_WIDTH = 1280; +static constexpr int WINDOW_HEIGHT = 720; +static constexpr int MIN_WINDOW_WIDTH = 800; +static constexpr int MIN_WINDOW_HEIGHT = 600; + +auto main() -> int { + sf::RenderWindow window(sf::VideoMode(WINDOW_WIDTH, WINDOW_HEIGHT), + "MicroMouse Simulator Pro", + sf::Style::Default); + window.setFramerateLimit(60); + + // Load and set application icon + sf::Image icon; + if (icon.loadFromFile("assets/icon.png")) { + window.setIcon(icon.getSize().x, icon.getSize().y, icon.getPixelsPtr()); + } else if (icon.loadFromFile("../assets/icon.png")) { + window.setIcon(icon.getSize().x, icon.getSize().y, icon.getPixelsPtr()); + } + + try { + Simulator simulator; + if (!simulator.initialize()) { + return 1; + } + + sf::Clock delta_clock; + + while (window.isOpen()) { + sf::Event event{}; + while (window.pollEvent(event)) { + if (event.type == sf::Event::Closed) { + window.close(); + } + if (event.type == sf::Event::Resized) { + // Enforce minimum window size + unsigned int width = event.size.width; + unsigned int height = event.size.height; + + if (width < MIN_WINDOW_WIDTH || height < MIN_WINDOW_HEIGHT) { + width = std::max(width, static_cast(MIN_WINDOW_WIDTH)); + height = std::max(height, static_cast(MIN_WINDOW_HEIGHT)); + window.setSize(sf::Vector2u(width, height)); + } + + // Update the view to match the new window size + sf::FloatRect visible_area(0.f, 0.f, + static_cast(width), + static_cast(height)); + window.setView(sf::View(visible_area)); + } + simulator.handle_event(window, event); + } + + // Update simulation + const float DELTA_TIME = delta_clock.restart().asSeconds(); + simulator.update_mouse_position(window); + simulator.update(DELTA_TIME); + + // Render + window.clear(Theme::BACKGROUND); + simulator.render(window); + window.display(); + } + } catch (const std::exception &e) { + std::cerr << "EXCEPTION: " << e.what() << "\n"; + return 1; + } catch (...) { + std::cerr << "UNKNOWN EXCEPTION CAUGHT\n"; + return 1; + } +} diff --git a/src/MazeGen.cpp b/src/MazeGen.cpp deleted file mode 100644 index d5378a0..0000000 --- a/src/MazeGen.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include - -std::string MazeGen::generate(size_t rows, size_t columns) { - std::string maze = ""; - for (size_t row = 0; row < rows; row++) { - maze += "|"; - for (size_t column = 0; column < columns; column++) { - maze += "-"; - } - maze += "|\n"; - } - return maze; -} - -size_t MazeGen::exampleFunc() { return 42; } diff --git a/src/controller/Simulator.cpp b/src/controller/Simulator.cpp new file mode 100644 index 0000000..9171681 --- /dev/null +++ b/src/controller/Simulator.cpp @@ -0,0 +1,421 @@ +/** + * @file Simulator.cpp + * @brief Implementation of the Simulator class for the MicroMouse simulator. + */ +#include "Simulator.hpp" +#include "MazeUtils.hpp" +#include "generators/EllersGenerator.hpp" +#include "generators/RecursiveBacktracker.hpp" +#include "solvers/AStarSolver.hpp" +#include "solvers/BFSSolver.hpp" +#include "solvers/DFSSolver.hpp" +#include "solvers/FloodFillSolver.hpp" +#include "solvers/WallFollowerSolver.hpp" +#include +#ifdef _WIN32 +#include +#include +#endif + +// ========================================================== +// CONSTRUCTOR & INITIALIZATION +// ========================================================== + +Simulator::Simulator() = default; + +auto Simulator::initialize() -> bool { + if (!ui_.load_resources()) { + std::cerr << "CRITICAL: Could not load any system font. App cannot " + "start.\n"; + return false; + } + + // Generate initial maze + start_generation(false); + return true; +} + +// ========================================================== +// EVENT HANDLING +// ========================================================== + +auto Simulator::handle_event(sf::RenderWindow &window, const sf::Event &event) + -> void { + // Let UI handle sidebar clicks + const int PREV_SOLVER = static_cast(solver_type_); + int current_solver_idx = static_cast(solver_type_); + int current_gen_idx = static_cast(gen_type_); + + ui_.handle_event( + window, event, maze_rows_, maze_cols_, display_rows_, display_cols_, + is_generating_, is_solving_, is_paused_, current_solver_idx, + current_gen_idx, anim_speed_, + [this](bool step_by_step) { start_generation(step_by_step); }, + [this]() { run_solver(); }, [this]() { step_solver_animation(); }, + [this]() { reset_simulator(); }, + [this](size_t r, size_t c, int s) { toggle_wall(r, c, s); }, + [this]() { save_maze(); }, [this]() { load_maze(); }, zoom_factor_, + camera_offset_); + + solver_type_ = static_cast(current_solver_idx); + gen_type_ = static_cast(current_gen_idx); + + // If solver changed while not solving, clear screen status + if (static_cast(solver_type_) != PREV_SOLVER) { + is_solving_ = false; + solved_path_.clear(); + exploration_path_.clear(); + solver_instance_.reset(); + } + + // Handle Instant Solve (Skip Animation) + if (event.type == sf::Event::MouseButtonPressed) { + const sf::Vector2f MPOS_VAL = + window.mapPixelToCoords(sf::Mouse::getPosition(window)); + handle_instant_solve_click(MPOS_VAL); + } +} + +// ========================================================== +// UPDATE & ANIMATION +// ========================================================== + +auto Simulator::update(float delta_time) -> void { + if (is_paused_) { + time_accumulator_ = 0.0F; + return; + } + + const float THRESHOLD = 1.0F / static_cast(anim_speed_); + time_accumulator_ += delta_time; + + if (time_accumulator_ >= THRESHOLD) { + time_accumulator_ = 0.0F; + + if (is_generating_ && maze_gen_) { + const bool STILL_RUNNING = maze_gen_->step(); + // Update grid after each step so UI can display changes + maze_grid_ = maze_gen_->get_grid(); + + if (!STILL_RUNNING) { + maze_graph_ = MazeUtils::convert_to_graph( + maze_grid_, maze_rows_, maze_cols_); + // Clear visited flag so the generation "orange" highlight disappears + for (auto& cell : maze_grid_) { + cell.visited = false; + } + is_generating_ = false; + } + } else if (is_solving_) { + if (solver_instance_) { + bool running = solver_instance_->step(); + exploration_path_ = solver_instance_->get_visited_order(); + + // Update the "Ribbon" path live for Wall Follower so the user + // sees the trail + if (solver_type_ == SolverType::WALL) { + solved_path_ = solver_instance_->get_path(); + } + + if (!running) { + is_solving_ = false; + solved_path_ = solver_instance_->get_path(); + } + } else { + is_solving_ = false; + } + } + } +} + +// ========================================================== +// RENDERING +// ========================================================== + +auto Simulator::render(sf::RenderWindow &window) -> void { + std::vector grid_values; + int heading = -1; + + if (solver_instance_) { + grid_values = solver_instance_->get_grid_values(); + heading = solver_instance_->get_current_heading(); + } + + ui_.draw(window, maze_rows_, maze_cols_, display_rows_, display_cols_, + maze_grid_, exploration_path_, solved_path_, is_generating_, + is_solving_, is_paused_, static_cast(solver_type_), + static_cast(gen_type_), anim_speed_, *maze_gen_, + mouse_pos_, zoom_factor_, camera_offset_, + grid_values, heading); +} + +auto Simulator::update_mouse_position(sf::RenderWindow &window) -> void { + mouse_pos_ = window.mapPixelToCoords(sf::Mouse::getPosition(window)); +} + +// ========================================================== +// PRIVATE METHODS +// ========================================================== + +auto Simulator::run_solver() -> void { + if (is_generating_ || is_solving_) return; + + const size_t START_NODE = 0; + const size_t END_NODE = (maze_rows_ * maze_cols_) - 1; + + // Reset UI state + solved_path_.clear(); + exploration_path_.clear(); + anim_step_idx_ = 0; + + // Instantiate Solver + switch (solver_type_) { + case SolverType::BFS: + solver_instance_ = std::make_unique(); + break; + case SolverType::DFS: + solver_instance_ = std::make_unique(); + break; + case SolverType::ASTAR: + solver_instance_ = std::make_unique(); + break; + case SolverType::FLOODFILL: + solver_instance_ = std::make_unique(); + break; + case SolverType::WALL: + solver_instance_ = std::make_unique(); + break; + } + + if (solver_instance_) { + solver_instance_->initialize(maze_grid_, maze_graph_, START_NODE, + END_NODE, maze_rows_, maze_cols_); + // FIX: Update exploration path immediately so the start node (and head) + // are visible even before the first simulation step occurs. + exploration_path_ = solver_instance_->get_visited_order(); + } + + is_solving_ = true; + is_paused_ = false; + anim_step_idx_ = 0; +} + +auto Simulator::start_generation(bool step_by_step) -> void { + is_solving_ = false; + solved_path_.clear(); + exploration_path_.clear(); + solver_instance_.reset(); + + // Select generator based on gen_type_ + if (gen_type_ == GeneratorType::RECURSIVE) { + maze_gen_ = std::make_unique(); + } else { + maze_gen_ = std::make_unique(); + } + maze_gen_->initialize(maze_rows_, maze_cols_); + + // FIX: Update grid immediately after initialization so the first state is + // visible + maze_grid_ = maze_gen_->get_grid(); + + is_generating_ = true; + is_paused_ = !step_by_step; + + if (!step_by_step) { + // Instant generation + while (maze_gen_->step()) { } + maze_grid_ = maze_gen_->get_grid(); + // Clear visited flag for instant generation too + for (auto& cell : maze_grid_) { + cell.visited = false; + } + maze_graph_ = + MazeUtils::convert_to_graph(maze_grid_, maze_rows_, maze_cols_); + is_generating_ = false; + } +} + +auto Simulator::reset_simulator() -> void { + maze_rows_ = DEFAULT_ROWS; + maze_cols_ = DEFAULT_COLS; + display_rows_ = DEFAULT_ROWS; + display_cols_ = DEFAULT_COLS; + is_generating_ = false; + is_solving_ = false; + is_paused_ = true; + zoom_factor_ = 1.0f; + camera_offset_ = {0.0f, 0.0f}; + solved_path_.clear(); + exploration_path_.clear(); + solver_instance_.reset(); + + start_generation(false); +} + +auto Simulator::handle_instant_solve_click(const sf::Vector2f &mouse_pos) + -> void { + if (ui_.get_layout().solve_inst_btn.contains(mouse_pos)) { + if (solver_instance_ && is_solving_) { + solver_instance_->solve_instant(); + exploration_path_ = solver_instance_->get_visited_order(); + solved_path_ = solver_instance_->get_path(); + is_solving_ = false; + is_paused_ = true; + } + } +} + +auto Simulator::step_solver_animation() -> void { + if (!is_solving_ || !solver_instance_) return; + + bool running = solver_instance_->step(); + exploration_path_ = solver_instance_->get_visited_order(); + + if (!running) { + is_solving_ = false; + solved_path_ = solver_instance_->get_path(); + } + is_paused_ = true; +} + +auto Simulator::toggle_wall(size_t row, size_t col, int wall_side) -> void { + if (is_generating_ || is_solving_) return; + if (row >= maze_rows_ || col >= maze_cols_) return; + + const size_t IDX = MazeUtils::get_1d_index(row, col, maze_cols_); + bool new_state = false; + + // Toggle the selected wall and its neighbor + switch (wall_side) { + case 0: // Top + maze_grid_[IDX].top = !maze_grid_[IDX].top; + new_state = maze_grid_[IDX].top; + if (row > 0) + maze_grid_[MazeUtils::get_1d_index(row - 1, col, maze_cols_)] + .bottom = new_state; + break; + case 1: // Right + maze_grid_[IDX].right = !maze_grid_[IDX].right; + new_state = maze_grid_[IDX].right; + if (col < maze_cols_ - 1) maze_grid_[IDX + 1].left = new_state; + break; + case 2: // Bottom + maze_grid_[IDX].bottom = !maze_grid_[IDX].bottom; + new_state = maze_grid_[IDX].bottom; + if (row < maze_rows_ - 1) + maze_grid_[MazeUtils::get_1d_index(row + 1, col, maze_cols_)].top = + new_state; + break; + case 3: // Left + maze_grid_[IDX].left = !maze_grid_[IDX].left; + new_state = maze_grid_[IDX].left; + if (col > 0) maze_grid_[IDX - 1].right = new_state; + break; + } + + maze_graph_ = + MazeUtils::convert_to_graph(maze_grid_, maze_rows_, maze_cols_); + + // Clear any previous paths + solved_path_.clear(); + exploration_path_.clear(); +} + +auto Simulator::save_maze() -> void { +#ifdef _WIN32 + OPENFILENAME ofn; + char szFile[260] = {0}; + + ZeroMemory(&ofn, sizeof(ofn)); + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = NULL; + ofn.lpstrFile = szFile; + ofn.nMaxFile = sizeof(szFile); + ofn.lpstrFilter = "Maze Files\0*.maze\0All Files\0*.*\0"; + ofn.nFilterIndex = 1; + ofn.lpstrFileTitle = NULL; + ofn.nMaxFileTitle = 0; + ofn.lpstrInitialDir = NULL; + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_OVERWRITEPROMPT; + + if (GetSaveFileName(&ofn) == TRUE) { + std::string path = szFile; + // Append .maze if not present + if (path.find(".maze") == std::string::npos) { + path += ".maze"; + } + + std::ofstream file(path); + if (file.is_open()) { + file << maze_rows_ << " " << maze_cols_ << "\n"; + for (const auto &cell : maze_grid_) { + int mask = 0; + if (cell.top) mask |= 1; + if (cell.right) mask |= 2; + if (cell.bottom) mask |= 4; + if (cell.left) mask |= 8; + file << mask << " "; + } + std::cout << "Maze saved to: " << path << "\n"; + } + } +#else + std::cerr << "Save dialog only supported on Windows.\n"; +#endif +} + +auto Simulator::load_maze() -> void { +#ifdef _WIN32 + OPENFILENAME ofn; + char szFile[260] = {0}; + + ZeroMemory(&ofn, sizeof(ofn)); + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = NULL; + ofn.lpstrFile = szFile; + ofn.nMaxFile = sizeof(szFile); + ofn.lpstrFilter = "Maze Files\0*.maze\0All Files\0*.*\0"; + ofn.nFilterIndex = 1; + ofn.lpstrFileTitle = NULL; + ofn.nMaxFileTitle = 0; + ofn.lpstrInitialDir = NULL; + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST; + + if (GetOpenFileName(&ofn) == TRUE) { + std::ifstream file(szFile); + if (file.is_open()) { + size_t r, c; + if (file >> r >> c) { + maze_rows_ = r; + maze_cols_ = c; + display_rows_ = r; + display_cols_ = c; + maze_grid_.clear(); + maze_grid_.resize(r * c); + + for (size_t i = 0; i < r * c; ++i) { + int mask; + if (file >> mask) { + maze_grid_[i].top = (mask & 1); + maze_grid_[i].right = (mask & 2); + maze_grid_[i].bottom = (mask & 4); + maze_grid_[i].left = (mask & 8); + maze_grid_[i].visited = false; + } + } + + maze_graph_ = MazeUtils::convert_to_graph( + maze_grid_, maze_rows_, maze_cols_); + is_solving_ = false; + solved_path_.clear(); + exploration_path_.clear(); + solver_instance_.reset(); + + std::cout << "Maze loaded from: " << szFile << "\n"; + } + } + } +#else + std::cerr << "Load dialog only supported on Windows.\n"; +#endif +} diff --git a/src/model/MazeUtils.cpp b/src/model/MazeUtils.cpp new file mode 100644 index 0000000..c9dbd7e --- /dev/null +++ b/src/model/MazeUtils.cpp @@ -0,0 +1,37 @@ +/** + * @file MazeUtils.cpp + * @brief Implementation of utility functions for maze operations. + */ + +#include "MazeUtils.hpp" + +auto MazeUtils::convert_to_graph(const std::vector& grid, size_t rows, size_t columns) -> Graph { + Graph graph(rows * columns); + + for (size_t row_idx = 0; row_idx < rows; ++row_idx) { + for (size_t col_idx = 0; col_idx < columns; ++col_idx) { + const size_t U_IDX = get_1d_index(row_idx, col_idx, columns); + const auto& cell = grid[U_IDX]; + + // Add edges if no wall exists between cells. + if (!cell.top && row_idx > 0) { + const size_t V_IDX = get_1d_index(row_idx - 1, col_idx, columns); + graph[U_IDX].push_back(V_IDX); + } + if (!cell.bottom && row_idx < rows - 1) { + const size_t V_IDX = get_1d_index(row_idx + 1, col_idx, columns); + graph[U_IDX].push_back(V_IDX); + } + if (!cell.left && col_idx > 0) { + const size_t V_IDX = get_1d_index(row_idx, col_idx - 1, columns); + graph[U_IDX].push_back(V_IDX); + } + if (!cell.right && col_idx < columns - 1) { + const size_t V_IDX = get_1d_index(row_idx, col_idx + 1, columns); + graph[U_IDX].push_back(V_IDX); + } + } + } + return graph; +} + diff --git a/src/model/generators/EllersGenerator.cpp b/src/model/generators/EllersGenerator.cpp new file mode 100644 index 0000000..5c5a3cd --- /dev/null +++ b/src/model/generators/EllersGenerator.cpp @@ -0,0 +1,144 @@ +/** + * @file EllersGenerator.cpp + * @brief Implementation of Eller's maze generation algorithm. + */ + +#include "generators/EllersGenerator.hpp" +#include +#include + + +void EllersGenerator::initialize(size_t rows, size_t cols) { + rows_ = rows; + cols_ = cols; + grid_.assign(rows * cols, Cell()); + sets_.assign(cols, 0); // 0 means unassigned + next_set_id_ = 1; + current_row_ = 0; + done_ = (rows == 0 || cols == 0); + initialized_ = true; + state_ = State::HORIZONTAL; + rng_ = std::mt19937(std::random_device{}()); + current_row_cells_.clear(); +} + +auto EllersGenerator::step() -> bool { + if (done_ || !initialized_) return false; + + current_row_cells_.clear(); + for (size_t c = 0; c < cols_; ++c) { + current_row_cells_.emplace_back(static_cast(current_row_), static_cast(c)); + } + + if (current_row_ == rows_ - 1) { + assign_sets(); + finalize_last_row(); + done_ = true; + + // Ensure starting and ending points are open + grid_[MazeUtils::get_1d_index(0, 0, cols_)].top = false; + grid_[MazeUtils::get_1d_index(rows_ - 1, cols_ - 1, cols_)].bottom = false; + + return false; + } + + assign_sets(); + horizontal_connections(); + vertical_connections(); + prepare_next_row(); + + current_row_++; + return true; +} + +void EllersGenerator::assign_sets() { + for (size_t c = 0; c < cols_; ++c) { + if (sets_[c] == 0) { + sets_[c] = next_set_id_++; + } + } +} + +void EllersGenerator::horizontal_connections() { + std::uniform_int_distribution dist(0, 1); + for (size_t c = 0; c < cols_ - 1; ++c) { + // If they are in the same set, we MUST NOT join them (would create loop) + // If they are in different sets, we randomly join them + if (sets_[c] != sets_[c + 1]) { + if (dist(rng_) == 1) { + // Join them + int old_set = sets_[c + 1]; + int new_set = sets_[c]; + for (size_t i = 0; i < cols_; ++i) { + if (sets_[i] == old_set) sets_[i] = new_set; + } + + size_t idx1 = MazeUtils::get_1d_index(current_row_, c, cols_); + size_t idx2 = MazeUtils::get_1d_index(current_row_, c + 1, cols_); + grid_[idx1].right = false; + grid_[idx2].left = false; + } + } + } +} + +void EllersGenerator::vertical_connections() { + std::uniform_int_distribution dist(0, 1); + + // Group columns by their set IDs + std::map> set_members; + for (size_t c = 0; c < cols_; ++c) { + set_members[sets_[c]].push_back(c); + } + + std::vector next_row_sets(cols_, 0); + + for (auto const& [set_id, members] : set_members) { + // Each set must have at least one vertical connection + // We ensure this by picking at least one member randomly + std::vector shuffled_members = members; + std::shuffle(shuffled_members.begin(), shuffled_members.end(), rng_); + + bool any_connection = false; + for (size_t c : shuffled_members) { + // Randomly decide to create a vertical connection + // But if it's the last member and no connection has been made, we MUST make it. + if (dist(rng_) == 1 || (!any_connection && c == shuffled_members.back())) { + any_connection = true; + + size_t idx_curr = MazeUtils::get_1d_index(current_row_, c, cols_); + size_t idx_next = MazeUtils::get_1d_index(current_row_ + 1, c, cols_); + + grid_[idx_curr].bottom = false; + grid_[idx_next].top = false; + next_row_sets[c] = set_id; + } + } + } + + sets_ = next_row_sets; +} + +void EllersGenerator::finalize_last_row() { + for (size_t c = 0; c < cols_ - 1; ++c) { + if (sets_[c] != sets_[c + 1]) { + // Join them + int old_set = sets_[c + 1]; + int new_set = sets_[c]; + for (size_t i = 0; i < cols_; ++i) { + if (sets_[i] == old_set) sets_[i] = new_set; + } + + size_t idx1 = MazeUtils::get_1d_index(current_row_, c, cols_); + size_t idx2 = MazeUtils::get_1d_index(current_row_, c + 1, cols_); + grid_[idx1].right = false; + grid_[idx2].left = false; + } + } +} + +void EllersGenerator::prepare_next_row() { + // This is handled at the end of vertical_connections() by updating sets_ + // Unassigned cells in the next row will be given new set IDs by assign_sets() in the next step. +} + diff --git a/src/model/generators/RecursiveBacktracker.cpp b/src/model/generators/RecursiveBacktracker.cpp new file mode 100644 index 0000000..73727a7 --- /dev/null +++ b/src/model/generators/RecursiveBacktracker.cpp @@ -0,0 +1,205 @@ +/** + * @file RecursiveBacktracker.cpp + * @brief Implementation of Recursive Backtracking maze generation algorithm. + */ + +#include "generators/RecursiveBacktracker.hpp" +#include +#include +#include + +auto RecursiveBacktracker::initialize(size_t rows, size_t columns) -> void { + rows_ = rows; + cols_ = columns; + grid_.clear(); + grid_.resize(rows * columns); + + // Reset cells + std::fill(grid_.begin(), grid_.end(), Cell()); + + // Reset path stack + path_stack_.clear(); + + // Reset RNG + rng_ = std::mt19937(std::random_device{}()); + + if (rows > 0 && columns > 0) { + // Start position: top-left cell (0, 0) + const size_t START_IDX = MazeUtils::get_1d_index(0, 0, cols_); + grid_[START_IDX].visited = true; + path_stack_.emplace_back(0, 0); + done_ = false; + initialized_ = true; + } else { + done_ = true; + initialized_ = false; + } +} + +auto RecursiveBacktracker::step() -> bool { + if (done_ || !initialized_ || path_stack_.empty()) { + if (!done_ && initialized_ && path_stack_.empty()) { + // Just finished processing. Finalize entrance/exit. + // Create entrance at (0,0) and exit at (rows-1, columns-1) + const size_t START_IDX = MazeUtils::get_1d_index(0, 0, cols_); + const size_t END_IDX = + MazeUtils::get_1d_index(rows_ - 1, cols_ - 1, cols_); + grid_[START_IDX].top = false; + grid_[END_IDX].bottom = false; + done_ = true; + return false; + } + done_ = true; + return false; + } + + // Get current cell from end of path + const int ROW = path_stack_.back().first; + const int COL = path_stack_.back().second; + + // Get all unvisited neighbors + auto neighbors = get_neighbors(ROW, COL); + + if (neighbors.empty()) { + path_stack_.pop_back(); // Backtrack + } else { + // Select a random unvisited neighbor + std::uniform_int_distribution dist(0, neighbors.size() - 1); + const size_t RANDOM_INDEX = dist(rng_); + + // Get coordinates of the chosen random neighbor + const int NROW = neighbors[RANDOM_INDEX].first; + const int NCOL = neighbors[RANDOM_INDEX].second; + + // Remove the wall between the current cell and the chosen neighbor + remove_walls(ROW, COL, NROW, NCOL); + + // Mark the chosen neighbor as visited and push it onto the path + const size_t N_IDX = MazeUtils::get_1d_index( + static_cast(NROW), static_cast(NCOL), cols_); + grid_[N_IDX].visited = true; + path_stack_.emplace_back(NROW, NCOL); //push on to the "stack" + } + + // Check if finished after this step + if (path_stack_.empty()) { + // Finalize immediately + const size_t START_IDX = MazeUtils::get_1d_index(0, 0, cols_); + const size_t END_IDX = + MazeUtils::get_1d_index(rows_ - 1, cols_ - 1, cols_); + + grid_[START_IDX].top = false; + grid_[END_IDX].bottom = false; + + done_ = true; + return false; // Done (base condition of recursion) + } + + return true; // Still running +} + +auto RecursiveBacktracker::is_done() const -> bool { return done_; } + +auto RecursiveBacktracker::get_neighbors(int row, int col) const + -> std::vector> { + std::vector> neighbors; + + // Check if top neighbor is visited + if (row > 0 && + !grid_[MazeUtils::get_1d_index(static_cast(row) - 1, + static_cast(col), cols_)] + .visited) { + neighbors.emplace_back(row - 1, col); + } + + // Check if bottom neighbor is visited + if (row + 1 < static_cast(rows_) && + !grid_[MazeUtils::get_1d_index(static_cast(row) + 1, + static_cast(col), cols_)] + .visited) { + neighbors.emplace_back(row + 1, col); + } + + // Check if left neighbor is visited + if (col > 0 && + !grid_[MazeUtils::get_1d_index(static_cast(row), + static_cast(col) - 1, cols_)] + .visited) { + neighbors.emplace_back(row, col - 1); + } + + // Check if right neighbor is visited + if (col + 1 < static_cast(cols_) && + !grid_[MazeUtils::get_1d_index(static_cast(row), + static_cast(col) + 1, cols_)] + .visited) { + neighbors.emplace_back(row, col + 1); + } + + return neighbors; +} + + +auto RecursiveBacktracker::remove_walls(int row, int col, int nrow, int ncol) -> void { + const size_t CURR_IDX = MazeUtils::get_1d_index( + static_cast(row), static_cast(col), cols_); + const size_t NEXT_IDX = MazeUtils::get_1d_index( + static_cast(nrow), static_cast(ncol), cols_); + + if (nrow == row - 1) { // Up + grid_[CURR_IDX].top = false; + grid_[NEXT_IDX].bottom = false; + } else if (nrow == row + 1) { // Down + grid_[CURR_IDX].bottom = false; + grid_[NEXT_IDX].top = false; + } else if (ncol == col - 1) { // Left + grid_[CURR_IDX].left = false; + grid_[NEXT_IDX].right = false; + } else if (ncol == col + 1) { // Right + grid_[CURR_IDX].right = false; + grid_[NEXT_IDX].left = false; + } +} + +auto RecursiveBacktracker::generate(size_t rows, size_t columns) -> std::vector { + RecursiveBacktracker gen; + gen.initialize(rows, columns); + while (gen.step()) { + // Loop until done + } + return gen.get_grid(); +} + +auto RecursiveBacktracker::render_ascii(const std::vector &grid, size_t rows, + size_t columns) -> std::string { + std::ostringstream out; + + for (size_t r_idx = 0; r_idx < rows; ++r_idx) { + for (size_t c_idx = 0; c_idx < columns; ++c_idx) { + out << "+" + << (grid[MazeUtils::get_1d_index(r_idx, c_idx, columns)].top + ? "---" + : " "); + } + out << "+\n"; + + for (size_t c_idx = 0; c_idx < columns; ++c_idx) { + out << (grid[MazeUtils::get_1d_index(r_idx, c_idx, columns)].left + ? "|" + : " "); + out << " "; + } + out << "|\n"; + } + + for (size_t c_idx = 0; c_idx < columns; ++c_idx) { + out << "+" + << (grid[MazeUtils::get_1d_index(rows - 1, c_idx, columns)].bottom + ? "---" + : " "); + } + out << "+\n"; + + return out.str(); +} + diff --git a/src/model/solvers/AStarSolver.cpp b/src/model/solvers/AStarSolver.cpp new file mode 100644 index 0000000..09606c2 --- /dev/null +++ b/src/model/solvers/AStarSolver.cpp @@ -0,0 +1,109 @@ +/** + * @file AStarSolver.cpp + * @brief Implementation of the A* pathfinding algorithm for maze solving. + */ + +#include "solvers/AStarSolver.hpp" +#include "MazeUtils.hpp" +#include +#include + +void AStarSolver::initialize(const std::vector& /*grid*/, + const Graph& graph, + size_t start, + size_t end, + size_t rows, + size_t cols) { + graph_ = &graph; + start_node_ = start; + end_node_ = end; + rows_ = rows; + cols_ = cols; + total_nodes_ = rows * cols; + + while(!priority_q_.empty()) priority_q_.pop(); + + g_score_.assign(total_nodes_, std::numeric_limits::infinity()); + predecessors_.assign(total_nodes_, static_cast(-1)); + visited_order_.clear(); + + solved_ = false; + finished_ = false; + + if (total_nodes_ > 0) { + // Heuristic function: Manhattan distance + auto heuristic = [&](size_t n) -> double { + auto [r1, c1] = MazeUtils::get_2d_coords(n, cols_); + auto [r2, c2] = MazeUtils::get_2d_coords(end_node_, cols_); + return static_cast( + std::abs(static_cast(r1) - static_cast(r2)) + + std::abs(static_cast(c1) - static_cast(c2))); + }; + + g_score_[start_node_] = 0.0; + priority_q_.emplace(heuristic(start_node_), start_node_); + visited_order_.push_back(start_node_); + } else { + finished_ = true; + } +} + +bool AStarSolver::step() { + if (finished_) return false; + if (priority_q_.empty()) { + finished_ = true; + return false; + } + + const size_t U_NODE = priority_q_.top().second; + priority_q_.pop(); + + visited_order_.push_back(U_NODE); + + if (U_NODE == end_node_) { + solved_ = true; + finished_ = true; + return false; + } + + auto heuristic = [&](size_t n) -> double { + auto [r1, c1] = MazeUtils::get_2d_coords(n, cols_); + auto [r2, c2] = MazeUtils::get_2d_coords(end_node_, cols_); + return static_cast( + std::abs(static_cast(r1) - static_cast(r2)) + + std::abs(static_cast(c1) - static_cast(c2))); + }; + + for (const size_t V_NODE : (*graph_)[U_NODE]) { + const double TENTATIVE_G = g_score_[U_NODE] + 1.0; + if (TENTATIVE_G < g_score_[V_NODE]) { + predecessors_[V_NODE] = U_NODE; + g_score_[V_NODE] = TENTATIVE_G; + priority_q_.emplace(TENTATIVE_G + heuristic(V_NODE), V_NODE); + } + } + + return true; +} + +std::vector AStarSolver::get_path() const { + if (!solved_) return {}; + + std::vector path; + for (size_t curr = end_node_; curr != static_cast(-1); + curr = predecessors_[curr]) { + path.push_back(curr); + } + std::reverse(path.begin(), path.end()); + return path; +} + +std::vector AStarSolver::get_visited_order() const { + return visited_order_; +} + +bool AStarSolver::is_solved() const { + return solved_; +} + + diff --git a/src/model/solvers/BFSSolver.cpp b/src/model/solvers/BFSSolver.cpp new file mode 100644 index 0000000..22ee03c --- /dev/null +++ b/src/model/solvers/BFSSolver.cpp @@ -0,0 +1,94 @@ +/** + * @file BFSSolver.cpp + * @brief Implementation of the Breadth-First Search (BFS) algorithm for maze solving. + */ + +#include "solvers/BFSSolver.hpp" +#include + +void BFSSolver::initialize(const std::vector& /*grid*/, + const Graph& graph, + size_t start, + size_t end, + size_t rows, + size_t cols) { + graph_ = &graph; + start_node_ = start; + end_node_ = end; + total_nodes_ = rows * cols; + + // Reset state + std::queue empty; + std::swap(work_queue_, empty); + visited_.assign(total_nodes_, false); + predecessors_.assign(total_nodes_, static_cast(-1)); + visited_order_.clear(); + solved_ = false; + finished_ = false; + + // Initial setup + if (total_nodes_ > 0) { + visited_[start_node_] = true; + work_queue_.push(start_node_); + visited_order_.push_back(start_node_); + + // Edge case: start == end + if (start_node_ == end_node_) { + solved_ = true; + finished_ = true; + } + } else { + finished_ = true; + } +} + +bool BFSSolver::step() { + if (finished_) return false; + if (work_queue_.empty()) { + finished_ = true; + return false; + } + + // Process one node from the queue + const size_t U_NODE = work_queue_.front(); + work_queue_.pop(); + + if (U_NODE == end_node_) { + solved_ = true; + finished_ = true; + return false; + } + + for (const size_t V_NODE : (*graph_)[U_NODE]) { + if (!visited_[V_NODE]) { + visited_[V_NODE] = true; + predecessors_[V_NODE] = U_NODE; + work_queue_.push(V_NODE); + visited_order_.push_back(V_NODE); + } + } + + return true; // Still have work to do +} + +std::vector BFSSolver::get_path() const { + if (!solved_) return {}; + + std::vector path; + for (size_t curr = end_node_; curr != static_cast(-1); + curr = predecessors_[curr]) { + path.push_back(curr); + } + std::reverse(path.begin(), path.end()); + return path; +} + +std::vector BFSSolver::get_visited_order() const { + return visited_order_; +} + +bool BFSSolver::is_solved() const { + return solved_; +} + + diff --git a/src/model/solvers/DFSSolver.cpp b/src/model/solvers/DFSSolver.cpp new file mode 100644 index 0000000..3814537 --- /dev/null +++ b/src/model/solvers/DFSSolver.cpp @@ -0,0 +1,103 @@ +/** + * @file DFSSolver.cpp + * @brief Implementation of the Depth-First Search (DFS) algorithm for maze solving. + */ + +#include "solvers/DFSSolver.hpp" +#include + +void DFSSolver::initialize(const std::vector& /*grid*/, + const Graph& graph, + size_t start, + size_t end, + size_t rows, + size_t cols) { + graph_ = &graph; + start_node_ = start; + end_node_ = end; + total_nodes_ = rows * cols; + + while(!work_stack_.empty()) work_stack_.pop(); + visited_.assign(total_nodes_, false); + predecessors_.assign(total_nodes_, static_cast(-1)); + visited_order_.clear(); + solved_ = false; + finished_ = false; + + if (total_nodes_ > 0) { + work_stack_.push(start_node_); + visited_[start_node_] = true; + visited_order_.push_back(start_node_); + + if (start_node_ == end_node_) { + solved_ = true; + finished_ = true; + } + } else { + finished_ = true; + } +} + +bool DFSSolver::step() { + if (finished_) return false; + if (work_stack_.empty()) { + finished_ = true; + return false; + } + + // DFS processes the top of stack + const size_t CURRENT_NODE = work_stack_.top(); + + if (CURRENT_NODE == end_node_) { + solved_ = true; + finished_ = true; + return false; + } + + // Find one unvisited neighbor + bool found_unvisited_neighbor = false; + for (const size_t NEIGHBOR : (*graph_)[CURRENT_NODE]) { + if (!visited_[NEIGHBOR]) { + visited_[NEIGHBOR] = true; + predecessors_[NEIGHBOR] = CURRENT_NODE; + work_stack_.push(NEIGHBOR); + visited_order_.push_back(NEIGHBOR); + found_unvisited_neighbor = true; + break; // Depth first: explore this immediately + } + } + + if (!found_unvisited_neighbor) { + work_stack_.pop(); // Backtrack + } + + return true; +} + +std::vector DFSSolver::get_path() const { + if (!solved_) return {}; + + // Standard path reconstruction logic + std::vector path; + size_t current_path_node = end_node_; + + // Safety check for loops or disconnected components (though predecessors shouldn't have loops) + size_t safety_ctr = 0; + while (current_path_node != static_cast(-1) && safety_ctr++ < total_nodes_) { + path.push_back(current_path_node); + if (predecessors_[current_path_node] == current_path_node) break; + current_path_node = predecessors_[current_path_node]; + } + std::reverse(path.begin(), path.end()); + return path; +} + +std::vector DFSSolver::get_visited_order() const { + return visited_order_; +} + +bool DFSSolver::is_solved() const { + return solved_; +} + + diff --git a/src/model/solvers/FloodFillSolver.cpp b/src/model/solvers/FloodFillSolver.cpp new file mode 100644 index 0000000..966927b --- /dev/null +++ b/src/model/solvers/FloodFillSolver.cpp @@ -0,0 +1,108 @@ +/** + * @file FloodFillSolver.cpp + * @brief Implementation of the Flood Fill algorithm for maze solving. + */ + +#include "solvers/FloodFillSolver.hpp" + +#include + +void FloodFillSolver::initialize(const std::vector& grid, + const Graph& graph, + size_t start, + size_t end, + size_t rows, + size_t cols) { + grid_ = &grid; + graph_ = &graph; + start_node_ = start; + end_node_ = end; + rows_ = rows; + cols_ = cols; + total_nodes_ = rows * cols; + + std::queue empty; + std::swap(work_queue_, empty); + distances_.assign(total_nodes_, static_cast(total_nodes_)); + visited_order_.clear(); + solved_ = false; + finished_ = false; + flooding_phase_ = true; + + if (total_nodes_ > 0) { + // Start flood from END node + distances_[end_node_] = 0; + work_queue_.push(end_node_); + // visited_order_.push_back(end_node_); // Optional here, sticking to push-on-pop or push-on-push + } else { + finished_ = true; + } +} + +bool FloodFillSolver::step() { + if (finished_) return false; + + if (work_queue_.empty()) { + finished_ = true; + // Check if start is reachable + if (distances_[start_node_] != static_cast(total_nodes_)) { + solved_ = true; + } + return false; + } + + const size_t U_NODE = work_queue_.front(); + work_queue_.pop(); + visited_order_.push_back(U_NODE); + + // Flood to neighbors + for (const size_t V_NODE : (*graph_)[U_NODE]) { + if (distances_[V_NODE] > distances_[U_NODE] + 1) { + distances_[V_NODE] = distances_[U_NODE] + 1; + work_queue_.push(V_NODE); + } + } + + // Optimization: If we reached start node, we technically "found" the path. + // visualizing the full flood is better though. + + return true; +} + +std::vector FloodFillSolver::get_path() const { + if (!solved_ && distances_[start_node_] == static_cast(total_nodes_)) return {}; + + // Trace path from start to end by following decreasing distances + // Note: If flood isn't complete but reached start, we can still trace. + if (distances_[start_node_] == static_cast(total_nodes_)) return {}; + + std::vector path; + size_t curr = start_node_; + path.push_back(curr); + + size_t safety = 0; + while (curr != end_node_ && safety++ < total_nodes_) { + size_t next_v = curr; + int min_d = distances_[curr]; + + for (const size_t V_NODE : (*graph_)[curr]) { + if (distances_[V_NODE] < min_d) { + min_d = distances_[V_NODE]; + next_v = V_NODE; + } + } + + if (next_v == curr) break; // Stuck? + curr = next_v; + path.push_back(curr); + } + return path; +} + +std::vector FloodFillSolver::get_visited_order() const { + return visited_order_; +} + +bool FloodFillSolver::is_solved() const { + return solved_ || (distances_[start_node_] != static_cast(total_nodes_)); +} diff --git a/src/model/solvers/WallFollowerSolver.cpp b/src/model/solvers/WallFollowerSolver.cpp new file mode 100644 index 0000000..f4f16c7 --- /dev/null +++ b/src/model/solvers/WallFollowerSolver.cpp @@ -0,0 +1,153 @@ +/** + * @file WallFollowerSolver.cpp + * @brief Implementation of the Wall Follower algorithm for maze solving. + */ + +#include "solvers/WallFollowerSolver.hpp" + +#include "MazeUtils.hpp" + +void WallFollowerSolver::initialize(const std::vector& grid, + const Graph& /*graph*/, + size_t start, + size_t end, + size_t rows, + size_t cols) { + grid_ = &grid; + start_node_ = start; + end_node_ = end; + rows_ = rows; + cols_ = cols; + + current_node_ = start; + facing_ = 1; // Default to Right? Or heuristic based on start pos? + // If we are at 0,0 (Top Left), facing Right is reasonable. + + path_.clear(); + visited_order_.clear(); + solved_ = false; + finished_ = false; + visited_states_.assign(rows * cols * 4, false); + + if (rows * cols > 0) { + path_.push_back(start); + visited_order_.push_back(start); + + if (start == end) { + solved_ = true; + finished_ = true; + } + } else { + finished_ = true; + } +} + +bool WallFollowerSolver::step() { + if (finished_) return false; + if (current_node_ == end_node_) { + solved_ = true; + finished_ = true; + return false; + } + + // Cycle detection + size_t state_idx = current_node_ * 4 + facing_; + if (visited_states_[state_idx]) { + finished_ = true; // Loop detected + return false; + } + visited_states_[state_idx] = true; + + // Direction vectors for 0:Up, 1:Right, 2:Down, 3:Left + // Row, Col deltas + const int DR[] = {-1, 0, 1, 0}; + const int DC[] = {0, 1, 0, -1}; + + // Helper to get next node in a direction + auto get_next = [&](int dir) -> size_t { + auto [r, c] = MazeUtils::get_2d_coords(current_node_, cols_); + int nr = static_cast(r) + DR[dir]; + int nc = static_cast(c) + DC[dir]; + // Bounds check (though walls should prevent OOB) + if (nr >= 0 && nr < (int)rows_ && nc >= 0 && nc < (int)cols_) { + return MazeUtils::get_1d_index(nr, nc, cols_); + } + return current_node_; + }; + + // Walls: 0=Top, 1=Right, 2=Bottom, 3=Left + const auto& c = (*grid_)[current_node_]; + bool walls[4]; + walls[0] = c.top; + walls[1] = c.right; + walls[2] = c.bottom; + walls[3] = c.left; + + // Helper to check if a direction is truly open (no wall AND in bounds) + auto is_open = [&](int dir) -> bool { + // 1. Check Maze Walls + if (walls[dir]) return false; + + // 2. Check Map Boundaries explicitly + // If we are at the edge and facing out, it's blocked even if the cell says "no wall" + auto [r, c] = MazeUtils::get_2d_coords(current_node_, cols_); + if (dir == 0 && r == 0) return false; // Up + if (dir == 1 && c == cols_ - 1) return false; // Right + if (dir == 2 && r == rows_ - 1) return false; // Down + if (dir == 3 && c == 0) return false; // Left + + return true; + }; + + // Left Hand Rule + // 1. Check relative Left + int left_dir = (facing_ + 3) % 4; + + if (is_open(left_dir)) { + // Prepare to move Left + facing_ = left_dir; + current_node_ = get_next(facing_); + path_.push_back(current_node_); + visited_order_.push_back(current_node_); + return true; + } + + // 2. Check Forward + if (is_open(facing_)) { + // Move Forward + current_node_ = get_next(facing_); + path_.push_back(current_node_); + visited_order_.push_back(current_node_); + return true; + } + + // 3. Turn Right (stay in cell) + facing_ = (facing_ + 1) % 4; + // We don't advance physical location, but we performed a step. + // Ideally we should move in the same step if possible to avoid sticking? + // But strict wall following allows turning. + + return true; +} + +void WallFollowerSolver::solve_instant() { + size_t limit = 10000; // Reduced limit, cycle detection handles most cases anyway + while(step() && limit > 0) { + limit--; + } + if (limit == 0 && !solved_) { + finished_ = true; + } +} + +std::vector WallFollowerSolver::get_path() const { + return path_; +} + +std::vector WallFollowerSolver::get_visited_order() const { + return visited_order_; +} + +bool WallFollowerSolver::is_solved() const { + return solved_; +} diff --git a/src/view/UI.cpp b/src/view/UI.cpp new file mode 100644 index 0000000..1b0c0b5 --- /dev/null +++ b/src/view/UI.cpp @@ -0,0 +1,638 @@ +/** + * @file UI.cpp + * @brief Implementation of the UI class for the MicroMouse simulator. + */ + +#include "UI.hpp" +#include "MazeUtils.hpp" +#include +#include +#include +#include +#include + + +UI::UI() : row_input_buffer_("16"), col_input_buffer_("16"), speed_input_buffer_("100") {} + +auto UI::load_resources() -> bool { + const std::vector K_FONT_PATHS = { + "assets/fonts/PixelOperatorMono-Bold.ttf", + "../assets/fonts/PixelOperatorMono-Bold.ttf", + "assets/fonts/Pixel-Regular.ttf", + "../assets/fonts/Pixel-Regular.ttf", + "assets/fonts/pixel.ttf", + "../assets/fonts/pixel.ttf", + "pixel.ttf", + // Fallback: OpenSans if available (found in project root) + "OpenSans-Regular.ttf", + "assets/fonts/OpenSans-Regular.ttf", + // System fallback (Monospace preferred for pixel-like aesthetic) + "C:/Windows/Fonts/lucon.ttf", + "C:/Windows/Fonts/cour.ttf" + }; + + if (std::any_of( + K_FONT_PATHS.begin(), K_FONT_PATHS.end(), + [this](const auto &path) { return font_.loadFromFile(path); })) { + // Attempt to disable smoothing for the font texture (affects all sizes usually) + // Note: SFML generates textures on the beam, so this is best-effort. + return true; + } + + return false; +} + +auto UI::handle_event(sf::RenderWindow &window, const sf::Event &event, + size_t &maze_rows, size_t &maze_cols, + size_t &display_rows, size_t &display_cols, + bool & /*is_generating*/, bool & /*is_solving*/, + bool &is_paused, int &solver_type, int &gen_type, int &animation_speed, + const std::function &start_gen, + const std::function &start_sol, + const std::function &step_fn, + const std::function &reset_fn, + const std::function &toggle_wall_fn, + const std::function &save_fn, + const std::function &load_fn, + float &zoom_factor, sf::Vector2f &camera_offset) -> void { + + const sf::Vector2f MPOS = + window.mapPixelToCoords(sf::Mouse::getPosition(window)); + const auto &l = layout_; + + static constexpr size_t K_MIN_MAZE_SIZE = 5; + static constexpr size_t K_MAX_MAZE_SIZE = 100; + static constexpr int K_SPEED_STEP = 5; + static constexpr int K_MIN_ANIM_SPEED = 1; + static constexpr int K_MAX_ANIM_SPEED = 200; + + static bool is_panning = false; + static sf::Vector2f last_mouse_pos; + + // Handle Zoom + if (event.type == sf::Event::MouseWheelScrolled && event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel) { + float delta = event.mouseWheelScroll.delta; + zoom_factor = std::clamp(zoom_factor + delta * 0.1f, 0.5f, 5.0f); + } + + // Handle Panning + if (event.type == sf::Event::MouseButtonPressed && event.mouseButton.button == sf::Mouse::Middle) { + is_panning = true; + last_mouse_pos = MPOS; + } else if (event.type == sf::Event::MouseButtonReleased && event.mouseButton.button == sf::Mouse::Middle) { + is_panning = false; + } else if (event.type == sf::Event::MouseMoved && is_panning) { + sf::Vector2f delta = MPOS - last_mouse_pos; + camera_offset += delta; + last_mouse_pos = MPOS; + } + + // Synchronize buffers if not focused (e.g. after reset or increment/decrement if we still had those) + if (!row_focused_) row_input_buffer_ = std::to_string(display_rows); + if (!col_focused_) col_input_buffer_ = std::to_string(display_cols); + if (!speed_focused_) speed_input_buffer_ = std::to_string(animation_speed); + + auto commit_dimensions = [&]() { + try { + if (!row_input_buffer_.empty()) display_rows = std::stoul(row_input_buffer_); + if (!col_input_buffer_.empty()) display_cols = std::stoul(col_input_buffer_); + } catch (...) {} + + display_rows = std::clamp(display_rows, K_MIN_MAZE_SIZE, K_MAX_MAZE_SIZE); + display_cols = std::clamp(display_cols, K_MIN_MAZE_SIZE, K_MAX_MAZE_SIZE); + + row_input_buffer_ = std::to_string(display_rows); + col_input_buffer_ = std::to_string(display_cols); + row_focused_ = false; + col_focused_ = false; + + maze_rows = display_rows; + maze_cols = display_cols; + start_gen(false); + }; + + auto commit_speed = [&]() { + try { + if (!speed_input_buffer_.empty()) animation_speed = std::stoi(speed_input_buffer_); + } catch (...) {} + animation_speed = std::clamp(animation_speed, K_MIN_ANIM_SPEED, K_MAX_ANIM_SPEED); + speed_input_buffer_ = std::to_string(animation_speed); + speed_focused_ = false; + }; + + // Handle clicks for focus and buttons + if (event.type == sf::Event::MouseButtonPressed) { + const bool CLICKED_ROW = l.row_input_box.contains(MPOS); + const bool CLICKED_COL = l.col_input_box.contains(MPOS); + const bool CLICKED_SPEED = l.speed_input_box.contains(MPOS); + + // Commit speed if focus is lost + if (speed_focused_ && !CLICKED_SPEED) { + commit_speed(); + } + + row_focused_ = CLICKED_ROW; + col_focused_ = CLICKED_COL; + speed_focused_ = CLICKED_SPEED; + + if (l.apply_btn.contains(MPOS)) { + commit_dimensions(); + } else if (l.gen_inst_btn.contains(MPOS)) start_gen(false); + else if (l.gen_step_btn.contains(MPOS)) start_gen(true); + else if (l.recursive_gen_btn.contains(MPOS)) gen_type = 0; + else if (l.ellers_gen_btn.contains(MPOS)) gen_type = 1; + else if (l.bfs_btn.contains(MPOS)) solver_type = 0; + else if (l.dfs_btn.contains(MPOS)) solver_type = 1; + else if (l.astar_btn.contains(MPOS)) solver_type = 2; + else if (l.flood_btn.contains(MPOS)) solver_type = 3; + else if (l.wall_btn.contains(MPOS)) solver_type = 4; + else if (l.solve_btn.contains(MPOS)) start_sol(); + else if (l.solve_inst_btn.contains(MPOS)) start_sol(); + else if (l.play_btn.contains(MPOS)) is_paused = !is_paused; + else if (l.step_btn.contains(MPOS)) { is_paused = true; step_fn(); } + else if (l.reset_btn.contains(MPOS)) reset_fn(); + else if (l.save_btn.contains(MPOS)) save_fn(); + else if (l.load_btn.contains(MPOS)) load_fn(); + else { + // Check if clicked in maze area + const float WINDOW_WIDTH = static_cast(window.getSize().x); + const float WINDOW_HEIGHT = static_cast(window.getSize().y); + const float SIDEBAR_WIDTH = std::clamp(WINDOW_WIDTH * 0.2f, 200.0f, 350.0f); + const float AVAILABLE_WIDTH = WINDOW_WIDTH - SIDEBAR_WIDTH - 40.0f; + const float AVAILABLE_HEIGHT = WINDOW_HEIGHT - 40.0f; + + const float BASE_CELL_SIZE_W = AVAILABLE_WIDTH / static_cast(maze_cols); + const float BASE_CELL_SIZE_H = AVAILABLE_HEIGHT / static_cast(maze_rows); + const float BASE_CELL_SIZE = std::min({BASE_CELL_SIZE_W, BASE_CELL_SIZE_H, 40.0f}); + + const float CELL_SIZE = BASE_CELL_SIZE * zoom_factor; + const float MAZE_W = static_cast(maze_cols) * CELL_SIZE; + const float MAZE_H = static_cast(maze_rows) * CELL_SIZE; + const float OFFSET_X = SIDEBAR_WIDTH + (WINDOW_WIDTH - SIDEBAR_WIDTH - MAZE_W) / 2.0F + camera_offset.x; + const float OFFSET_Y = (WINDOW_HEIGHT - MAZE_H) / 2.0F + camera_offset.y; + + float rel_x = MPOS.x - OFFSET_X; + float rel_y = MPOS.y - OFFSET_Y; + + if (rel_x >= 0 && rel_x < MAZE_W && rel_y >= 0 && rel_y < MAZE_H) { + size_t c = static_cast(rel_x / CELL_SIZE); + size_t r = static_cast(rel_y / CELL_SIZE); + + float cell_rel_x = fmod(rel_x, CELL_SIZE); + float cell_rel_y = fmod(rel_y, CELL_SIZE); + + // Proximity threshold for clicking a wall (25% of cell size) + const float THRESHOLD = CELL_SIZE * 0.25f; + + // Determine which wall is closest + float dist_top = cell_rel_y; + float dist_bottom = CELL_SIZE - cell_rel_y; + float dist_left = cell_rel_x; + float dist_right = CELL_SIZE - cell_rel_x; + + float min_dist = std::min({dist_top, dist_bottom, dist_left, dist_right}); + + if (min_dist < THRESHOLD) { + if (min_dist == dist_top) toggle_wall_fn(r, c, 0); + else if (min_dist == dist_right) toggle_wall_fn(r, c, 1); + else if (min_dist == dist_bottom) toggle_wall_fn(r, c, 2); + else if (min_dist == dist_left) toggle_wall_fn(r, c, 3); + } + } + } + } + + // Handle character input + if (event.type == sf::Event::TextEntered) { + if (row_focused_ || col_focused_ || speed_focused_) { + if (event.text.unicode == 13 || event.text.unicode == 10) { // Enter + if (speed_focused_) commit_speed(); + else commit_dimensions(); + } else if (event.text.unicode == 8) { // Backspace + std::string &buffer = row_focused_ ? row_input_buffer_ : (col_focused_ ? col_input_buffer_ : speed_input_buffer_); + if (!buffer.empty()) buffer.pop_back(); + } else if (event.text.unicode >= 48 && event.text.unicode <= 57) { // 0-9 + std::string &buffer = row_focused_ ? row_input_buffer_ : (col_focused_ ? col_input_buffer_ : speed_input_buffer_); + if (buffer.length() < 3) buffer += static_cast(event.text.unicode); + } + } + } +} + +auto UI::draw(sf::RenderWindow &window, size_t maze_rows, size_t maze_cols, + size_t display_rows, size_t display_cols, + const std::vector &grid, + const std::vector &exploration_path, + const std::vector &solved_path, + bool is_generating, bool is_solving, + bool is_paused, int solver_type, int gen_type, + int animation_speed, const GeneratorInterface &maze_gen, + const sf::Vector2f &mouse_pos, + float zoom_factor, const sf::Vector2f &camera_offset, + const std::vector& grid_values, + int heading) -> void { + + draw_sidebar(window, display_rows, display_cols, is_generating, is_solving, + is_paused, solver_type, gen_type, exploration_path.size(), + solved_path.size(), animation_speed, mouse_pos); + + draw_maze(window, maze_rows, maze_cols, grid, exploration_path, + solved_path, is_generating, maze_gen, grid_values, heading, + zoom_factor, camera_offset); +} +auto UI::draw_sidebar(sf::RenderWindow &window, size_t display_rows, + size_t display_cols, bool is_generating, bool is_solving, + bool is_paused, int solver_type, int gen_type, + size_t exploration_count, size_t solved_count, + int animation_speed, + const sf::Vector2f &mouse_pos) -> void { + // Calculate responsive dimensions + const float WINDOW_WIDTH = static_cast(window.getSize().x); + const float WINDOW_HEIGHT = static_cast(window.getSize().y); + const float SIDEBAR_WIDTH = std::clamp(WINDOW_WIDTH * 0.2f, 200.0f, 350.0f); + const float PADDING = std::clamp(WINDOW_WIDTH * 0.015f, 12.0f, 24.0f); + + // Responsive font sizes + const unsigned int HEADER_SIZE = static_cast(std::clamp(WINDOW_HEIGHT * 0.03f, 18.0f, 26.0f)); + const unsigned int SECTION_SIZE = static_cast(std::clamp(WINDOW_HEIGHT * 0.02f, 12.0f, 16.0f)); + const unsigned int TEXT_SIZE = static_cast(std::clamp(WINDOW_HEIGHT * 0.018f, 11.0f, 14.0f)); + const unsigned int STATS_SIZE = static_cast(std::clamp(WINDOW_HEIGHT * 0.017f, 10.0f, 13.0f)); + + sf::RectangleShape sb_bg( + sf::Vector2f(SIDEBAR_WIDTH, WINDOW_HEIGHT)); + sb_bg.setFillColor(Theme::SIDEBAR); + window.draw(sb_bg); + + float cx = PADDING; + float cy = PADDING; + + // Header + sf::Text header("MICROMOUSE", font_, HEADER_SIZE); + header.setStyle(sf::Text::Bold); + header.setFillColor(Theme::PRIMARY); + header.setPosition(cx, cy); + window.draw(header); + cy += HEADER_SIZE + PADDING * 1.5f; + + // Section: Maze Configuration + sf::Text cfg_title("MAZE CONFIG", font_, SECTION_SIZE); + cfg_title.setFillColor(Theme::TEXT_DIM); + cfg_title.setPosition(cx, cy); + window.draw(cfg_title); + cy += SECTION_SIZE + PADDING * 0.5f; + + const float INPUT_HEIGHT = std::clamp(WINDOW_HEIGHT * 0.05f, 32.0f, 40.0f); + const float INPUT_BOX_WIDTH = SIDEBAR_WIDTH - PADDING * 2.0f; + + UIComponents::draw_input_box(window, font_, "Rows", row_input_buffer_, layout_.row_input_box, cx, cy, INPUT_BOX_WIDTH, row_focused_); + cy += INPUT_HEIGHT; + UIComponents::draw_input_box(window, font_, "Cols", col_input_buffer_, layout_.col_input_box, cx, cy, INPUT_BOX_WIDTH, col_focused_); + cy += INPUT_HEIGHT + PADDING * 0.4f; + + UIComponents::draw_btn(window, font_, "APPLY DIMENSIONS", layout_.apply_btn, cx, cy, + SIDEBAR_WIDTH - PADDING * 2.0f, false, mouse_pos); + cy += INPUT_HEIGHT + PADDING * 0.5f; + + // Section: Speed Control + sf::Text spd_title("ANIMATION SPEED", font_, SECTION_SIZE); + spd_title.setFillColor(Theme::TEXT_DIM); + spd_title.setPosition(cx, cy); + window.draw(spd_title); + cy += SECTION_SIZE + PADDING * 0.5f; + + UIComponents::draw_input_box(window, font_, "Speed", speed_input_buffer_, layout_.speed_input_box, cx, cy, INPUT_BOX_WIDTH, speed_focused_); + cy += INPUT_HEIGHT + PADDING; + + // Section: Generation + sf::Text gen_title("GENERATION", font_, SECTION_SIZE); + gen_title.setFillColor(Theme::TEXT_DIM); + gen_title.setPosition(cx, cy); + window.draw(gen_title); + cy += SECTION_SIZE + PADDING * 0.5f; + const float BTN_SPACING = PADDING * 0.5f; + const float BTN_WIDTH = (SIDEBAR_WIDTH - PADDING * 2.0f - BTN_SPACING) / 2.0F; + const float BTN_HEIGHT_SPACING = std::clamp(WINDOW_HEIGHT * 0.062f, 38.0f, 50.0f); + UIComponents::draw_btn(window, font_, "Quick Gen", layout_.gen_inst_btn, cx, cy, + BTN_WIDTH, false, mouse_pos); + UIComponents::draw_btn(window, font_, "Step Gen", layout_.gen_step_btn, + cx + BTN_WIDTH + BTN_SPACING, cy, + BTN_WIDTH, false, mouse_pos); + cy += BTN_HEIGHT_SPACING - PADDING * 0.2f; + + UIComponents::draw_btn(window, font_, "Recursive", layout_.recursive_gen_btn, cx, cy, + BTN_WIDTH, gen_type == 0, mouse_pos); + UIComponents::draw_btn(window, font_, "Eller's", layout_.ellers_gen_btn, + cx + BTN_WIDTH + BTN_SPACING, cy, + BTN_WIDTH, gen_type == 1, mouse_pos); + cy += BTN_HEIGHT_SPACING; + + // Section: Solver Algorithms + sf::Text sol_title("ALGORITHMS", font_, SECTION_SIZE); + sol_title.setFillColor(Theme::TEXT_DIM); + sol_title.setPosition(cx, cy); + window.draw(sol_title); + cy += SECTION_SIZE + PADDING * 0.5f; + const float ALGO_BTN_WIDTH = (SIDEBAR_WIDTH - PADDING * 2.0f - BTN_SPACING) / 2.0F; + UIComponents::draw_btn(window, font_, "BFS", layout_.bfs_btn, cx, cy, ALGO_BTN_WIDTH, solver_type == 0, + mouse_pos); + UIComponents::draw_btn(window, font_, "DFS", layout_.dfs_btn, cx + ALGO_BTN_WIDTH + BTN_SPACING, cy, ALGO_BTN_WIDTH, + solver_type == 1, mouse_pos); + cy += BTN_HEIGHT_SPACING - PADDING * 0.2f; + UIComponents::draw_btn(window, font_, "A*", layout_.astar_btn, cx, cy, ALGO_BTN_WIDTH, solver_type == 2, + mouse_pos); + UIComponents::draw_btn(window, font_, "Flood Fill", layout_.flood_btn, cx + ALGO_BTN_WIDTH + BTN_SPACING, cy, + ALGO_BTN_WIDTH, solver_type == 3, mouse_pos); + cy += BTN_HEIGHT_SPACING - PADDING * 0.2f; + UIComponents::draw_btn(window, font_, "Wall Follow", layout_.wall_btn, cx, cy, + SIDEBAR_WIDTH - PADDING * 2.0f, solver_type == 4, mouse_pos); + cy += BTN_HEIGHT_SPACING; + UIComponents::draw_btn(window, font_, "ANIMATE SOLVE", layout_.solve_btn, cx, cy, ALGO_BTN_WIDTH, false, + mouse_pos); + UIComponents::draw_btn(window, font_, "INSTANT SOLVE", layout_.solve_inst_btn, + cx + ALGO_BTN_WIDTH + BTN_SPACING, cy, ALGO_BTN_WIDTH, false, mouse_pos); + cy += BTN_HEIGHT_SPACING; + + // Section: Global Controls + std::string pl = is_paused ? "RESUME" : "PAUSE"; + if (!is_generating && !is_solving) pl = "START"; + UIComponents::draw_btn(window, font_, pl, layout_.play_btn, cx, cy, ALGO_BTN_WIDTH, + !is_paused && (is_generating || is_solving), mouse_pos); + UIComponents::draw_btn(window, font_, "STEP", layout_.step_btn, cx + ALGO_BTN_WIDTH + BTN_SPACING, cy, ALGO_BTN_WIDTH, + false, mouse_pos); + cy += BTN_HEIGHT_SPACING - PADDING * 0.2f; + UIComponents::draw_btn(window, font_, "RESET SIMULATOR", layout_.reset_btn, cx, cy, + SIDEBAR_WIDTH - PADDING * 2.0f, false, mouse_pos); + cy += BTN_HEIGHT_SPACING; + + // Section: File Operations + sf::Text file_title("FILE OPERATIONS", font_, SECTION_SIZE); + file_title.setFillColor(Theme::TEXT_DIM); + file_title.setPosition(cx, cy); + window.draw(file_title); + cy += SECTION_SIZE + PADDING * 0.5f; + + UIComponents::draw_btn(window, font_, "EXPORT MAZE", layout_.save_btn, cx, cy, ALGO_BTN_WIDTH, false, mouse_pos); + UIComponents::draw_btn(window, font_, "OPEN MAZE", layout_.load_btn, cx + ALGO_BTN_WIDTH + BTN_SPACING, cy, ALGO_BTN_WIDTH, false, mouse_pos); + cy += BTN_HEIGHT_SPACING; + + // Section: Stats Card + // Section: Stats Card + if (exploration_count > 0 || solved_count > 0) { + const float CARD_HEIGHT = std::clamp(WINDOW_HEIGHT * 0.08f, 50.0f, 70.0f); + sf::RectangleShape card(sf::Vector2f(SIDEBAR_WIDTH - PADDING * 2.0f, CARD_HEIGHT)); + card.setPosition(cx, cy); + card.setFillColor(sf::Color(15, 23, 42, 180)); + card.setOutlineThickness(1.0F); + card.setOutlineColor(Theme::SURFACE); + window.draw(card); + + std::stringstream ss; + ss << "Explored: " << exploration_count << "\n"; + if (solver_type == 4) { // Wall Follower + ss << "Actual Path: " << solved_count; + } else { + ss << "Final Path: " << solved_count; + } + + sf::Text st(ss.str(), font_, STATS_SIZE); + st.setFillColor(Theme::TEXT_MAIN); + st.setPosition(cx + 12.0F, cy + 12.0F); + window.draw(st); + } +} + +auto UI::draw_maze(sf::RenderWindow &window, size_t rows, size_t cols, + const std::vector &grid, + const std::vector &exploration_path, + const std::vector &solved_path, + bool is_generating, + const GeneratorInterface &maze_gen, + const std::vector& grid_values, + int heading, + float zoom_factor, const sf::Vector2f &camera_offset) -> void { + if (grid.empty()) return; + + // Calculate responsive dimensions + const float WINDOW_WIDTH = static_cast(window.getSize().x); + const float WINDOW_HEIGHT = static_cast(window.getSize().y); + const float SIDEBAR_WIDTH = std::clamp(WINDOW_WIDTH * 0.2f, 200.0f, 350.0f); + const float AVAILABLE_WIDTH = WINDOW_WIDTH - SIDEBAR_WIDTH - 40.0f; + const float AVAILABLE_HEIGHT = WINDOW_HEIGHT - 40.0f; + + // Scale base cell size by zoom factor + const float BASE_CELL_SIZE_W = AVAILABLE_WIDTH / static_cast(cols); + const float BASE_CELL_SIZE_H = AVAILABLE_HEIGHT / static_cast(rows); + const float BASE_CELL_SIZE = std::min({BASE_CELL_SIZE_W, BASE_CELL_SIZE_H, 40.0f}); + + const float CELL_SIZE = BASE_CELL_SIZE * zoom_factor; + const float WALL_THICKNESS = std::max(1.0f, CELL_SIZE * 0.08f); + const float MAZE_W = static_cast(cols) * CELL_SIZE; + const float MAZE_H = static_cast(rows) * CELL_SIZE; + const float OFFSET_X = SIDEBAR_WIDTH + (WINDOW_WIDTH - SIDEBAR_WIDTH - MAZE_W) / 2.0F + camera_offset.x; + const float OFFSET_Y = (WINDOW_HEIGHT - MAZE_H) / 2.0F + camera_offset.y; + + // --- Performance Optimization: Frustum Culling --- + // Calculate the range of cells currently visible in the window + int start_col = std::max(0, static_cast((-OFFSET_X + SIDEBAR_WIDTH) / CELL_SIZE)); + int end_col = std::min(static_cast(cols) - 1, static_cast((WINDOW_WIDTH - OFFSET_X) / CELL_SIZE)); + int start_row = std::max(0, static_cast(-OFFSET_Y / CELL_SIZE)); + int end_row = std::min(static_cast(rows) - 1, static_cast((WINDOW_HEIGHT - OFFSET_Y) / CELL_SIZE)); + + // If no cells are visible, we can skip most of the drawing + if (start_col > end_col || start_row > end_row) return; + + // 1. Draw Grid Cells using VertexArray for batching + sf::VertexArray cell_va(sf::Quads); + for (int r = start_row; r <= end_row; ++r) { + const size_t ROW_OFFSET = r * cols; + for (int c = start_col; c <= end_col; ++c) { + const size_t IDX = ROW_OFFSET + c; + float x = OFFSET_X + c * CELL_SIZE; + float y = OFFSET_Y + r * CELL_SIZE; + + sf::Color color; + if (grid[IDX].visited) { + color = Theme::VISITED_CELL; + } else { + color = sf::Color(30, 41, 59, 40); + } + if (IDX == 0) color = sf::Color(34, 197, 94); + else if (IDX == (rows * cols - 1)) color = Theme::ACCENT; + + cell_va.append(sf::Vertex(sf::Vector2f(x, y), color)); + cell_va.append(sf::Vertex(sf::Vector2f(x + CELL_SIZE, y), color)); + cell_va.append(sf::Vertex(sf::Vector2f(x + CELL_SIZE, y + CELL_SIZE), color)); + cell_va.append(sf::Vertex(sf::Vector2f(x, y + CELL_SIZE), color)); + } + } + window.draw(cell_va); + + // 2. Draw Maze Generation Animation Path + if (is_generating) { + const auto &gp = maze_gen.get_path(); + if (gp.size() > 1) { + for (size_t i = 0; i < gp.size() - 1; ++i) { + sf::Vertex line[] = { + sf::Vertex(sf::Vector2f(OFFSET_X + gp[i].second * CELL_SIZE + CELL_SIZE / 2.0F, + OFFSET_Y + gp[i].first * CELL_SIZE + CELL_SIZE / 2.0F), + Theme::PRIMARY), + sf::Vertex(sf::Vector2f(OFFSET_X + gp[i+1].second * CELL_SIZE + CELL_SIZE / 2.0F, + OFFSET_Y + gp[i+1].first * CELL_SIZE + CELL_SIZE / 2.0F), + Theme::PRIMARY)}; + window.draw(line, 2, sf::Lines); + } + sf::CircleShape h(CELL_SIZE * 0.35F); + h.setOrigin(h.getRadius(), h.getRadius()); + h.setFillColor(sf::Color::White); + h.setPosition(OFFSET_X + gp.back().second * CELL_SIZE + CELL_SIZE / 2.0F, + OFFSET_Y + gp.back().first * CELL_SIZE + CELL_SIZE / 2.0F); + window.draw(h); + } + } + + // 3. Draw Exploration Data as Dots + if (!exploration_path.empty()) { + const float DOT_RADIUS = std::max(2.0f, CELL_SIZE * 0.22f); + sf::CircleShape dot(DOT_RADIUS); + dot.setOrigin(DOT_RADIUS, DOT_RADIUS); + dot.setFillColor(Theme::EXPLORATION_COLOR); + for (const auto NODE_IDX : exploration_path) { + auto [r, c] = MazeUtils::get_2d_coords(NODE_IDX, cols); + dot.setPosition(OFFSET_X + c * CELL_SIZE + CELL_SIZE / 2.0F, + OFFSET_Y + r * CELL_SIZE + CELL_SIZE / 2.0F); + window.draw(dot); + } + + // Highlight head + size_t head_node = exploration_path.back(); + auto [hr, hc] = MazeUtils::get_2d_coords(head_node, cols); + float hx = OFFSET_X + hc * CELL_SIZE + CELL_SIZE / 2.0F; + float hy = OFFSET_Y + hr * CELL_SIZE + CELL_SIZE / 2.0F; + + if (heading >= 0) { + // Draw a directional triangle + sf::ConvexShape triangle(3); + float r = CELL_SIZE * 0.4f; + triangle.setPoint(0, sf::Vector2f(0, -r)); + triangle.setPoint(1, sf::Vector2f(r * 0.866f, r * 0.5f)); + triangle.setPoint(2, sf::Vector2f(-r * 0.866f, r * 0.5f)); + triangle.setFillColor(sf::Color::Yellow); + triangle.setPosition(hx, hy); + triangle.setRotation(static_cast(heading * 90)); + window.draw(triangle); + } else { + // Draw a rectangle + sf::RectangleShape head(sf::Vector2f(CELL_SIZE, CELL_SIZE)); + head.setPosition(OFFSET_X + hc * CELL_SIZE, OFFSET_Y + hr * CELL_SIZE); + head.setFillColor(sf::Color(255, 255, 255, 100)); + window.draw(head); + } + } + + // New: Draw Grid Values (Flood Fill Distances) + if (!grid_values.empty() && grid_values.size() == rows * cols) { + // Use a larger base size for better quality when scaling down + unsigned int base_size = 32; + + // Disable smoothing for crisp pixel font rendering + // We need to const_cast because SFML's getTexture returns a const reference + const_cast(font_.getTexture(base_size)).setSmooth(false); + + sf::Text txt("", font_, base_size); + txt.setFillColor(sf::Color::White); // White text + txt.setOutlineColor(sf::Color::Black); // Black outline + txt.setOutlineThickness(2.0f); // Thick outline for contrast + txt.setStyle(sf::Text::Bold); + + for (int r = start_row; r <= end_row; ++r) { + const size_t ROW_OFFSET = r * cols; + for (int c = start_col; c <= end_col; ++c) { + int val = grid_values[ROW_OFFSET + c]; + // Only draw reachable/relevant values + if (val < static_cast(rows * cols)) { + txt.setString(std::to_string(val)); + sf::FloatRect b = txt.getLocalBounds(); + + // Reset scale + txt.setScale(1.0f, 1.0f); + + // Calculate scale to fit within the cell + float max_w = CELL_SIZE * 0.75f; // Slightly more padding + float max_h = CELL_SIZE * 0.75f; + float scale = 1.0f; + + if (b.width > 0 && b.height > 0) { + float s_w = max_w / b.width; + float s_h = max_h / b.height; + scale = std::min(s_w, s_h); + } + + // Cap the max scale so numbers don't look huge in large cells + scale = std::min(scale, 0.8f); + // Also don't draw if tiny (sub-pixel) + if (scale * base_size < 2.0f) continue; + + txt.setScale(scale, scale); + + // Re-center + txt.setOrigin(b.left + b.width/2.0f, b.top + b.height/2.0f); + txt.setPosition(OFFSET_X + c * CELL_SIZE + CELL_SIZE/2.0f, + OFFSET_Y + r * CELL_SIZE + CELL_SIZE/2.0f); + + window.draw(txt); + } + } + } + } + + // 4. Overlay Solution Path as Ribbon + if (!solved_path.empty()) { + const float RIBBON_THICK = std::max(3.0f, CELL_SIZE * 0.25f); + for (size_t i = 0; i < solved_path.size() - 1; ++i) { + auto [r1, c1] = MazeUtils::get_2d_coords(solved_path[i], cols); + auto [r2, c2] = MazeUtils::get_2d_coords(solved_path[i+1], cols); + float x1 = OFFSET_X + c1 * CELL_SIZE + CELL_SIZE / 2.0F, y1 = OFFSET_Y + r1 * CELL_SIZE + CELL_SIZE / 2.0F; + float x2 = OFFSET_X + c2 * CELL_SIZE + CELL_SIZE / 2.0F, y2 = OFFSET_Y + r2 * CELL_SIZE + CELL_SIZE / 2.0F; + float dx = x2 - x1, dy = y2 - y1; + float length = std::sqrt(dx*dx + dy*dy); + float angle = std::atan2(dy, dx) * 180.0F / 3.14159F; + sf::RectangleShape seg(sf::Vector2f(length, RIBBON_THICK)); + seg.setOrigin(0, RIBBON_THICK / 2.0F); + seg.setPosition(x1, y1); + seg.setRotation(angle); + seg.setFillColor(Theme::PATH_COLOR); + window.draw(seg); + } + } + + // 5. Draw Walls + sf::RectangleShape wh(sf::Vector2f(CELL_SIZE + WALL_THICKNESS, WALL_THICKNESS)); + sf::RectangleShape wv(sf::Vector2f(WALL_THICKNESS, CELL_SIZE + WALL_THICKNESS)); + wh.setFillColor(Theme::WALL_COLOR); + wv.setFillColor(Theme::WALL_COLOR); + + // 5. Draw Walls using VertexArray + sf::VertexArray wall_va(sf::Quads); + for (int r = start_row; r <= end_row; ++r) { + const size_t ROW_OFFSET = r * cols; + for (int c = start_col; c <= end_col; ++c) { + const auto &cell = grid[ROW_OFFSET + c]; + float px = OFFSET_X + c * CELL_SIZE, py = OFFSET_Y + r * CELL_SIZE; + + auto append_wall = [&](float x, float y, float w, float h) { + wall_va.append(sf::Vertex(sf::Vector2f(x, y), Theme::WALL_COLOR)); + wall_va.append(sf::Vertex(sf::Vector2f(x + w, y), Theme::WALL_COLOR)); + wall_va.append(sf::Vertex(sf::Vector2f(x + w, y + h), Theme::WALL_COLOR)); + wall_va.append(sf::Vertex(sf::Vector2f(x, y + h), Theme::WALL_COLOR)); + }; + + if (cell.top) append_wall(px - WALL_THICKNESS/2.0f, py - WALL_THICKNESS/2.0f, CELL_SIZE + WALL_THICKNESS, WALL_THICKNESS); + if (cell.bottom) append_wall(px - WALL_THICKNESS/2.0f, py + CELL_SIZE - WALL_THICKNESS/2.0f, CELL_SIZE + WALL_THICKNESS, WALL_THICKNESS); + if (cell.left) append_wall(px - WALL_THICKNESS/2.0f, py - WALL_THICKNESS/2.0f, WALL_THICKNESS, CELL_SIZE + WALL_THICKNESS); + if (cell.right) append_wall(px + CELL_SIZE - WALL_THICKNESS/2.0f, py - WALL_THICKNESS/2.0f, WALL_THICKNESS, CELL_SIZE + WALL_THICKNESS); + } + } + window.draw(wall_va); +} + + diff --git a/src/view/UIComponents.cpp b/src/view/UIComponents.cpp new file mode 100644 index 0000000..e1b44dd --- /dev/null +++ b/src/view/UIComponents.cpp @@ -0,0 +1,86 @@ +/** + * @file UIComponents.cpp + * @brief Implementation of reusable UI components. + */ + +#include "view/UIComponents.hpp" + +#include "view/UITheme.hpp" +#include + +namespace UIComponents { + +void draw_btn(sf::RenderWindow &window, const sf::Font &font, + const std::string &label, sf::FloatRect &bounds, + float x, float y, float width, + bool active, const sf::Vector2f &mouse_pos) { + // Responsive button height based on window size + const float WINDOW_HEIGHT = static_cast(window.getSize().y); + const float BTN_HEIGHT = std::clamp(WINDOW_HEIGHT * 0.05f, 30.0f, 42.0f); + const float OUTLINE_THICK = 2.0F; + const unsigned int FONT_SIZE = static_cast(std::clamp(WINDOW_HEIGHT * 0.02f, 12.0f, 16.0f)); + + sf::RectangleShape shape(sf::Vector2f(width, BTN_HEIGHT)); + shape.setPosition(x, y); + bounds = shape.getGlobalBounds(); + + const bool HOVERED = bounds.contains(mouse_pos); + + if (active) { + shape.setFillColor(Theme::PRIMARY); + shape.setOutlineThickness(OUTLINE_THICK); + shape.setOutlineColor(sf::Color::White); + } else if (HOVERED) { + shape.setFillColor(Theme::PRIMARY_HOVER); + } else { + shape.setFillColor(Theme::SURFACE); + } + + window.draw(shape); + + sf::Text text(label, font, FONT_SIZE); + text.setFillColor(Theme::TEXT_MAIN); + sf::FloatRect tb = text.getLocalBounds(); + + // Auto-scale text if too wide for the button + const float MAX_W = width - 12.0f; // 6px padding on each side + if (tb.width > MAX_W) { + float s = MAX_W / tb.width; + text.setScale(s, s); + } + + // Origin-based centering is easier for scaled text + text.setOrigin(tb.left + tb.width / 2.0f, tb.top + tb.height / 2.0f); + text.setPosition(x + width / 2.0f, y + BTN_HEIGHT / 2.0f); + + window.draw(text); +} + +void draw_input_box(sf::RenderWindow &window, const sf::Font &font, + const std::string &label, const std::string &value, + sf::FloatRect &bounds, float x, float y, + float width, bool focused) { + const float LABEL_WIDTH = 55.0f; + const float PADDING = 8.0f; + const float HEIGHT = std::clamp(static_cast(window.getSize().y) * 0.045f, 28.0f, 36.0f); + + sf::Text t(label + ":", font, 14); + t.setFillColor(Theme::TEXT_DIM); + t.setPosition(x, y + (HEIGHT - 20.0f) / 2.0f); + window.draw(t); + + sf::RectangleShape rect(sf::Vector2f(width - LABEL_WIDTH, HEIGHT)); + rect.setPosition(x + LABEL_WIDTH, y); + rect.setFillColor(sf::Color(15, 23, 42)); + rect.setOutlineThickness(focused ? 2.0f : 1.0f); + rect.setOutlineColor(focused ? Theme::PRIMARY : Theme::SURFACE); + window.draw(rect); + bounds = rect.getGlobalBounds(); + + sf::Text val(value + (focused ? "_" : ""), font, 14); + val.setFillColor(Theme::TEXT_MAIN); + val.setPosition(x + LABEL_WIDTH + PADDING, y + (HEIGHT - 20.0f) / 2.0f); + window.draw(val); +} + +} // namespace UIComponents diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 986f3ed..c9d6f3b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,4 +18,4 @@ include(GoogleTest) # Searches the compiled executable (mms-test) for all defined tests (using TEST/TEST_F macros) # and registers them with CTest. This allows you to run tests using the 'ctest' command. -gtest_discover_tests(mms-test) \ No newline at end of file +gtest_discover_tests(mms-test) diff --git a/tests/MazeGen-Test.cpp b/tests/MazeGen-Test.cpp index e7a38c4..b475986 100644 --- a/tests/MazeGen-Test.cpp +++ b/tests/MazeGen-Test.cpp @@ -1,4 +1,44 @@ -#include +#include #include -TEST(TestTopic, TrivialEquality) { EXPECT_EQ(MazeGen::exampleFunc(), 42); } \ No newline at end of file +TEST(RecursiveBacktrackerTest, Instantiation) { + RecursiveBacktracker gen; + // Verify object creation + SUCCEED(); +} + +TEST(RecursiveBacktrackerTest, InitializationShowsFirstGrid) { + RecursiveBacktracker gen; + gen.initialize(5, 5); + + // After initialization, grid should be available + const auto &grid = gen.get_grid(); + EXPECT_EQ(grid.size(), 25); // 5x5 = 25 cells + + // First cell (0,0) should be marked as visited + EXPECT_TRUE(grid[0].visited); + + // All other cells should not be visited yet + for (size_t i = 1; i < grid.size(); ++i) { + EXPECT_FALSE(grid[i].visited); + } +} + +TEST(RecursiveBacktrackerTest, StepByStepGeneration) { + RecursiveBacktracker gen; + gen.initialize(3, 3); + + // Grid should be available immediately after init + auto grid = gen.get_grid(); + EXPECT_EQ(grid.size(), 9); + + // Perform one step + gen.step(); + + // Grid should be updated after step + grid = gen.get_grid(); + EXPECT_EQ(grid.size(), 9); + + // At least the starting cell should be visited + EXPECT_TRUE(grid[0].visited); +}