diff --git a/CMakeLists.txt b/CMakeLists.txt index 5dcd72c..3f2c8ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,22 +28,22 @@ find_package(SDL3_ttf CONFIG REQUIRED) add_executable(tetris src/main.cpp - src/gameplay/Game.cpp + src/gameplay/core/Game.cpp src/core/GravityManager.cpp - src/core/StateManager.cpp + src/core/state/StateManager.cpp # New core architecture classes - src/core/ApplicationManager.cpp - src/core/InputManager.cpp - src/core/AssetManager.cpp + src/core/application/ApplicationManager.cpp + src/core/input/InputManager.cpp + src/core/assets/AssetManager.cpp src/core/GlobalState.cpp - src/graphics/RenderManager.cpp + src/graphics/renderers/RenderManager.cpp src/persistence/Scores.cpp - src/graphics/Starfield.cpp - src/graphics/Starfield3D.cpp - src/graphics/Font.cpp - src/graphics/GameRenderer.cpp + src/graphics/effects/Starfield.cpp + src/graphics/effects/Starfield3D.cpp + src/graphics/ui/Font.cpp + src/graphics/renderers/GameRenderer.cpp src/audio/Audio.cpp - src/gameplay/LineEffect.cpp + src/gameplay/effects/LineEffect.cpp src/audio/SoundEffect.cpp # State implementations (new) src/states/LoadingState.cpp @@ -122,22 +122,22 @@ target_include_directories(tetris PRIVATE # Experimental refactored version (for testing new architecture) add_executable(tetris_refactored src/main_new.cpp - src/gameplay/Game.cpp + src/gameplay/core/Game.cpp src/core/GravityManager.cpp - src/core/StateManager.cpp + src/core/state/StateManager.cpp # New core architecture classes - src/core/ApplicationManager.cpp - src/core/InputManager.cpp - src/core/AssetManager.cpp + src/core/application/ApplicationManager.cpp + src/core/input/InputManager.cpp + src/core/assets/AssetManager.cpp src/core/GlobalState.cpp - src/graphics/RenderManager.cpp + src/graphics/renderers/RenderManager.cpp src/persistence/Scores.cpp - src/graphics/Starfield.cpp - src/graphics/Starfield3D.cpp - src/graphics/Font.cpp - src/graphics/GameRenderer.cpp + src/graphics/effects/Starfield.cpp + src/graphics/effects/Starfield3D.cpp + src/graphics/ui/Font.cpp + src/graphics/renderers/GameRenderer.cpp src/audio/Audio.cpp - src/gameplay/LineEffect.cpp + src/gameplay/effects/LineEffect.cpp src/audio/SoundEffect.cpp # State implementations src/states/LoadingState.cpp diff --git a/LOADING_FIX_SUMMARY.md b/LOADING_FIX_SUMMARY.md new file mode 100644 index 0000000..877fc05 --- /dev/null +++ b/LOADING_FIX_SUMMARY.md @@ -0,0 +1,29 @@ +# Loading Screen Fix Summary + +## Issue +The loading screen was getting stuck at 99% and not transitioning to the main menu. + +## Root Cause Analysis +1. **Floating Point Precision**: The loading progress calculation involved adding `0.2 + 0.7 + 0.1`. In standard IEEE 754 double precision, this sum results in `0.9999999999999999`, which is slightly less than `1.0`. + - The transition condition `loadingProgress >= 1.0` failed because of this. + - The percentage display showed `99%` because `int(0.999... * 100)` is `99`. + +2. **Potential Thread Synchronization**: There was a possibility that the audio loading thread finished loading all tracks but hadn't yet set the `loadingComplete` flag (e.g., due to a delay in thread cleanup/shutdown). This would prevent `musicLoaded` from becoming true, which is also required for the transition. + +## Fix Implemented +1. **Precision Handling**: Added a check to force `loadingProgress` to `1.0` if it exceeds `0.99`. + ```cpp + if (loadingProgress > 0.99) loadingProgress = 1.0; + ``` + +2. **Robust Completion Check**: Modified the condition for `musicLoaded` to accept completion if the number of loaded tracks matches the expected total, even if the thread hasn't officially signaled completion yet. + ```cpp + if (Audio::instance().isLoadingComplete() || (totalTracks > 0 && currentTrackLoading >= totalTracks)) { + Audio::instance().shuffle(); + musicLoaded = true; + } + ``` + +## Verification +- Verified mathematically that the floating point sum was indeed `< 1.0`. +- The code now explicitly handles this case and ensures a smooth transition to the main menu. diff --git a/assets/music/nice_combo.wav b/assets/music/nice_combo.wav index 8f7e99c..2210784 100644 Binary files a/assets/music/nice_combo.wav and b/assets/music/nice_combo.wav differ diff --git a/docs/CODE_ORGANIZATION.md b/docs/CODE_ORGANIZATION.md new file mode 100644 index 0000000..14b0a8b --- /dev/null +++ b/docs/CODE_ORGANIZATION.md @@ -0,0 +1,425 @@ +# Code Organization & Structure Improvements + +## ✅ Progress Tracker + +### Phase 1: Core Reorganization - IN PROGRESS ⚠️ + +**✅ Completed:** + +- ✅ Created new directory structure (interfaces/, application/, assets/, input/, state/, memory/) +- ✅ Created core interfaces (IRenderer.h, IAudioSystem.h, IAssetLoader.h, IInputHandler.h, IGameRules.h) +- ✅ Created ServiceContainer for dependency injection +- ✅ Moved ApplicationManager to core/application/ +- ✅ Moved AssetManager to core/assets/ +- ✅ Moved InputManager to core/input/ +- ✅ Moved StateManager to core/state/ +- ✅ Moved Game files to gameplay/core/ +- ✅ Moved Font files to graphics/ui/ +- ✅ Moved Starfield files to graphics/effects/ +- ✅ Moved RenderManager and GameRenderer to graphics/renderers/ +- ✅ Moved LineEffect to gameplay/effects/ +- ✅ Cleaned up duplicate files +- ✅ Audio and Scores files already properly located + +**⚠️ Currently In Progress:** + +- ✅ Updated critical include paths in main.cpp, state files, graphics renderers +- ✅ Fixed RenderManager duplicate method declarations +- ✅ Resolved GameRenderer.h and LoadingState.cpp include paths +- ⚠️ Still fixing remaining include path issues (ongoing) +- ⚠️ Still debugging Game.h redefinition errors (ongoing) + +**❌ Next Steps:** + +- ❌ Complete all remaining #include statement updates +- ❌ Resolve Game.h redefinition compilation errors +- ❌ Test successful compilation of both tetris and tetris_refactored targets +- ❌ Update documentation +- ❌ Begin Phase 2 - Interface implementation + +### Phase 2: Interface Extraction - NOT STARTED ❌ + +### Phase 3: Module Separation - NOT STARTED ❌ + +### Phase 4: Documentation & Standards - NOT STARTED ❌ + +## Current Structure Analysis + +### Strengths + +- ✅ Clear domain separation (core/, gameplay/, graphics/, audio/, etc.) +- ✅ Consistent naming conventions +- ✅ Modern C++ header organization +- ✅ Proper forward declarations + +### Areas for Improvement + +- ⚠️ Some files in root src/ should be moved to appropriate subdirectories +- ⚠️ Missing interfaces/contracts +- ⚠️ Some circular dependencies +- ⚠️ CMakeLists.txt has duplicate entries + +## Proposed Directory Restructure + +```text +src/ +├── core/ # Core engine systems +│ ├── interfaces/ # Abstract interfaces (NEW) +│ │ ├── IRenderer.h +│ │ ├── IAudioSystem.h +│ │ ├── IAssetLoader.h +│ │ ├── IInputHandler.h +│ │ └── IGameRules.h +│ ├── application/ # Application lifecycle (NEW) +│ │ ├── ApplicationManager.cpp/h +│ │ ├── ServiceContainer.cpp/h +│ │ └── SystemCoordinator.cpp/h +│ ├── assets/ # Asset management +│ │ ├── AssetManager.cpp/h +│ │ └── AssetLoader.cpp/h +│ ├── input/ # Input handling +│ │ └── InputManager.cpp/h +│ ├── state/ # State management +│ │ └── StateManager.cpp/h +│ ├── memory/ # Memory management (NEW) +│ │ ├── ObjectPool.h +│ │ └── MemoryTracker.h +│ └── Config.h +│ └── GlobalState.cpp/h +│ └── GravityManager.cpp/h +├── gameplay/ # Game logic +│ ├── core/ # Core game mechanics +│ │ ├── Game.cpp/h +│ │ ├── Board.cpp/h # Extract from Game.cpp +│ │ ├── Piece.cpp/h # Extract from Game.cpp +│ │ └── PieceFactory.cpp/h # Extract from Game.cpp +│ ├── rules/ # Game rules (NEW) +│ │ ├── ClassicTetrisRules.cpp/h +│ │ ├── ModernTetrisRules.cpp/h +│ │ └── ScoringSystem.cpp/h +│ ├── effects/ # Visual effects +│ │ └── LineEffect.cpp/h +│ └── mechanics/ # Game mechanics (NEW) +│ ├── RotationSystem.cpp/h +│ ├── KickTable.cpp/h +│ └── BagRandomizer.cpp/h +├── graphics/ # Rendering and visual +│ ├── renderers/ # Different renderers +│ │ ├── RenderManager.cpp/h +│ │ ├── GameRenderer.cpp/h +│ │ ├── UIRenderer.cpp/h # Extract from various places +│ │ └── EffectRenderer.cpp/h # New +│ ├── effects/ # Visual effects +│ │ ├── Starfield.cpp/h +│ │ ├── Starfield3D.cpp/h +│ │ └── ParticleSystem.cpp/h # New +│ ├── ui/ # UI components +│ │ ├── Font.cpp/h +│ │ ├── Button.cpp/h # New +│ │ ├── Panel.cpp/h # New +│ │ └── ScoreDisplay.cpp/h # New +│ └── resources/ # Graphics resources +│ ├── TextureAtlas.cpp/h # New +│ └── SpriteManager.cpp/h # New +├── audio/ # Audio system +│ ├── Audio.cpp/h +│ ├── SoundEffect.cpp/h +│ ├── MusicManager.cpp/h # New +│ └── AudioMixer.cpp/h # New +├── persistence/ # Data persistence +│ ├── Scores.cpp/h +│ ├── Settings.cpp/h # New +│ ├── SaveGame.cpp/h # New +│ └── Serialization.cpp/h # New +├── states/ # Game states +│ ├── State.h # Base interface +│ ├── LoadingState.cpp/h +│ ├── MenuState.cpp/h +│ ├── LevelSelectorState.cpp/h +│ ├── PlayingState.cpp/h +│ ├── PausedState.cpp/h # New +│ ├── GameOverState.cpp/h # New +│ └── SettingsState.cpp/h # New +├── network/ # Future: Multiplayer (NEW) +│ ├── NetworkManager.h +│ ├── Protocol.h +│ └── MultiplayerGame.h +├── utils/ # Utilities (NEW) +│ ├── Logger.cpp/h +│ ├── Timer.cpp/h +│ ├── MathUtils.h +│ └── StringUtils.h +├── platform/ # Platform-specific (NEW) +│ ├── Platform.h +│ ├── Windows/ +│ ├── Linux/ +│ └── macOS/ +└── main.cpp # Keep original main +└── main_new.cpp # Refactored main +``` + +## Module Dependencies + +### Clean Dependency Graph + +```text +Application Layer: main.cpp → ApplicationManager +Core Layer: ServiceContainer → All Managers +Gameplay Layer: Game → Rules → Mechanics +Graphics Layer: RenderManager → Renderers → Resources +Audio Layer: AudioSystem → Concrete Implementations +Persistence Layer: SaveSystem → Serialization +Platform Layer: Platform Abstraction (lowest level) +``` + +### Dependency Rules + +1. **No circular dependencies** +2. **Higher layers can depend on lower layers only** +3. **Use interfaces for cross-layer communication** +4. **Platform layer has no dependencies on other layers** + +## Header Organization + +### 1. Consistent Header Structure + +```cpp +// Standard template for all headers +#pragma once + +// System includes +#include +#include + +// External library includes +#include + +// Internal includes (from most general to most specific) +#include "core/interfaces/IRenderer.h" +#include "graphics/resources/Texture.h" +#include "MyClass.h" + +// Forward declarations +class GameRenderer; +class TextureAtlas; + +// Class definition +class MyClass { + // Public interface first +public: + // Constructors/Destructors + MyClass(); + ~MyClass(); + + // Core functionality + void update(double deltaTime); + void render(); + + // Getters/Setters + int getValue() const { return value; } + void setValue(int v) { value = v; } + +// Private implementation +private: + // Member variables + int value{0}; + std::unique_ptr renderer; + + // Private methods + void initializeRenderer(); +}; +``` + +### 2. Include Guards and PCH + +```cpp +// PrecompiledHeaders.h (NEW) +#pragma once + +// Standard library +#include +#include +#include +#include +#include +#include +#include + +// External libraries (stable) +#include +#include + +// Common project headers +#include "core/Config.h" +#include "core/interfaces/IRenderer.h" +``` + +## Code Style Improvements + +### 1. Consistent Naming Conventions + +```cpp +// Classes: PascalCase +class GameRenderer; +class TextureAtlas; + +// Functions/Methods: camelCase +void updateGameLogic(); +bool isValidPosition(); + +// Variables: camelCase +int currentScore; +double deltaTime; + +// Constants: UPPER_SNAKE_CASE +const int MAX_LEVEL = 30; +const double GRAVITY_MULTIPLIER = 1.0; + +// Private members: camelCase with suffix +class MyClass { +private: + int memberVariable_; // or m_memberVariable + static int staticCounter_; +}; +``` + +### 2. Documentation Standards + +```cpp +/** + * @brief Manages the core game state and logic for Tetris + * + * The Game class handles piece movement, rotation, line clearing, + * and scoring according to classic Tetris rules. + * + * @example + * ```cpp + * Game game(startLevel); + * game.reset(0); + * game.move(-1); // Move left + * game.rotate(1); // Rotate clockwise + * ``` + */ +class Game { +public: + /** + * @brief Moves the current piece horizontally + * @param dx Direction to move (-1 for left, +1 for right) + * @return true if the move was successful, false if blocked + */ + bool move(int dx); + + /** + * @brief Gets the current score + * @return Current score value + * @note Score never decreases during gameplay + */ + int score() const noexcept { return score_; } +}; +``` + +## CMake Improvements + +### 1. Modular CMakeLists.txt + +```cmake +# CMakeLists.txt (main) +cmake_minimum_required(VERSION 3.20) +project(tetris_sdl3 LANGUAGES CXX) + +# Global settings +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find packages +find_package(SDL3 CONFIG REQUIRED) +find_package(SDL3_ttf CONFIG REQUIRED) + +# Add subdirectories +add_subdirectory(src/core) +add_subdirectory(src/gameplay) +add_subdirectory(src/graphics) +add_subdirectory(src/audio) +add_subdirectory(src/persistence) +add_subdirectory(src/states) + +# Main executable +add_executable(tetris src/main.cpp) +target_link_libraries(tetris PRIVATE + tetris::core + tetris::gameplay + tetris::graphics + tetris::audio + tetris::persistence + tetris::states +) + +# Tests +if(BUILD_TESTING) + add_subdirectory(tests) +endif() +``` + +### 2. Module CMakeLists.txt + +```cmake +# src/core/CMakeLists.txt +add_library(tetris_core + ApplicationManager.cpp + StateManager.cpp + InputManager.cpp + AssetManager.cpp + GlobalState.cpp + GravityManager.cpp +) + +add_library(tetris::core ALIAS tetris_core) + +target_include_directories(tetris_core + PUBLIC ${CMAKE_SOURCE_DIR}/src + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(tetris_core + PUBLIC SDL3::SDL3 SDL3_ttf::SDL3_ttf +) + +# Export for use by other modules +target_compile_features(tetris_core PUBLIC cxx_std_20) +``` + +## Implementation Timeline + +### Phase 1: Core Reorganization (Week 1-2) + +1. Create new directory structure +2. Move files to appropriate locations +3. Update CMakeLists.txt files +4. Fix include paths + +### Phase 2: Interface Extraction (Week 3-4) + +1. Create interface headers +2. Update implementations to use interfaces +3. Add dependency injection container + +### Phase 3: Module Separation (Week 5-6) + +1. Split large classes (Game, ApplicationManager) +2. Create separate CMake modules +3. Establish clean dependency graph + +### Phase 4: Documentation & Standards (Week 7-8) + +1. Add comprehensive documentation +2. Implement coding standards +3. Add static analysis tools +4. Update build scripts + +## Benefits + +1. **Maintainability**: Clear module boundaries and responsibilities +2. **Testability**: Easy to mock and test individual components +3. **Scalability**: Easy to add new features without affecting existing code +4. **Team Development**: Multiple developers can work on different modules +5. **Code Reuse**: Modular design enables component reuse diff --git a/docs/PERFORMANCE_OPTIMIZATION.md b/docs/PERFORMANCE_OPTIMIZATION.md new file mode 100644 index 0000000..8dd0f91 --- /dev/null +++ b/docs/PERFORMANCE_OPTIMIZATION.md @@ -0,0 +1,163 @@ +# Performance Optimization Recommendations + +## Current Performance Analysis + +### Memory Management +- **Good**: Proper RAII patterns, smart pointers +- **Improvement**: Object pooling for frequently created/destroyed objects + +### Rendering Performance +- **Current**: SDL3 with immediate mode rendering +- **Optimization Opportunities**: Batch rendering, texture atlasing + +### Game Logic Performance +- **Current**: Simple collision detection, adequate for Tetris +- **Good**: Efficient board representation using flat array + +## Specific Optimizations + +### 1. Object Pooling for Game Pieces + +```cpp +// src/gameplay/PiecePool.h +class PiecePool { +private: + std::vector> available; + std::vector> inUse; + +public: + std::unique_ptr acquire(PieceType type); + void release(std::unique_ptr piece); + void preAllocate(size_t count); +}; +``` + +### 2. Texture Atlas for UI Elements + +```cpp +// src/graphics/TextureAtlas.h +class TextureAtlas { +private: + SDL_Texture* atlasTexture; + std::unordered_map regions; + +public: + void loadAtlas(const std::string& atlasPath, const std::string& configPath); + SDL_Rect getRegion(const std::string& name) const; + SDL_Texture* getTexture() const { return atlasTexture; } +}; +``` + +### 3. Batch Rendering System + +```cpp +// src/graphics/BatchRenderer.h +class BatchRenderer { +private: + struct RenderCommand { + SDL_Texture* texture; + SDL_Rect srcRect; + SDL_Rect dstRect; + }; + + std::vector commands; + +public: + void addSprite(SDL_Texture* texture, const SDL_Rect& src, const SDL_Rect& dst); + void flush(); + void clear(); +}; +``` + +### 4. Memory-Efficient Board Representation + +```cpp +// Current: std::array board (40 integers = 160 bytes) +// Optimized: Bitset representation for filled/empty + color array for occupied cells + +class OptimizedBoard { +private: + std::bitset occupied; // 25 bytes (200 bits) + std::array colors; // 200 bytes, but only for occupied cells + +public: + bool isOccupied(int x, int y) const; + uint8_t getColor(int x, int y) const; + void setCell(int x, int y, uint8_t color); + void clearCell(int x, int y); +}; +``` + +### 5. Cache-Friendly Data Structures + +```cpp +// Group related data together for better cache locality +struct GameState { + // Hot data (frequently accessed) + std::array board; + Piece currentPiece; + int score; + int level; + int lines; + + // Cold data (less frequently accessed) + std::vector bag; + Piece holdPiece; + bool gameOver; + bool paused; +}; +``` + +## Performance Measurement + +### 1. Add Profiling Infrastructure + +```cpp +// src/core/Profiler.h +class Profiler { +private: + std::unordered_map startTimes; + std::unordered_map averageTimes; + +public: + void beginTimer(const std::string& name); + void endTimer(const std::string& name); + void printStats(); +}; + +// Usage: +// profiler.beginTimer("GameLogic"); +// game.update(deltaTime); +// profiler.endTimer("GameLogic"); +``` + +### 2. Frame Rate Optimization + +```cpp +// Target 60 FPS with consistent frame timing +class FrameRateManager { +private: + std::chrono::high_resolution_clock::time_point lastFrame; + double targetFrameTime = 1000.0 / 60.0; // 16.67ms + +public: + void beginFrame(); + void endFrame(); + double getDeltaTime() const; + bool shouldSkipFrame() const; +}; +``` + +## Expected Performance Gains + +1. **Object Pooling**: 30-50% reduction in allocation overhead +2. **Texture Atlas**: 20-30% improvement in rendering performance +3. **Batch Rendering**: 40-60% reduction in draw calls +4. **Optimized Board**: 60% reduction in memory usage +5. **Cache Optimization**: 10-20% improvement in game logic performance + +## Implementation Priority + +1. **High Impact, Low Effort**: Profiling infrastructure, frame rate management +2. **Medium Impact, Medium Effort**: Object pooling, optimized board representation +3. **High Impact, High Effort**: Texture atlas, batch rendering system diff --git a/docs/REFACTORING_SOLID_PRINCIPLES.md b/docs/REFACTORING_SOLID_PRINCIPLES.md new file mode 100644 index 0000000..d0e03a1 --- /dev/null +++ b/docs/REFACTORING_SOLID_PRINCIPLES.md @@ -0,0 +1,128 @@ +# SOLID Principles Refactoring Plan + +## Current Architecture Issues + +### 1. Single Responsibility Principle (SRP) Violations +- `ApplicationManager` handles initialization, coordination, rendering coordination, and asset management +- `Game` class mixes game logic with some presentation concerns + +### 2. Open/Closed Principle (OCP) Opportunities +- Hard-coded piece types and behaviors +- Limited extensibility for new game modes or rule variations + +### 3. Dependency Inversion Principle (DIP) Missing +- Concrete dependencies instead of interfaces +- Direct instantiation rather than dependency injection + +## Proposed Improvements + +### 1. Extract Interfaces + +```cpp +// src/core/interfaces/IRenderer.h +class IRenderer { +public: + virtual ~IRenderer() = default; + virtual void clear(uint8_t r, uint8_t g, uint8_t b, uint8_t a) = 0; + virtual void present() = 0; + virtual SDL_Renderer* getSDLRenderer() = 0; +}; + +// src/core/interfaces/IAudioSystem.h +class IAudioSystem { +public: + virtual ~IAudioSystem() = default; + virtual void playSound(const std::string& name) = 0; + virtual void playMusic(const std::string& name) = 0; + virtual void setMasterVolume(float volume) = 0; +}; + +// src/core/interfaces/IAssetLoader.h +class IAssetLoader { +public: + virtual ~IAssetLoader() = default; + virtual SDL_Texture* loadTexture(const std::string& path) = 0; + virtual void loadFont(const std::string& name, const std::string& path, int size) = 0; +}; +``` + +### 2. Dependency Injection Container + +```cpp +// src/core/ServiceContainer.h +class ServiceContainer { +private: + std::unordered_map> services; + +public: + template + void registerService(std::shared_ptr service) { + services[std::type_index(typeid(T))] = service; + } + + template + std::shared_ptr getService() { + auto it = services.find(std::type_index(typeid(T))); + if (it != services.end()) { + return std::static_pointer_cast(it->second); + } + return nullptr; + } +}; +``` + +### 3. Break Down ApplicationManager + +```cpp +// src/core/ApplicationLifecycle.h +class ApplicationLifecycle { +public: + bool initialize(int argc, char* argv[]); + void run(); + void shutdown(); +}; + +// src/core/SystemCoordinator.h +class SystemCoordinator { +public: + void initializeSystems(ServiceContainer& container); + void updateSystems(double deltaTime); + void shutdownSystems(); +}; +``` + +### 4. Strategy Pattern for Game Rules + +```cpp +// src/gameplay/interfaces/IGameRules.h +class IGameRules { +public: + virtual ~IGameRules() = default; + virtual int calculateScore(int linesCleared, int level) = 0; + virtual double getGravitySpeed(int level) = 0; + virtual bool shouldLevelUp(int lines) = 0; +}; + +// src/gameplay/rules/ClassicTetrisRules.h +class ClassicTetrisRules : public IGameRules { +public: + int calculateScore(int linesCleared, int level) override; + double getGravitySpeed(int level) override; + bool shouldLevelUp(int lines) override; +}; +``` + +## Implementation Priority + +1. **Phase 1**: Extract core interfaces (IRenderer, IAudioSystem) +2. **Phase 2**: Implement dependency injection container +3. **Phase 3**: Break down ApplicationManager responsibilities +4. **Phase 4**: Add strategy patterns for game rules +5. **Phase 5**: Improve testability with mock implementations + +## Benefits + +- **Testability**: Easy to mock dependencies for unit tests +- **Extensibility**: New features without modifying existing code +- **Maintainability**: Clear responsibilities and loose coupling +- **Flexibility**: Easy to swap implementations (e.g., different renderers) diff --git a/docs/TESTING_STRATEGY.md b/docs/TESTING_STRATEGY.md new file mode 100644 index 0000000..e90bea5 --- /dev/null +++ b/docs/TESTING_STRATEGY.md @@ -0,0 +1,309 @@ +# Testing Strategy Enhancement + +## Current Testing State + +### Existing Tests +- ✅ GravityTests.cpp - Basic gravity manager testing +- ✅ Catch2 framework integration +- ✅ CTest integration in CMake + +### Coverage Gaps +- ❌ Game logic testing (piece movement, rotation, line clearing) +- ❌ Collision detection testing +- ❌ Scoring system testing +- ❌ State management testing +- ❌ Integration tests +- ❌ Performance tests + +## Comprehensive Testing Strategy + +### 1. Unit Tests Expansion + +```cpp +// tests/GameLogicTests.cpp +TEST_CASE("Piece Movement", "[game][movement]") { + Game game(0); + Piece originalPiece = game.current(); + + SECTION("Move left when possible") { + game.move(-1); + REQUIRE(game.current().x == originalPiece.x - 1); + } + + SECTION("Cannot move left at boundary") { + // Move piece to left edge + while (game.current().x > 0) { + game.move(-1); + } + int edgeX = game.current().x; + game.move(-1); + REQUIRE(game.current().x == edgeX); // Should not move further + } +} + +// tests/CollisionTests.cpp +TEST_CASE("Collision Detection", "[game][collision]") { + Game game(0); + + SECTION("Piece collides with bottom") { + // Force piece to bottom + while (!game.isGameOver()) { + game.hardDrop(); + if (game.isGameOver()) break; + } + // Verify collision behavior + } + + SECTION("Piece collides with placed blocks") { + // Place a block manually + // Test collision with new piece + } +} + +// tests/ScoringTests.cpp +TEST_CASE("Scoring System", "[game][scoring]") { + Game game(0); + int initialScore = game.score(); + + SECTION("Single line clear") { + // Set up board with almost complete line + // Clear line and verify score increase + } + + SECTION("Tetris (4 lines)") { + // Set up board for Tetris + // Verify bonus scoring + } +} +``` + +### 2. Mock Objects for Testing + +```cpp +// tests/mocks/MockRenderer.h +class MockRenderer : public IRenderer { +private: + mutable std::vector calls; + +public: + void clear(uint8_t r, uint8_t g, uint8_t b, uint8_t a) override { + calls.push_back("clear"); + } + + void present() override { + calls.push_back("present"); + } + + SDL_Renderer* getSDLRenderer() override { + return nullptr; // Mock implementation + } + + const std::vector& getCalls() const { return calls; } + void clearCalls() { calls.clear(); } +}; + +// tests/mocks/MockAudioSystem.h +class MockAudioSystem : public IAudioSystem { +private: + std::vector playedSounds; + +public: + void playSound(const std::string& name) override { + playedSounds.push_back(name); + } + + void playMusic(const std::string& name) override { + playedSounds.push_back("music:" + name); + } + + void setMasterVolume(float volume) override { + // Mock implementation + } + + const std::vector& getPlayedSounds() const { return playedSounds; } +}; +``` + +### 3. Integration Tests + +```cpp +// tests/integration/StateTransitionTests.cpp +TEST_CASE("State Transitions", "[integration][states]") { + ApplicationManager app; + // Mock dependencies + + SECTION("Loading to Menu transition") { + // Simulate loading completion + // Verify menu state activation + } + + SECTION("Menu to Game transition") { + // Simulate start game action + // Verify game state initialization + } +} + +// tests/integration/GamePlayTests.cpp +TEST_CASE("Complete Game Session", "[integration][gameplay]") { + Game game(0); + + SECTION("Play until first line clear") { + // Simulate complete game session + // Verify all systems work together + } +} +``` + +### 4. Performance Tests + +```cpp +// tests/performance/PerformanceTests.cpp +TEST_CASE("Game Logic Performance", "[performance]") { + Game game(0); + + SECTION("1000 piece drops should complete in reasonable time") { + auto start = std::chrono::high_resolution_clock::now(); + + for (int i = 0; i < 1000; ++i) { + game.hardDrop(); + if (game.isGameOver()) { + game.reset(0); + } + } + + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + + REQUIRE(duration.count() < 100); // Should complete in under 100ms + } +} + +// tests/performance/MemoryTests.cpp +TEST_CASE("Memory Usage", "[performance][memory]") { + SECTION("No memory leaks during gameplay") { + size_t initialMemory = getCurrentMemoryUsage(); + + { + Game game(0); + // Simulate gameplay + for (int i = 0; i < 100; ++i) { + game.hardDrop(); + if (game.isGameOver()) game.reset(0); + } + } + + size_t finalMemory = getCurrentMemoryUsage(); + REQUIRE(finalMemory <= initialMemory + 1024); // Allow small overhead + } +} +``` + +### 5. Property-Based Testing + +```cpp +// tests/property/PropertyTests.cpp +TEST_CASE("Property: Game state consistency", "[property]") { + Game game(0); + + SECTION("Score never decreases") { + int previousScore = game.score(); + + // Perform random valid actions + for (int i = 0; i < 100; ++i) { + performRandomValidAction(game); + REQUIRE(game.score() >= previousScore); + previousScore = game.score(); + } + } + + SECTION("Board state remains valid") { + for (int i = 0; i < 1000; ++i) { + performRandomValidAction(game); + REQUIRE(isBoardStateValid(game)); + } + } +} +``` + +### 6. Test Data Management + +```cpp +// tests/fixtures/GameFixtures.h +class GameFixtures { +public: + static Game createGameWithAlmostFullLine() { + Game game(0); + // Set up specific board state + return game; + } + + static Game createGameNearGameOver() { + Game game(0); + // Fill board almost to top + return game; + } + + static std::vector createTetrisPieceSequence() { + return {I, O, T, S, Z, J, L}; + } +}; +``` + +## Test Automation & CI + +### 1. GitHub Actions Configuration + +```yaml +# .github/workflows/tests.yml +name: Tests +on: [push, pull_request] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + build-type: [Debug, Release] + + steps: + - uses: actions/checkout@v3 + - name: Install dependencies + run: | + # Install vcpkg and dependencies + - name: Configure CMake + run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} + - name: Build + run: cmake --build build --config ${{ matrix.build-type }} + - name: Test + run: ctest --test-dir build --build-config ${{ matrix.build-type }} +``` + +### 2. Code Coverage + +```cmake +# Add to CMakeLists.txt +option(ENABLE_COVERAGE "Enable code coverage" OFF) + +if(ENABLE_COVERAGE) + target_compile_options(tetris PRIVATE --coverage) + target_link_libraries(tetris PRIVATE --coverage) +endif() +``` + +## Quality Metrics Targets + +- **Unit Test Coverage**: > 80% +- **Integration Test Coverage**: > 60% +- **Performance Regression**: < 5% per release +- **Memory Leak Detection**: 0 leaks in test suite +- **Static Analysis**: 0 critical issues + +## Implementation Priority + +1. **Phase 1**: Core game logic unit tests (movement, rotation, collision) +2. **Phase 2**: Mock objects and dependency injection for testability +3. **Phase 3**: Integration tests for state management +4. **Phase 4**: Performance and memory tests +5. **Phase 5**: Property-based testing and fuzzing +6. **Phase 6**: CI/CD pipeline with automated testing diff --git a/src/audio/Audio.cpp b/src/audio/Audio.cpp index dfc5f8e..5e4ff42 100644 --- a/src/audio/Audio.cpp +++ b/src/audio/Audio.cpp @@ -20,6 +20,12 @@ #pragma comment(lib, "mfuuid.lib") #pragma comment(lib, "ole32.lib") using Microsoft::WRL::ComPtr; +#ifdef max +#undef max +#endif +#ifdef min +#undef min +#endif #endif Audio& Audio::instance(){ static Audio inst; return inst; } @@ -277,3 +283,40 @@ void Audio::shutdown(){ if(mfStarted){ MFShutdown(); mfStarted=false; } #endif } + +// IAudioSystem interface implementation +void Audio::playSound(const std::string& name) { + // This is a simplified implementation - in a full implementation, + // you would load sound effects by name from assets + // For now, we'll just trigger a generic sound effect + // In practice, this would load a sound file and play it via playSfx +} + +void Audio::playMusic(const std::string& name) { + // This is a simplified implementation - in a full implementation, + // you would load music tracks by name + // For now, we'll just start the current playlist + if (!tracks.empty() && !playing) { + start(); + } +} + +void Audio::stopMusic() { + playing = false; +} + +void Audio::setMasterVolume(float volume) { + m_masterVolume = std::max(0.0f, std::min(1.0f, volume)); +} + +void Audio::setMusicVolume(float volume) { + m_musicVolume = std::max(0.0f, std::min(1.0f, volume)); +} + +void Audio::setSoundVolume(float volume) { + m_sfxVolume = std::max(0.0f, std::min(1.0f, volume)); +} + +bool Audio::isMusicPlaying() const { + return playing; +} diff --git a/src/audio/Audio.h b/src/audio/Audio.h index 7275b3d..6707529 100644 --- a/src/audio/Audio.h +++ b/src/audio/Audio.h @@ -8,6 +8,7 @@ #include #include #include +#include "../core/interfaces/IAudioSystem.h" struct AudioTrack { std::string path; @@ -18,9 +19,20 @@ struct AudioTrack { bool ok = false; }; -class Audio { +class Audio : public IAudioSystem { public: static Audio& instance(); + + // IAudioSystem interface implementation + void playSound(const std::string& name) override; + void playMusic(const std::string& name) override; + void stopMusic() override; + void setMasterVolume(float volume) override; + void setMusicVolume(float volume) override; + void setSoundVolume(float volume) override; + bool isMusicPlaying() const override; + + // Existing Audio class methods bool init(); // initialize backend (MF on Windows) void addTrack(const std::string& path); // decode MP3 -> PCM16 stereo 44100 void addTrackAsync(const std::string& path); // add track for background loading @@ -57,4 +69,9 @@ private: struct SfxPlay { std::vector pcm; size_t cursor=0; }; std::vector activeSfx; std::mutex sfxMutex; + + // Volume control + float m_masterVolume = 1.0f; + float m_musicVolume = 1.0f; + float m_sfxVolume = 1.0f; }; diff --git a/src/core/GlobalState.cpp b/src/core/GlobalState.cpp index f81c608..d8d8cd3 100644 --- a/src/core/GlobalState.cpp +++ b/src/core/GlobalState.cpp @@ -181,3 +181,30 @@ void GlobalState::resetAnimationState() { fireworks.clear(); lastFireworkTime = 0; } + +void GlobalState::updateLogicalDimensions(int windowWidth, int windowHeight) { + // For now, keep logical dimensions proportional to window size + // You can adjust this logic based on your specific needs + + // Option 1: Keep fixed aspect ratio and scale uniformly + const float targetAspect = static_cast(Config::Logical::WIDTH) / static_cast(Config::Logical::HEIGHT); + const float windowAspect = static_cast(windowWidth) / static_cast(windowHeight); + + if (windowAspect > targetAspect) { + // Window is wider than target aspect - fit to height + currentLogicalHeight = Config::Logical::HEIGHT; + currentLogicalWidth = static_cast(currentLogicalHeight * windowAspect); + } else { + // Window is taller than target aspect - fit to width + currentLogicalWidth = Config::Logical::WIDTH; + currentLogicalHeight = static_cast(currentLogicalWidth / windowAspect); + } + + // Ensure minimum sizes + currentLogicalWidth = std::max(currentLogicalWidth, 800); + currentLogicalHeight = std::max(currentLogicalHeight, 600); + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, + "[GlobalState] Updated logical dimensions: %dx%d (window: %dx%d)", + currentLogicalWidth, currentLogicalHeight, windowWidth, windowHeight); +} diff --git a/src/core/GlobalState.h b/src/core/GlobalState.h index f471a62..bd469c0 100644 --- a/src/core/GlobalState.h +++ b/src/core/GlobalState.h @@ -66,6 +66,10 @@ public: // Viewport and scaling SDL_Rect logicalVP{0, 0, 1200, 1000}; // Will use Config::Logical constants float logicalScale = 1.0f; + + // Dynamic logical dimensions (computed from window size) + int currentLogicalWidth = 1200; + int currentLogicalHeight = 1000; // Fireworks system (for menu animation) struct BlockParticle { @@ -88,6 +92,11 @@ public: void createFirework(float x, float y); void drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex); + // Logical dimensions management + void updateLogicalDimensions(int windowWidth, int windowHeight); + int getLogicalWidth() const { return currentLogicalWidth; } + int getLogicalHeight() const { return currentLogicalHeight; } + // Reset methods for different states void resetGameState(); void resetUIState(); diff --git a/src/core/ServiceContainer.h b/src/core/ServiceContainer.h new file mode 100644 index 0000000..4809487 --- /dev/null +++ b/src/core/ServiceContainer.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include + +/** + * @brief Dependency injection container for managing services + * + * Provides a centralized way to register and retrieve services, + * enabling loose coupling and better testability. + */ +class ServiceContainer { +private: + std::unordered_map> services_; + +public: + /** + * @brief Register a service instance + * @tparam T Service type + * @param service Shared pointer to service instance + */ + template + void registerService(std::shared_ptr service) { + services_[std::type_index(typeid(T))] = service; + } + + /** + * @brief Get a service instance + * @tparam T Service type + * @return Shared pointer to service instance + * @throws std::runtime_error if service is not registered + */ + template + std::shared_ptr getService() { + auto it = services_.find(std::type_index(typeid(T))); + if (it != services_.end()) { + return std::static_pointer_cast(it->second); + } + throw std::runtime_error("Service not registered: " + std::string(typeid(T).name())); + } + + /** + * @brief Get a service instance (const version) + * @tparam T Service type + * @return Shared pointer to service instance + * @throws std::runtime_error if service is not registered + */ + template + std::shared_ptr getService() const { + auto it = services_.find(std::type_index(typeid(T))); + if (it != services_.end()) { + return std::static_pointer_cast(it->second); + } + throw std::runtime_error("Service not registered: " + std::string(typeid(T).name())); + } + + /** + * @brief Check if a service is registered + * @tparam T Service type + * @return true if service is registered, false otherwise + */ + template + bool hasService() const { + return services_.find(std::type_index(typeid(T))) != services_.end(); + } + + /** + * @brief Unregister a service + * @tparam T Service type + */ + template + void unregisterService() { + services_.erase(std::type_index(typeid(T))); + } + + /** + * @brief Clear all registered services + */ + void clear() { + services_.clear(); + } + + /** + * @brief Get the number of registered services + * @return Number of registered services + */ + size_t getServiceCount() const { + return services_.size(); + } +}; diff --git a/src/core/ApplicationManager.cpp b/src/core/application/ApplicationManager.cpp similarity index 77% rename from src/core/ApplicationManager.cpp rename to src/core/application/ApplicationManager.cpp index d98f60c..d013923 100644 --- a/src/core/ApplicationManager.cpp +++ b/src/core/application/ApplicationManager.cpp @@ -1,30 +1,35 @@ #include "ApplicationManager.h" -#include "StateManager.h" -#include "InputManager.h" +#include "../state/StateManager.h" +#include "../input/InputManager.h" +#include "../interfaces/IAudioSystem.h" +#include "../interfaces/IRenderer.h" +#include "../interfaces/IAssetLoader.h" +#include "../interfaces/IInputHandler.h" #include -#include "../audio/Audio.h" -#include "../audio/SoundEffect.h" -#include "../persistence/Scores.h" -#include "../states/State.h" -#include "../states/LoadingState.h" -#include "../states/MenuState.h" -#include "../states/LevelSelectorState.h" -#include "../states/PlayingState.h" -#include "AssetManager.h" -#include "Config.h" -#include "GlobalState.h" -#include "../graphics/RenderManager.h" -#include "../graphics/Font.h" -#include "../graphics/Starfield3D.h" -#include "../graphics/Starfield.h" -#include "../graphics/GameRenderer.h" -#include "../gameplay/Game.h" -#include "../gameplay/LineEffect.h" +#include "../../audio/Audio.h" +#include "../../audio/SoundEffect.h" +#include "../../persistence/Scores.h" +#include "../../states/State.h" +#include "../../states/LoadingState.h" +#include "../../states/MenuState.h" +#include "../../states/LevelSelectorState.h" +#include "../../states/PlayingState.h" +#include "../assets/AssetManager.h" +#include "../Config.h" +#include "../GlobalState.h" +#include "../../graphics/renderers/RenderManager.h" +#include "../../graphics/ui/Font.h" +#include "../../graphics/effects/Starfield3D.h" +#include "../../graphics/effects/Starfield.h" +#include "../../graphics/renderers/GameRenderer.h" +#include "../../gameplay/core/Game.h" +#include "../../gameplay/effects/LineEffect.h" #include #include #include #include #include +#include ApplicationManager::ApplicationManager() = default; @@ -33,6 +38,115 @@ static void traceFile(const char* msg) { if (f) f << msg << "\n"; } +// Helper: extracted from inline lambda to avoid MSVC parsing issues with complex lambdas +void ApplicationManager::renderLoading(ApplicationManager* app, RenderManager& renderer) { + // Clear background first + renderer.clear(0, 0, 0, 255); + + // Use 3D starfield for loading screen (full screen) + if (app->m_starfield3D) { + int winW_actual = 0, winH_actual = 0; + if (app->m_renderManager) app->m_renderManager->getWindowSize(winW_actual, winH_actual); + if (winW_actual > 0 && winH_actual > 0) app->m_starfield3D->resize(winW_actual, winH_actual); + app->m_starfield3D->draw(renderer.getSDLRenderer()); + } + + SDL_Rect logicalVP = {0,0,0,0}; + float logicalScale = 1.0f; + if (app->m_renderManager) { + logicalVP = app->m_renderManager->getLogicalViewport(); + logicalScale = app->m_renderManager->getLogicalScale(); + } + SDL_SetRenderViewport(renderer.getSDLRenderer(), &logicalVP); + SDL_SetRenderScale(renderer.getSDLRenderer(), logicalScale, logicalScale); + + float contentOffsetX = 0.0f; + float contentOffsetY = 0.0f; + + auto drawRectOriginal = [&](float x, float y, float w, float h, SDL_Color c) { + SDL_SetRenderDrawColor(renderer.getSDLRenderer(), c.r, c.g, c.b, c.a); + SDL_FRect fr; + fr.x = x + contentOffsetX; + fr.y = y + contentOffsetY; + fr.w = w; + fr.h = h; + SDL_RenderFillRect(renderer.getSDLRenderer(), &fr); + }; + + // Compute dynamic logical width/height based on the RenderManager's + // computed viewport and scale so the loading UI sizes itself to the + // actual content area rather than a hardcoded design size. + float LOGICAL_W = static_cast(Config::Logical::WIDTH); + float LOGICAL_H = static_cast(Config::Logical::HEIGHT); + if (logicalScale > 0.0f && logicalVP.w > 0 && logicalVP.h > 0) { + // logicalVP is in window pixels; divide by scale to get logical units + LOGICAL_W = static_cast(logicalVP.w) / logicalScale; + LOGICAL_H = static_cast(logicalVP.h) / logicalScale; + } + const bool isLimitedHeight = LOGICAL_H < 450.0f; + SDL_Texture* logoTex = app->m_assetManager->getTexture("logo"); + const float logoHeight = logoTex ? (isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f) : 0; + const float loadingTextHeight = 20; + const float barHeight = 20; + const float barPaddingVertical = isLimitedHeight ? 15 : 35; + const float percentTextHeight = 24; + const float spacingBetweenElements = isLimitedHeight ? 5 : 15; + + const float totalContentHeight = logoHeight + (logoHeight > 0 ? spacingBetweenElements : 0) + loadingTextHeight + barPaddingVertical + barHeight + spacingBetweenElements + percentTextHeight; + + float currentY = (LOGICAL_H - totalContentHeight) / 2.0f; + + if (logoTex) { + const int lw = 872, lh = 273; + const float maxLogoWidth = std::min(LOGICAL_W * 0.9f, 600.0f); + const float availableHeight = isLimitedHeight ? LOGICAL_H * 0.25f : LOGICAL_H * 0.4f; + const float availableWidth = maxLogoWidth; + const float scaleFactorWidth = availableWidth / static_cast(lw); + const float scaleFactorHeight = availableHeight / static_cast(lh); + const float scaleFactor = std::min(scaleFactorWidth, scaleFactorHeight); + const float displayWidth = lw * scaleFactor; + const float displayHeight = lh * scaleFactor; + const float logoX = (LOGICAL_W - displayWidth) / 2.0f; + SDL_FRect dst{logoX + contentOffsetX, currentY + contentOffsetY, displayWidth, displayHeight}; + SDL_RenderTexture(renderer.getSDLRenderer(), logoTex, nullptr, &dst); + currentY += displayHeight + spacingBetweenElements; + } + + FontAtlas* pixelFont = (FontAtlas*)app->m_assetManager->getFont("pixel_font"); + FontAtlas* fallbackFont = (FontAtlas*)app->m_assetManager->getFont("main_font"); + FontAtlas* loadingFont = pixelFont ? pixelFont : fallbackFont; + if (loadingFont) { + const std::string loadingText = "LOADING"; + int tW=0, tH=0; loadingFont->measure(loadingText, 1.0f, tW, tH); + float textX = (LOGICAL_W - (float)tW) * 0.5f; + loadingFont->draw(renderer.getSDLRenderer(), textX + contentOffsetX, currentY + contentOffsetY, loadingText, 1.0f, {255,204,0,255}); + } + + currentY += loadingTextHeight + barPaddingVertical; + + const int barW = 400, barH = 20; + const int bx = (LOGICAL_W - barW) / 2; + float loadingProgress = app->m_assetManager->getLoadingProgress(); + drawRectOriginal(bx - 3, currentY - 3, barW + 6, barH + 6, {68,68,80,255}); + drawRectOriginal(bx, currentY, barW, barH, {34,34,34,255}); + drawRectOriginal(bx, currentY, int(barW * loadingProgress), barH, {255,204,0,255}); + currentY += barH + spacingBetweenElements; + + if (loadingFont) { + int percentage = int(loadingProgress * 100); + char percentText[16]; + std::snprintf(percentText, sizeof(percentText), "%d%%", percentage); + std::string pStr(percentText); + int pW=0, pH=0; loadingFont->measure(pStr, 1.5f, pW, pH); + float percentX = (LOGICAL_W - (float)pW) * 0.5f; + loadingFont->draw(renderer.getSDLRenderer(), percentX + contentOffsetX, currentY + contentOffsetY, pStr, 1.5f, {255,204,0,255}); + } + + SDL_SetRenderViewport(renderer.getSDLRenderer(), nullptr); + SDL_SetRenderScale(renderer.getSDLRenderer(), 1.0f, 1.0f); +} + + ApplicationManager::~ApplicationManager() { if (m_initialized) { shutdown(); @@ -49,6 +163,9 @@ bool ApplicationManager::initialize(int argc, char* argv[]) { // Initialize GlobalState GlobalState::instance().initialize(); + + // Set initial logical dimensions + GlobalState::instance().updateLogicalDimensions(m_windowWidth, m_windowHeight); // Initialize SDL first if (!initializeSDL()) { @@ -63,6 +180,9 @@ bool ApplicationManager::initialize(int argc, char* argv[]) { return false; } + // Register services for dependency injection + registerServices(); + // Initialize game systems if (!initializeGame()) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize game systems"); @@ -212,6 +332,7 @@ bool ApplicationManager::initializeManagers() { m_renderManager->setFullscreen(!fs); } // Don’t also forward Alt+Enter as an Enter keypress to states (prevents accidental "Start") + // Don't also forward Alt+Enter as an Enter keypress to states (prevents accidental "Start") consume = true; } @@ -269,6 +390,9 @@ bool ApplicationManager::initializeManagers() { // Handle window resize events for RenderManager if (we.type == SDL_EVENT_WINDOW_RESIZED && m_renderManager) { m_renderManager->handleWindowResize(we.data1, we.data2); + + // Update GlobalState logical dimensions when window resizes + GlobalState::instance().updateLogicalDimensions(we.data1, we.data2); } // Forward all window events to StateManager @@ -289,6 +413,45 @@ bool ApplicationManager::initializeManagers() { return true; } +void ApplicationManager::registerServices() { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registering services for dependency injection..."); + + // Register concrete implementations as interface singletons + if (m_renderManager) { + std::shared_ptr renderPtr(m_renderManager.get(), [](RenderManager*) { + // Custom deleter that does nothing since the unique_ptr manages lifetime + }); + m_serviceContainer.registerSingleton(renderPtr); + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IRenderer service"); + } + + if (m_assetManager) { + std::shared_ptr assetPtr(m_assetManager.get(), [](AssetManager*) { + // Custom deleter that does nothing since the unique_ptr manages lifetime + }); + m_serviceContainer.registerSingleton(assetPtr); + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IAssetLoader service"); + } + + if (m_inputManager) { + std::shared_ptr inputPtr(m_inputManager.get(), [](InputManager*) { + // Custom deleter that does nothing since the unique_ptr manages lifetime + }); + m_serviceContainer.registerSingleton(inputPtr); + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Registered IInputHandler service"); + } + + // Register Audio system singleton + auto& audioInstance = Audio::instance(); + auto audioPtr = std::shared_ptr