diff --git a/IMPROVEMENTS_CHECKLIST.md b/IMPROVEMENTS_CHECKLIST.md deleted file mode 100644 index acf363f..0000000 --- a/IMPROVEMENTS_CHECKLIST.md +++ /dev/null @@ -1,363 +0,0 @@ -# Tetris SDL3 - Improvements Checklist - -Quick reference for implementing the recommendations from CODE_ANALYSIS.md - ---- - -## ๐Ÿ”ด High Priority (Critical) - -### 1. Smart Pointer Wrapper for SDL Resources -**Status:** โŒ Not Started -**Effort:** 2-3 hours -**Impact:** Prevents memory leaks, improves safety - -**Action Items:** -- [ ] Create `src/utils/SDLPointers.h` with smart pointer wrappers -- [ ] Replace raw `SDL_Texture*` in `MenuState.h` (lines 17-21) -- [ ] Replace raw `SDL_Texture*` in `PlayingState.h` -- [ ] Update `main.cpp` texture loading -- [ ] Test all states to ensure no regressions - -**Code Template:** -```cpp -// src/utils/SDLPointers.h -#pragma once -#include -#include - -struct SDL_TextureDeleter { - void operator()(SDL_Texture* tex) const { - if (tex) SDL_DestroyTexture(tex); - } -}; - -struct SDL_SurfaceDeleter { - void operator()(SDL_Surface* surf) const { - if (surf) SDL_DestroySurface(surf); - } -}; - -using SDL_TexturePtr = std::unique_ptr; -using SDL_SurfacePtr = std::unique_ptr; -``` - ---- - -### 2. Remove Debug File I/O -**Status:** โŒ Not Started -**Effort:** 30 minutes -**Impact:** Performance, code cleanliness - -**Action Items:** -- [ ] Remove or wrap `fopen("tetris_trace.log")` calls in `MenuState.cpp` -- [ ] Remove or wrap similar calls in other files -- [ ] Replace with SDL_LogTrace or conditional compilation -- [ ] Delete `tetris_trace.log` from repository - -**Files to Update:** -- `src/states/MenuState.cpp` (lines 182-184, 195-203, 277-278, 335-337) -- `src/main.cpp` (if any similar patterns exist) - -**Replacement Pattern:** -```cpp -// Before: -FILE* f = fopen("tetris_trace.log", "a"); -if (f) { fprintf(f, "MenuState::render entry\n"); fclose(f); } - -// After: -SDL_LogTrace(SDL_LOG_CATEGORY_APPLICATION, "MenuState::render entry"); -``` - ---- - -### 3. Improve Error Handling in Asset Loading -**Status:** โŒ Not Started -**Effort:** 2 hours -**Impact:** Better debugging, prevents crashes - -**Action Items:** -- [ ] Update `loadTextureFromImage` to return error information -- [ ] Add validation for all asset loads in `main.cpp` -- [ ] Create fallback assets for missing resources -- [ ] Add startup asset validation - -**Example:** -```cpp -struct AssetLoadResult { - SDL_TexturePtr texture; - std::string error; - bool success; -}; - -AssetLoadResult loadTextureFromImage(SDL_Renderer* renderer, - const std::string& path); -``` - ---- - -## ๐ŸŸก Medium Priority (Important) - -### 4. Extract Common Patterns -**Status:** โŒ Not Started -**Effort:** 3-4 hours -**Impact:** Reduces code duplication - -**Action Items:** -- [ ] Create `ExitPopupHelper` class in `StateContext.h` -- [ ] Update `MenuState.cpp` to use helper -- [ ] Update `PlayingState.cpp` to use helper -- [ ] Update `OptionsState.cpp` to use helper - ---- - -### 5. Move Magic Numbers to Config.h -**Status:** โŒ Not Started -**Effort:** 1 hour -**Impact:** Maintainability - -**Action Items:** -- [ ] Add menu button constants to `Config::UI` -- [ ] Add rendering constants to appropriate namespace -- [ ] Update `MenuState.cpp` to use config constants -- [ ] Update `UIRenderer.cpp` to use config constants - -**Constants to Add:** -```cpp -namespace Config::UI { - constexpr float MENU_BUTTON_WIDTH = 200.0f; - constexpr float MENU_BUTTON_HEIGHT = 70.0f; - constexpr float MENU_BUTTON_Y_FRACTION = 0.865f; - constexpr float MENU_BUTTON_SPACING = 210.0f; -} -``` - ---- - -### 6. Add Unit Tests -**Status:** โš ๏ธ Minimal (only GravityTests) -**Effort:** 8-10 hours -**Impact:** Code quality, regression prevention - -**Action Items:** -- [ ] Create `tests/GameLogicTests.cpp` - - [ ] Test piece spawning - - [ ] Test rotation - - [ ] Test collision detection - - [ ] Test line clearing - - [ ] Test scoring -- [ ] Create `tests/ScoreManagerTests.cpp` - - [ ] Test score submission - - [ ] Test high score detection - - [ ] Test persistence -- [ ] Create `tests/StateTransitionTests.cpp` - - [ ] Test state transitions - - [ ] Test state lifecycle (onEnter/onExit) -- [ ] Update CMakeLists.txt to include new tests - ---- - -## ๐ŸŸข Low Priority (Nice to Have) - -### 7. Add Doxygen Documentation -**Status:** โŒ Not Started -**Effort:** 4-6 hours -**Impact:** Developer onboarding - -**Action Items:** -- [ ] Create `Doxyfile` configuration -- [ ] Add class-level documentation to core classes -- [ ] Add function-level documentation to public APIs -- [ ] Generate HTML documentation -- [ ] Add to build process - ---- - -### 8. Performance Profiling -**Status:** โŒ Not Started -**Effort:** 4-6 hours -**Impact:** Depends on findings - -**Action Items:** -- [ ] Profile with Visual Studio Profiler / Instruments -- [ ] Identify hotspots -- [ ] Optimize critical paths -- [ ] Add performance benchmarks - ---- - -### 9. Standardize Member Variable Naming -**Status:** โš ๏ธ Inconsistent -**Effort:** 2-3 hours -**Impact:** Code consistency - -**Action Items:** -- [ ] Decide on naming convention (recommend `m_` prefix for private members) -- [ ] Update all class member variables -- [ ] Update documentation to reflect convention - -**Convention Recommendation:** -```cpp -class Example { -public: - int publicValue; // No prefix for public members - -private: - int m_privateValue; // m_ prefix for private members - float m_memberVariable; // Consistent across all classes -}; -``` - ---- - -## ๐Ÿ“‹ Code Quality Improvements - -### 10. Add .clang-format -**Status:** โŒ Not Started -**Effort:** 15 minutes -**Impact:** Consistent formatting - -**Action Items:** -- [ ] Create `.clang-format` file in project root -- [ ] Run formatter on all source files -- [ ] Add format check to CI/CD - -**Suggested .clang-format:** -```yaml -BasedOnStyle: LLVM -IndentWidth: 4 -ColumnLimit: 120 -PointerAlignment: Left -AllowShortFunctionsOnASingleLine: Empty -``` - ---- - -### 11. Add README.md -**Status:** โŒ Missing -**Effort:** 1 hour -**Impact:** Project documentation - -**Action Items:** -- [ ] Create `README.md` with: - - [ ] Project description - - [ ] Screenshots/GIF - - [ ] Build instructions - - [ ] Dependencies - - [ ] Controls - - [ ] License - ---- - -### 12. Set Up CI/CD -**Status:** โŒ Not Started -**Effort:** 2-3 hours -**Impact:** Automated testing - -**Action Items:** -- [ ] Create `.github/workflows/build.yml` -- [ ] Add Windows build job -- [ ] Add macOS build job -- [ ] Add test execution -- [ ] Add artifact upload - ---- - -## ๐Ÿ”ง Refactoring Opportunities - -### 13. Create Asset Manager -**Status:** โŒ Not Started -**Effort:** 4-5 hours -**Impact:** Better resource management - -**Action Items:** -- [ ] Create `src/core/assets/AssetManager.h` -- [ ] Implement texture caching -- [ ] Implement font caching -- [ ] Update states to use AssetManager -- [ ] Add asset preloading - ---- - -### 14. Implement Event System -**Status:** โŒ Not Started -**Effort:** 6-8 hours -**Impact:** Decoupling, flexibility - -**Action Items:** -- [ ] Create `src/core/events/EventBus.h` -- [ ] Define event types -- [ ] Replace callbacks with events -- [ ] Update Game class to publish events -- [ ] Update Audio system to subscribe to events - ---- - -### 15. Component-Based UI -**Status:** โŒ Not Started -**Effort:** 8-10 hours -**Impact:** UI maintainability - -**Action Items:** -- [ ] Create `src/ui/components/Button.h` -- [ ] Create `src/ui/components/Panel.h` -- [ ] Create `src/ui/components/Label.h` -- [ ] Refactor MenuState to use components -- [ ] Refactor OptionsState to use components - ---- - -## ๐Ÿ“Š Progress Tracking - -| Category | Total Items | Completed | In Progress | Not Started | -|----------|-------------|-----------|-------------|-------------| -| High Priority | 3 | 0 | 0 | 3 | -| Medium Priority | 3 | 0 | 0 | 3 | -| Low Priority | 3 | 0 | 0 | 3 | -| Code Quality | 3 | 0 | 0 | 3 | -| Refactoring | 3 | 0 | 0 | 3 | -| **TOTAL** | **15** | **0** | **0** | **15** | - ---- - -## ๐ŸŽฏ Suggested Implementation Order - -### Week 1: Critical Fixes -1. Remove debug file I/O (30 min) -2. Smart pointer wrapper (2-3 hours) -3. Improve error handling (2 hours) - -### Week 2: Code Quality -4. Move magic numbers to Config.h (1 hour) -5. Extract common patterns (3-4 hours) -6. Add .clang-format (15 min) -7. Add README.md (1 hour) - -### Week 3: Testing -8. Add GameLogicTests (4 hours) -9. Add ScoreManagerTests (2 hours) -10. Add StateTransitionTests (2 hours) - -### Week 4: Documentation & CI -11. Set up CI/CD (2-3 hours) -12. Add Doxygen documentation (4-6 hours) - -### Future Iterations: -13. Performance profiling -14. Asset Manager -15. Event System -16. Component-Based UI - ---- - -## ๐Ÿ“ Notes - -- Mark items as completed by changing โŒ to โœ… -- Update progress table as you complete items -- Feel free to reorder based on your priorities -- Some items can be done in parallel -- Consider creating GitHub issues for tracking - ---- - -**Last Updated:** 2025-12-03 -**Next Review:** After completing High Priority items diff --git a/QUICK_START_IMPROVEMENTS.md b/QUICK_START_IMPROVEMENTS.md deleted file mode 100644 index 764c369..0000000 --- a/QUICK_START_IMPROVEMENTS.md +++ /dev/null @@ -1,774 +0,0 @@ -# Quick Start: Implementing Top 3 Improvements - -This guide provides complete, copy-paste ready code for the three most impactful improvements. - ---- - -## ๐Ÿš€ Improvement #1: Smart Pointer Wrapper for SDL Resources - -### Step 1: Create the Utility Header - -**File:** `src/utils/SDLPointers.h` - -```cpp -#pragma once -#include -#include - -/** - * @file SDLPointers.h - * @brief Smart pointer wrappers for SDL resources - * - * Provides RAII wrappers for SDL resources to prevent memory leaks - * and ensure proper cleanup in all code paths. - */ - -namespace SDL { - -/** - * @brief Deleter for SDL_Texture - */ -struct TextureDeleter { - void operator()(SDL_Texture* tex) const { - if (tex) { - SDL_DestroyTexture(tex); - } - } -}; - -/** - * @brief Deleter for SDL_Surface - */ -struct SurfaceDeleter { - void operator()(SDL_Surface* surf) const { - if (surf) { - SDL_DestroySurface(surf); - } - } -}; - -/** - * @brief Deleter for SDL_Renderer - */ -struct RendererDeleter { - void operator()(SDL_Renderer* renderer) const { - if (renderer) { - SDL_DestroyRenderer(renderer); - } - } -}; - -/** - * @brief Deleter for SDL_Window - */ -struct WindowDeleter { - void operator()(SDL_Window* window) const { - if (window) { - SDL_DestroyWindow(window); - } - } -}; - -/** - * @brief Smart pointer for SDL_Texture - * - * Example usage: - * @code - * SDL::TexturePtr texture(SDL_CreateTexture(...)); - * if (!texture) { - * // Handle error - * } - * // Automatic cleanup when texture goes out of scope - * @endcode - */ -using TexturePtr = std::unique_ptr; - -/** - * @brief Smart pointer for SDL_Surface - */ -using SurfacePtr = std::unique_ptr; - -/** - * @brief Smart pointer for SDL_Renderer - */ -using RendererPtr = std::unique_ptr; - -/** - * @brief Smart pointer for SDL_Window - */ -using WindowPtr = std::unique_ptr; - -} // namespace SDL -``` - ---- - -### Step 2: Update MenuState.h - -**File:** `src/states/MenuState.h` - -**Before:** -```cpp -private: - int selectedButton = 0; - - // Button icons (optional - will use text if nullptr) - SDL_Texture* playIcon = nullptr; - SDL_Texture* levelIcon = nullptr; - SDL_Texture* optionsIcon = nullptr; - SDL_Texture* exitIcon = nullptr; -``` - -**After:** -```cpp -#include "../utils/SDLPointers.h" // Add this include - -private: - int selectedButton = 0; - - // Button icons (optional - will use text if nullptr) - SDL::TexturePtr playIcon; - SDL::TexturePtr levelIcon; - SDL::TexturePtr optionsIcon; - SDL::TexturePtr exitIcon; -``` - ---- - -### Step 3: Update MenuState.cpp - -**File:** `src/states/MenuState.cpp` - -**Remove the manual cleanup from onExit:** - -**Before:** -```cpp -void MenuState::onExit() { - if (ctx.showExitConfirmPopup) { - *ctx.showExitConfirmPopup = false; - } - - // Clean up icon textures - if (playIcon) { SDL_DestroyTexture(playIcon); playIcon = nullptr; } - if (levelIcon) { SDL_DestroyTexture(levelIcon); levelIcon = nullptr; } - if (optionsIcon) { SDL_DestroyTexture(optionsIcon); optionsIcon = nullptr; } - if (exitIcon) { SDL_DestroyTexture(exitIcon); exitIcon = nullptr; } -} -``` - -**After:** -```cpp -void MenuState::onExit() { - if (ctx.showExitConfirmPopup) { - *ctx.showExitConfirmPopup = false; - } - - // Icon textures are automatically cleaned up by smart pointers -} -``` - -**Update usage in render method:** - -**Before:** -```cpp -std::array icons = { - playIcon, - levelIcon, - optionsIcon, - exitIcon -}; -``` - -**After:** -```cpp -std::array icons = { - playIcon.get(), - levelIcon.get(), - optionsIcon.get(), - exitIcon.get() -}; -``` - ---- - -### Step 4: Update main.cpp Texture Loading - -**File:** `src/main.cpp` - -**Update the function signature and implementation:** - -**Before:** -```cpp -static SDL_Texture* loadTextureFromImage(SDL_Renderer* renderer, const std::string& path, int* outW = nullptr, int* outH = nullptr) { - if (!renderer) { - return nullptr; - } - - const std::string resolvedPath = AssetPath::resolveImagePath(path); - SDL_Surface* surface = IMG_Load(resolvedPath.c_str()); - if (!surface) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load image %s (resolved: %s): %s", path.c_str(), resolvedPath.c_str(), SDL_GetError()); - return nullptr; - } - - if (outW) { *outW = surface->w; } - if (outH) { *outH = surface->h; } - - SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); - SDL_DestroySurface(surface); - - if (!texture) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create texture from %s: %s", resolvedPath.c_str(), SDL_GetError()); - return nullptr; - } - - if (resolvedPath != path) { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded %s via %s", path.c_str(), resolvedPath.c_str()); - } - - return texture; -} -``` - -**After:** -```cpp -#include "utils/SDLPointers.h" // Add at top of file - -static SDL::TexturePtr loadTextureFromImage(SDL_Renderer* renderer, const std::string& path, int* outW = nullptr, int* outH = nullptr) { - if (!renderer) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Renderer is null"); - return nullptr; - } - - const std::string resolvedPath = AssetPath::resolveImagePath(path); - SDL::SurfacePtr surface(IMG_Load(resolvedPath.c_str())); - if (!surface) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load image %s (resolved: %s): %s", - path.c_str(), resolvedPath.c_str(), SDL_GetError()); - return nullptr; - } - - if (outW) { *outW = surface->w; } - if (outH) { *outH = surface->h; } - - SDL::TexturePtr texture(SDL_CreateTextureFromSurface(renderer, surface.get())); - // surface is automatically destroyed here - - if (!texture) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create texture from %s: %s", - resolvedPath.c_str(), SDL_GetError()); - return nullptr; - } - - if (resolvedPath != path) { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Loaded %s via %s", path.c_str(), resolvedPath.c_str()); - } - - return texture; -} -``` - ---- - -## ๐Ÿงน Improvement #2: Remove Debug File I/O - -### Step 1: Replace with SDL Logging - -**File:** `src/states/MenuState.cpp` - -**Before:** -```cpp -// Trace entry to persistent log for debugging abrupt exit/crash during render -{ - FILE* f = fopen("tetris_trace.log", "a"); - if (f) { - fprintf(f, "MenuState::render entry\n"); - fclose(f); - } -} -``` - -**After:** -```cpp -// Use SDL's built-in logging (only in debug builds) -#ifdef _DEBUG - SDL_LogTrace(SDL_LOG_CATEGORY_APPLICATION, "MenuState::render entry"); -#endif -``` - -**Or, if you want it always enabled but less verbose:** -```cpp -SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, "MenuState::render entry"); -``` - ---- - -### Step 2: Create a Logging Utility (Optional, Better Approach) - -**File:** `src/utils/Logger.h` - -```cpp -#pragma once -#include - -/** - * @brief Centralized logging utility - * - * Wraps SDL logging with compile-time control over verbosity. - */ -namespace Logger { - -#ifdef _DEBUG - constexpr bool TRACE_ENABLED = true; -#else - constexpr bool TRACE_ENABLED = false; -#endif - -/** - * @brief Log a trace message (only in debug builds) - */ -template -inline void trace(const char* fmt, Args... args) { - if constexpr (TRACE_ENABLED) { - SDL_LogTrace(SDL_LOG_CATEGORY_APPLICATION, fmt, args...); - } -} - -/** - * @brief Log a debug message - */ -template -inline void debug(const char* fmt, Args... args) { - SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, fmt, args...); -} - -/** - * @brief Log an info message - */ -template -inline void info(const char* fmt, Args... args) { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, fmt, args...); -} - -/** - * @brief Log a warning message - */ -template -inline void warn(const char* fmt, Args... args) { - SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, fmt, args...); -} - -/** - * @brief Log an error message - */ -template -inline void error(const char* fmt, Args... args) { - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, fmt, args...); -} - -} // namespace Logger -``` - -**Usage in MenuState.cpp:** -```cpp -#include "../utils/Logger.h" - -void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) { - Logger::trace("MenuState::render entry"); - - // ... rest of render code - - Logger::trace("MenuState::render exit"); -} -``` - ---- - -### Step 3: Update All Files - -**Files to update:** -- `src/states/MenuState.cpp` (multiple locations) -- `src/main.cpp` (if any similar patterns) - -**Search and replace pattern:** -```cpp -// Find: -FILE* f = fopen("tetris_trace.log", "a"); -if (f) { - fprintf(f, ".*"); - fclose(f); -} - -// Replace with: -Logger::trace("..."); -``` - ---- - -## ๐ŸŽฏ Improvement #3: Extract Common Patterns - -### Step 1: Create ExitPopupHelper - -**File:** `src/states/StateHelpers.h` (new file) - -```cpp -#pragma once - -/** - * @file StateHelpers.h - * @brief Helper classes for common state patterns - */ - -/** - * @brief Helper for managing exit confirmation popup - * - * Encapsulates the common pattern of showing/hiding an exit popup - * and managing the selected button state. - * - * Example usage: - * @code - * ExitPopupHelper exitPopup(ctx.exitPopupSelectedButton, ctx.showExitConfirmPopup); - * - * if (exitPopup.isVisible()) { - * exitPopup.setSelection(0); // Select YES - * } - * - * if (exitPopup.isYesSelected()) { - * // Handle exit - * } - * @endcode - */ -class ExitPopupHelper { -public: - /** - * @brief Construct helper with pointers to state variables - * @param selectedButton Pointer to selected button index (0=YES, 1=NO) - * @param showPopup Pointer to popup visibility flag - */ - ExitPopupHelper(int* selectedButton, bool* showPopup) - : m_selectedButton(selectedButton) - , m_showPopup(showPopup) - {} - - /** - * @brief Set the selected button - * @param value 0 for YES, 1 for NO - */ - void setSelection(int value) { - if (m_selectedButton) { - *m_selectedButton = value; - } - } - - /** - * @brief Get the currently selected button - * @return 0 for YES, 1 for NO, defaults to 1 (NO) if pointer is null - */ - int getSelection() const { - return m_selectedButton ? *m_selectedButton : 1; - } - - /** - * @brief Select YES button - */ - void selectYes() { - setSelection(0); - } - - /** - * @brief Select NO button - */ - void selectNo() { - setSelection(1); - } - - /** - * @brief Check if YES is selected - */ - bool isYesSelected() const { - return getSelection() == 0; - } - - /** - * @brief Check if NO is selected - */ - bool isNoSelected() const { - return getSelection() == 1; - } - - /** - * @brief Show the popup - */ - void show() { - if (m_showPopup) { - *m_showPopup = true; - } - } - - /** - * @brief Hide the popup - */ - void hide() { - if (m_showPopup) { - *m_showPopup = false; - } - } - - /** - * @brief Check if popup is visible - */ - bool isVisible() const { - return m_showPopup && *m_showPopup; - } - - /** - * @brief Toggle between YES and NO - */ - void toggleSelection() { - setSelection(isYesSelected() ? 1 : 0); - } - -private: - int* m_selectedButton; - bool* m_showPopup; -}; -``` - ---- - -### Step 2: Update MenuState.cpp - -**File:** `src/states/MenuState.cpp` - -**Add include:** -```cpp -#include "StateHelpers.h" -``` - -**Before:** -```cpp -void MenuState::handleEvent(const SDL_Event& e) { - if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { - auto setExitSelection = [&](int value) { - if (ctx.exitPopupSelectedButton) { - *ctx.exitPopupSelectedButton = value; - } - }; - auto getExitSelection = [&]() -> int { - return ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1; - }; - auto isExitPromptVisible = [&]() -> bool { - return ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup; - }; - auto setExitPrompt = [&](bool visible) { - if (ctx.showExitConfirmPopup) { - *ctx.showExitConfirmPopup = visible; - } - }; - - if (isExitPromptVisible()) { - switch (e.key.scancode) { - case SDL_SCANCODE_LEFT: - case SDL_SCANCODE_UP: - setExitSelection(0); - return; - case SDL_SCANCODE_RIGHT: - case SDL_SCANCODE_DOWN: - setExitSelection(1); - return; - case SDL_SCANCODE_RETURN: - case SDL_SCANCODE_KP_ENTER: - case SDL_SCANCODE_SPACE: - if (getExitSelection() == 0) { - setExitPrompt(false); - if (ctx.requestQuit) { - ctx.requestQuit(); - } - } else { - setExitPrompt(false); - } - return; - case SDL_SCANCODE_ESCAPE: - setExitPrompt(false); - setExitSelection(1); - return; - } - } - - // ... rest of code - } -} -``` - -**After:** -```cpp -void MenuState::handleEvent(const SDL_Event& e) { - if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { - ExitPopupHelper exitPopup(ctx.exitPopupSelectedButton, ctx.showExitConfirmPopup); - - auto triggerPlay = [&]() { - if (ctx.startPlayTransition) { - ctx.startPlayTransition(); - } else if (ctx.stateManager) { - ctx.stateManager->setState(AppState::Playing); - } - }; - - if (exitPopup.isVisible()) { - switch (e.key.scancode) { - case SDL_SCANCODE_LEFT: - case SDL_SCANCODE_UP: - exitPopup.selectYes(); - return; - case SDL_SCANCODE_RIGHT: - case SDL_SCANCODE_DOWN: - exitPopup.selectNo(); - return; - case SDL_SCANCODE_RETURN: - case SDL_SCANCODE_KP_ENTER: - case SDL_SCANCODE_SPACE: - if (exitPopup.isYesSelected()) { - exitPopup.hide(); - if (ctx.requestQuit) { - ctx.requestQuit(); - } else { - SDL_Event quit{}; - quit.type = SDL_EVENT_QUIT; - SDL_PushEvent(&quit); - } - } else { - exitPopup.hide(); - } - return; - case SDL_SCANCODE_ESCAPE: - exitPopup.hide(); - exitPopup.selectNo(); - return; - default: - return; - } - } - - switch (e.key.scancode) { - case SDL_SCANCODE_LEFT: - case SDL_SCANCODE_UP: - { - const int total = 4; - selectedButton = (selectedButton + total - 1) % total; - break; - } - case SDL_SCANCODE_RIGHT: - case SDL_SCANCODE_DOWN: - { - const int total = 4; - selectedButton = (selectedButton + 1) % total; - break; - } - case SDL_SCANCODE_RETURN: - case SDL_SCANCODE_KP_ENTER: - case SDL_SCANCODE_SPACE: - if (!ctx.stateManager) { - break; - } - switch (selectedButton) { - case 0: - triggerPlay(); - break; - case 1: - if (ctx.requestFadeTransition) { - ctx.requestFadeTransition(AppState::LevelSelector); - } else if (ctx.stateManager) { - ctx.stateManager->setState(AppState::LevelSelector); - } - break; - case 2: - if (ctx.requestFadeTransition) { - ctx.requestFadeTransition(AppState::Options); - } else if (ctx.stateManager) { - ctx.stateManager->setState(AppState::Options); - } - break; - case 3: - exitPopup.show(); - exitPopup.selectNo(); - break; - } - break; - case SDL_SCANCODE_ESCAPE: - exitPopup.show(); - exitPopup.selectNo(); - break; - default: - break; - } - } -} -``` - ---- - -### Step 3: Apply to Other States - -Apply the same pattern to: -- `src/states/PlayingState.cpp` -- `src/states/OptionsState.cpp` - -The refactoring is identical - just replace the lambda functions with `ExitPopupHelper`. - ---- - -## โœ… Testing Your Changes - -After implementing these improvements: - -1. **Build the project:** - ```powershell - cd d:\Sites\Work\tetris - cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug - cmake --build build - ``` - -2. **Run the game:** - ```powershell - .\build\Debug\tetris.exe - ``` - -3. **Test scenarios:** - - [ ] Menu loads without crashes - - [ ] All textures load correctly - - [ ] Exit popup works (ESC key) - - [ ] Navigation works (arrow keys) - - [ ] No memory leaks (check with debugger) - - [ ] Logging appears in console (debug build) - -4. **Check for memory leaks:** - - Run with Visual Studio debugger - - Check Output window for memory leak reports - - Should see no leaks from SDL textures - ---- - -## ๐Ÿ“Š Expected Impact - -After implementing these three improvements: - -| Metric | Before | After | Improvement | -|--------|--------|-------|-------------| -| **Memory Safety** | โญโญโญ | โญโญโญโญโญ | +67% | -| **Code Clarity** | โญโญโญโญ | โญโญโญโญโญ | +25% | -| **Maintainability** | โญโญโญโญ | โญโญโญโญโญ | +25% | -| **Lines of Code** | 100% | ~95% | -5% | -| **Potential Bugs** | Medium | Low | -50% | - ---- - -## ๐ŸŽ‰ Next Steps - -After successfully implementing these improvements: - -1. Review the full `CODE_ANALYSIS.md` for more recommendations -2. Check `IMPROVEMENTS_CHECKLIST.md` for the complete task list -3. Consider implementing the medium-priority items next -4. Add unit tests to prevent regressions - -**Great job improving your codebase!** ๐Ÿš€ diff --git a/src/graphics/challenge_mode.md b/challenge_mode.md similarity index 100% rename from src/graphics/challenge_mode.md rename to challenge_mode.md diff --git a/check_events.cpp b/check_events.cpp deleted file mode 100644 index bfeb252..0000000 --- a/check_events.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include -#include -int main() { std::cout << \ -SDL_EVENT_QUIT: -\ << SDL_EVENT_QUIT << std::endl; return 0; } diff --git a/settings.ini b/settings.ini index 99029f7..d68ba68 100644 --- a/settings.ini +++ b/settings.ini @@ -6,7 +6,7 @@ Fullscreen=1 [Audio] Music=1 -Sound=1 +Sound=0 [Gameplay] SmoothScroll=1 diff --git a/src/app/TetrisApp.cpp b/src/app/TetrisApp.cpp index 689f9d5..75d2224 100644 --- a/src/app/TetrisApp.cpp +++ b/src/app/TetrisApp.cpp @@ -703,7 +703,12 @@ void TetrisApp::Impl::runLoop() } } else { if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) { - game->reset(startLevelSelection); + if (game->getMode() == GameMode::Challenge) { + game->startChallengeRun(1); + } else { + game->setMode(GameMode::Endless); + game->reset(startLevelSelection); + } state = AppState::Playing; stateMgr->setState(state); } else if (e.key.scancode == SDL_SCANCODE_ESCAPE) { @@ -732,6 +737,14 @@ void TetrisApp::Impl::runLoop() if (menuInput.activated) { switch (*menuInput.activated) { case ui::BottomMenuItem::Play: + if (game) game->setMode(GameMode::Endless); + startMenuPlayTransition(); + break; + case ui::BottomMenuItem::Challenge: + if (game) { + game->setMode(GameMode::Challenge); + game->startChallengeRun(1); + } startMenuPlayTransition(); break; case ui::BottomMenuItem::Level: diff --git a/src/gameplay/core/Game.cpp b/src/gameplay/core/Game.cpp index 10cd22d..b4a9109 100644 --- a/src/gameplay/core/Game.cpp +++ b/src/gameplay/core/Game.cpp @@ -51,7 +51,10 @@ namespace { } void Game::reset(int startLevel_) { + // Standard reset is primarily for endless; Challenge reuses the same pipeline and then + // immediately sets up its own level state. std::fill(board.begin(), board.end(), 0); + clearAsteroidGrid(); std::fill(blockCounts.begin(), blockCounts.end(), 0); bag.clear(); _score = 0; _lines = 0; _level = startLevel_; startLevel = startLevel_; @@ -59,6 +62,8 @@ void Game::reset(int startLevel_) { _currentCombo = 0; _maxCombo = 0; _comboCount = 0; + challengeComplete = false; + challengeLevelActive = false; // Initialize gravity using NES timing table (ms per cell by level) gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier); fallAcc = 0; gameOver=false; paused=false; @@ -69,9 +74,209 @@ void Game::reset(int startLevel_) { _pausedTime = 0; _lastPauseStart = 0; hold = Piece{}; hold.type = PIECE_COUNT; canHold=true; - refillBag(); - pieceSequence = 0; - spawn(); + refillBag(); + pieceSequence = 0; + spawn(); + + if (mode == GameMode::Challenge) { + int lvl = startLevel_ <= 0 ? 1 : startLevel_; + startChallengeRun(lvl); + } +} + +void Game::clearAsteroidGrid() { + for (auto &cell : asteroidGrid) { + cell.reset(); + } + asteroidsRemainingCount = 0; + asteroidsTotalThisLevel = 0; +} + +void Game::startChallengeRun(int startingLevel) { + mode = GameMode::Challenge; + int lvl = std::clamp(startingLevel, 1, ASTEROID_MAX_LEVEL); + // Reset all stats and timers like a fresh run + _score = 0; _lines = 0; _level = lvl; startLevel = lvl; + _tetrisesMade = 0; + _currentCombo = 0; + _maxCombo = 0; + _comboCount = 0; + _startTime = SDL_GetPerformanceCounter(); + _pausedTime = 0; + _lastPauseStart = 0; + // Reseed challenge RNG so levels are deterministic per run but distinct per session + if (challengeSeedBase == 0) { + challengeSeedBase = static_cast(SDL_GetTicks()); + } + challengeRng.seed(challengeSeedBase + static_cast(lvl)); + setupChallengeLevel(lvl, false); +} + +void Game::beginNextChallengeLevel() { + if (mode != GameMode::Challenge || challengeComplete) { + return; + } + int next = challengeLevelIndex + 1; + if (next > ASTEROID_MAX_LEVEL) { + challengeComplete = true; + challengeLevelActive = false; + return; + } + setupChallengeLevel(next, true); +} + +void Game::setupChallengeLevel(int level, bool preserveStats) { + challengeLevelIndex = std::clamp(level, 1, ASTEROID_MAX_LEVEL); + _level = challengeLevelIndex; + startLevel = challengeLevelIndex; + challengeComplete = false; + challengeLevelActive = true; + // Refresh deterministic RNG for this level + challengeRng.seed(challengeSeedBase + static_cast(challengeLevelIndex)); + + // Optionally reset cumulative stats (new run) or keep them (between levels) + if (!preserveStats) { + std::fill(blockCounts.begin(), blockCounts.end(), 0); + _score = 0; + _lines = 0; + _tetrisesMade = 0; + _currentCombo = 0; + _comboCount = 0; + _maxCombo = 0; + _startTime = SDL_GetPerformanceCounter(); + _pausedTime = 0; + _lastPauseStart = 0; + } else { + _currentCombo = 0; + } + + // Clear playfield and piece state + std::fill(board.begin(), board.end(), 0); + clearAsteroidGrid(); + completedLines.clear(); + hardDropCells.clear(); + hardDropFxId = 0; + fallAcc = 0.0; + gameOver = false; + paused = false; + softDropping = false; + hold = Piece{}; + hold.type = PIECE_COUNT; + canHold = true; + bag.clear(); + refillBag(); + pieceSequence = 0; + spawn(); + + // Challenge gravity scales upward per level (faster = smaller ms per cell) + double baseMs = gravityMsForLevel(0, gravityGlobalMultiplier); + double speedFactor = 1.0 + static_cast(challengeLevelIndex) * 0.02; + gravityMs = (speedFactor > 0.0) ? (baseMs / speedFactor) : baseMs; + + // Place asteroids for this level + placeAsteroidsForLevel(challengeLevelIndex); + + if (levelUpCallback) { + levelUpCallback(_level); + } +} + +AsteroidType Game::chooseAsteroidTypeForLevel(int level) { + // Simple weight distribution by level bands + int normalWeight = 100; + int armoredWeight = 0; + int fallingWeight = 0; + int coreWeight = 0; + + if (level >= 10) { + armoredWeight = 20; + normalWeight = 80; + } + if (level >= 20) { + fallingWeight = 20; + normalWeight = 60; + } + if (level >= 40) { + fallingWeight = 30; + armoredWeight = 25; + normalWeight = 45; + } + if (level >= 60) { + coreWeight = 20; + fallingWeight = 30; + armoredWeight = 25; + normalWeight = 25; + } + + int total = normalWeight + armoredWeight + fallingWeight + coreWeight; + if (total <= 0) return AsteroidType::Normal; + std::uniform_int_distribution dist(0, total - 1); + int pick = dist(challengeRng); + if (pick < normalWeight) return AsteroidType::Normal; + pick -= normalWeight; + if (pick < armoredWeight) return AsteroidType::Armored; + pick -= armoredWeight; + if (pick < fallingWeight) return AsteroidType::Falling; + return AsteroidType::Core; +} + +AsteroidCell Game::makeAsteroidForType(AsteroidType t) const { + AsteroidCell cell{}; + cell.type = t; + switch (t) { + case AsteroidType::Normal: + cell.hitsRemaining = 1; + cell.gravityEnabled = false; + break; + case AsteroidType::Armored: + cell.hitsRemaining = 2; + cell.gravityEnabled = false; + break; + case AsteroidType::Falling: + cell.hitsRemaining = 2; + cell.gravityEnabled = false; + break; + case AsteroidType::Core: + cell.hitsRemaining = 3; + cell.gravityEnabled = false; + break; + } + cell.visualState = 0; + return cell; +} + +void Game::placeAsteroidsForLevel(int level) { + int desired = std::clamp(level, 1, ASTEROID_MAX_LEVEL); + // Placement window grows upward with level but caps at half board + int height = std::clamp(2 + level / 3, 2, ROWS / 2); + int minRow = ROWS - 1 - height; + int maxRow = ROWS - 1; + minRow = std::max(0, minRow); + + std::uniform_int_distribution xDist(0, COLS - 1); + std::uniform_int_distribution yDist(minRow, maxRow); + + int attempts = 0; + const int maxAttempts = desired * 16; + while (asteroidsRemainingCount < desired && attempts < maxAttempts) { + int x = xDist(challengeRng); + int y = yDist(challengeRng); + int idx = y * COLS + x; + attempts++; + if (board[idx] != 0 || asteroidGrid[idx].has_value()) { + continue; + } + AsteroidType type = chooseAsteroidTypeForLevel(level); + AsteroidCell cell = makeAsteroidForType(type); + board[idx] = ASTEROID_BASE + static_cast(type); + asteroidGrid[idx] = cell; + ++asteroidsRemainingCount; + ++asteroidsTotalThisLevel; + } + + if (asteroidsRemainingCount < desired) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "[CHALLENGE] Placed %d/%d asteroids for level %d", asteroidsRemainingCount, desired, level); + } } double Game::elapsed() const { @@ -235,23 +440,28 @@ void Game::lockPiece() { _tetrisesMade += 1; } - // JS level progression (NES-like) using starting level rules - // Both startLevel and _level are 0-based now. - int targetLevel = startLevel; - int firstThreshold = (startLevel + 1) * 10; - - if (_lines >= firstThreshold) { - targetLevel = startLevel + 1 + (_lines - firstThreshold) / 10; - } - - // If we haven't reached the first threshold yet, we are still at startLevel. - // The above logic handles this (targetLevel initialized to startLevel). + if (mode != GameMode::Challenge) { + // JS level progression (NES-like) using starting level rules + // Both startLevel and _level are 0-based now. + int targetLevel = startLevel; + int firstThreshold = (startLevel + 1) * 10; - if (targetLevel > _level) { - _level = targetLevel; - // Update gravity to exact NES speed for the new level - gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier); - if (levelUpCallback) levelUpCallback(_level); + if (_lines >= firstThreshold) { + targetLevel = startLevel + 1 + (_lines - firstThreshold) / 10; + } + + // If we haven't reached the first threshold yet, we are still at startLevel. + // The above logic handles this (targetLevel initialized to startLevel). + + if (targetLevel > _level) { + _level = targetLevel; + // Update gravity to exact NES speed for the new level + gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier); + if (levelUpCallback) levelUpCallback(_level); + } + } else { + // Challenge keeps level tied to the current challenge stage; gravity already set there + _level = challengeLevelIndex; } // Trigger sound effect callback for line clears @@ -295,30 +505,130 @@ void Game::clearCompletedLines() { void Game::actualClearLines() { if (completedLines.empty()) return; - - int write = ROWS - 1; + + std::array newBoard{}; + std::array, COLS*ROWS> newAst{}; + for (auto &cell : newAst) cell.reset(); + std::fill(newBoard.begin(), newBoard.end(), 0); + + handleAsteroidsOnClearedRows(completedLines, newBoard, newAst); + + board = newBoard; + asteroidGrid = newAst; + + // Apply asteroid-specific gravity after the board collapses + applyAsteroidGravity(); + + if (mode == GameMode::Challenge) { + if (asteroidsRemainingCount <= 0) { + beginNextChallengeLevel(); + } + } +} + +void Game::handleAsteroidsOnClearedRows(const std::vector& clearedRows, + std::array& outBoard, + std::array, COLS*ROWS>& outAsteroids) { + std::vector clearedFlags(ROWS, false); + for (int r : clearedRows) { + if (r >= 0 && r < ROWS) { + clearedFlags[r] = true; + } + } + + // Track asteroid count updates during processing + int destroyedThisPass = 0; + + // Precompute how many cleared rows are at or below each row to reposition survivors + std::array clearedBelow{}; + int running = 0; for (int y = ROWS - 1; y >= 0; --y) { - // Check if this row should be cleared - bool shouldClear = std::find(completedLines.begin(), completedLines.end(), y) != completedLines.end(); - - if (!shouldClear) { - // Keep this row, move it down if necessary - if (write != y) { - for (int x = 0; x < COLS; ++x) { - board[write*COLS + x] = board[y*COLS + x]; + clearedBelow[y] = running; + if (clearedFlags[y]) { + ++running; + } + } + + for (int y = ROWS - 1; y >= 0; --y) { + for (int x = 0; x < COLS; ++x) { + int srcIdx = y * COLS + x; + bool rowCleared = clearedFlags[y]; + bool isAsteroid = asteroidGrid[srcIdx].has_value(); + + if (rowCleared) { + if (!isAsteroid) { + continue; // normal blocks in cleared rows vanish + } + + AsteroidCell cell = *asteroidGrid[srcIdx]; + if (cell.hitsRemaining > 0) { + --cell.hitsRemaining; + } + if (cell.hitsRemaining == 0) { + destroyedThisPass++; + continue; + } + + // Update visual/gravity state for surviving asteroids + cell.visualState = static_cast(std::min(3, cell.visualState + 1)); + if (cell.type == AsteroidType::Falling || cell.type == AsteroidType::Core) { + cell.gravityEnabled = true; + } + + int destY = y + clearedBelow[y]; // shift down by cleared rows below + if (destY >= ROWS) { + continue; // off the board after collapse + } + int destIdx = destY * COLS + x; + outBoard[destIdx] = ASTEROID_BASE + static_cast(cell.type); + outAsteroids[destIdx] = cell; + } else { + int destY = y + clearedBelow[y]; + if (destY >= ROWS) { + continue; + } + int destIdx = destY * COLS + x; + outBoard[destIdx] = board[srcIdx]; + if (isAsteroid) { + outAsteroids[destIdx] = asteroidGrid[srcIdx]; } } - --write; - } - // If shouldClear is true, we skip this row (effectively removing it) - } - - // Clear the top rows that are now empty - for (int y = write; y >= 0; --y) { - for (int x = 0; x < COLS; ++x) { - board[y*COLS + x] = 0; } } + + if (destroyedThisPass > 0) { + asteroidsRemainingCount = std::max(0, asteroidsRemainingCount - destroyedThisPass); + } +} + +void Game::applyAsteroidGravity() { + if (asteroidsRemainingCount <= 0) { + return; + } + bool moved = false; + do { + moved = false; + for (int y = ROWS - 2; y >= 0; --y) { + for (int x = 0; x < COLS; ++x) { + int idx = y * COLS + x; + if (!asteroidGrid[idx].has_value()) { + continue; + } + if (!asteroidGrid[idx]->gravityEnabled) { + continue; + } + int belowIdx = (y + 1) * COLS + x; + if (board[belowIdx] == 0) { + // Move asteroid down one cell + board[belowIdx] = board[idx]; + asteroidGrid[belowIdx] = asteroidGrid[idx]; + board[idx] = 0; + asteroidGrid[idx].reset(); + moved = true; + } + } + } + } while (moved); } bool Game::tryMoveDown() { diff --git a/src/gameplay/core/Game.h b/src/gameplay/core/Game.h index 5f36ffe..c5b7fbd 100644 --- a/src/gameplay/core/Game.h +++ b/src/gameplay/core/Game.h @@ -7,12 +7,26 @@ #include #include #include +#include #include #include "../../core/GravityManager.h" enum PieceType { I, O, T, S, Z, J, L, PIECE_COUNT }; using Shape = std::array; // four rotation bitmasks +// Game runtime mode +enum class GameMode { Endless, Challenge }; + +// Special obstacle blocks used by Challenge mode +enum class AsteroidType : uint8_t { Normal = 0, Armored = 1, Falling = 2, Core = 3 }; + +struct AsteroidCell { + AsteroidType type{AsteroidType::Normal}; + uint8_t hitsRemaining{1}; + bool gravityEnabled{false}; + uint8_t visualState{0}; +}; + class Game { public: static constexpr int COLS = 10; @@ -21,8 +35,10 @@ public: struct Piece { PieceType type{PIECE_COUNT}; int rot{0}; int x{3}; int y{-2}; }; - explicit Game(int startLevel = 0) { reset(startLevel); } + explicit Game(int startLevel = 0, GameMode mode = GameMode::Endless) : mode(mode) { reset(startLevel); } void reset(int startLevel = 0); + void startChallengeRun(int startingLevel = 1); // resets stats and starts challenge level 1 (or provided) + void beginNextChallengeLevel(); // advances to the next challenge level preserving score/time // Simulation ----------------------------------------------------------- void tickGravity(double frameMs); // advance gravity accumulator & drop @@ -42,13 +58,20 @@ public: bool isGameOver() const { return gameOver; } bool isPaused() const { return paused; } void setPaused(bool p); + GameMode getMode() const { return mode; } + void setMode(GameMode m) { mode = m; } int score() const { return _score; } int lines() const { return _lines; } int level() const { return _level; } + int challengeLevel() const { return challengeLevelIndex; } + int asteroidsRemaining() const { return asteroidsRemainingCount; } + int asteroidsTotal() const { return asteroidsTotalThisLevel; } + bool isChallengeComplete() const { return challengeComplete; } int startLevelBase() const { return startLevel; } double elapsed() const; // Now calculated from start time void updateElapsedTime(); // Update elapsed time from system clock bool isSoftDropping() const { return softDropping; } + const std::array, COLS*ROWS>& asteroidCells() const { return asteroidGrid; } // Block statistics const std::array& getBlockCounts() const { return blockCounts; } @@ -87,6 +110,9 @@ public: int comboCount() const { return _comboCount; } private: + static constexpr int ASTEROID_BASE = 100; // sentinel offset for board encoding + static constexpr int ASTEROID_MAX_LEVEL = 100; + std::array board{}; // 0 empty else color index Piece cur{}, hold{}, nextPiece{}; // current, held & next piece bool canHold{true}; @@ -132,6 +158,17 @@ private: uint32_t hardDropFxId{0}; uint64_t pieceSequence{0}; + // Challenge mode state ------------------------------------------------- + GameMode mode{GameMode::Endless}; + int challengeLevelIndex{1}; + int asteroidsRemainingCount{0}; + int asteroidsTotalThisLevel{0}; + bool challengeComplete{false}; + std::array, COLS*ROWS> asteroidGrid{}; + uint32_t challengeSeedBase{0}; + std::mt19937 challengeRng{ std::random_device{}() }; + bool challengeLevelActive{false}; + // Internal helpers ---------------------------------------------------- void refillBag(); void spawn(); @@ -140,5 +177,14 @@ private: int checkLines(); // Find completed lines and store them void actualClearLines(); // Actually remove lines from board bool tryMoveDown(); // one-row fall; returns true if moved + void clearAsteroidGrid(); + void setupChallengeLevel(int level, bool preserveStats); + void placeAsteroidsForLevel(int level); + AsteroidType chooseAsteroidTypeForLevel(int level); + AsteroidCell makeAsteroidForType(AsteroidType t) const; + void handleAsteroidsOnClearedRows(const std::vector& clearedRows, + std::array& outBoard, + std::array, COLS*ROWS>& outAsteroids); + void applyAsteroidGravity(); // Gravity tuning helpers (public API declared above) }; diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index f1feabd..d707772 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -867,6 +867,8 @@ void GameRenderer::renderPlayingState( // Draw the game board const auto &board = game->boardRef(); + const auto &asteroidCells = game->asteroidCells(); + const bool challengeMode = game->getMode() == GameMode::Challenge; float impactStrength = 0.0f; float impactEased = 0.0f; std::array impactMask{}; @@ -954,7 +956,39 @@ void GameRenderer::renderPlayingState( bx += amplitude * std::sin(t * freq); by += amplitude * 0.75f * std::cos(t * (freq + 1.1f)); } - drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1); + + bool isAsteroid = challengeMode && asteroidCells[cellIdx].has_value(); + if (isAsteroid) { + const AsteroidCell& cell = *asteroidCells[cellIdx]; + SDL_Color base{}; + switch (cell.type) { + case AsteroidType::Normal: base = SDL_Color{172, 138, 104, 255}; break; + case AsteroidType::Armored: base = SDL_Color{130, 150, 176, 255}; break; + case AsteroidType::Falling: base = SDL_Color{210, 120, 82, 255}; break; + case AsteroidType::Core: base = SDL_Color{198, 78, 200, 255}; break; + } + float hpScale = std::clamp(static_cast(cell.hitsRemaining) / 3.0f, 0.25f, 1.0f); + SDL_Color fill{ + static_cast(base.r * hpScale + 40 * (1.0f - hpScale)), + static_cast(base.g * hpScale + 40 * (1.0f - hpScale)), + static_cast(base.b * hpScale + 40 * (1.0f - hpScale)), + 255 + }; + drawRect(renderer, bx, by, finalBlockSize - 1, finalBlockSize - 1, fill); + // Subtle outline to differentiate types + SDL_Color outline = base; + outline.a = 220; + SDL_FRect border{bx + 1.0f, by + 1.0f, finalBlockSize - 2.0f, finalBlockSize - 2.0f}; + SDL_SetRenderDrawColor(renderer, outline.r, outline.g, outline.b, outline.a); + SDL_RenderRect(renderer, &border); + if (cell.gravityEnabled) { + SDL_SetRenderDrawColor(renderer, 255, 230, 120, 180); + SDL_FRect glow{bx + 2.0f, by + 2.0f, finalBlockSize - 4.0f, finalBlockSize - 4.0f}; + SDL_RenderRect(renderer, &glow); + } + } else { + drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1); + } } } } @@ -1287,8 +1321,12 @@ void GameRenderer::renderPlayingState( char levelStr[16]; snprintf(levelStr, sizeof(levelStr), "%02d", game->level()); + char challengeLevelStr[16]; + snprintf(challengeLevelStr, sizeof(challengeLevelStr), "%02d/100", game->challengeLevel()); + char asteroidStr[32]; + snprintf(asteroidStr, sizeof(asteroidStr), "%d LEFT", game->asteroidsRemaining()); - // Next level progress + // Next level progress (endless only) int startLv = game->startLevelBase(); int firstThreshold = (startLv + 1) * 10; int linesDone = game->lines(); @@ -1343,12 +1381,22 @@ void GameRenderer::renderPlayingState( statLines.push_back({scoreStr, 25.0f, 0.9f, valueColor}); statLines.push_back({"LINES", 70.0f, 1.0f, labelColor}); statLines.push_back({linesStr, 95.0f, 0.9f, valueColor}); - statLines.push_back({"LEVEL", 140.0f, 1.0f, labelColor}); - statLines.push_back({levelStr, 165.0f, 0.9f, valueColor}); - statLines.push_back({"NEXT LVL", 200.0f, 1.0f, labelColor}); - statLines.push_back({nextStr, 225.0f, 0.9f, nextColor}); - statLines.push_back({"TIME", 265.0f, 1.0f, labelColor}); - statLines.push_back({timeStr, 290.0f, 0.9f, valueColor}); + + if (game->getMode() == GameMode::Challenge) { + statLines.push_back({"LEVEL", 140.0f, 1.0f, labelColor}); + statLines.push_back({challengeLevelStr, 165.0f, 0.9f, valueColor}); + statLines.push_back({"ASTEROIDS", 200.0f, 1.0f, labelColor}); + statLines.push_back({asteroidStr, 225.0f, 0.9f, nextColor}); + statLines.push_back({"TIME", 265.0f, 1.0f, labelColor}); + statLines.push_back({timeStr, 290.0f, 0.9f, valueColor}); + } else { + statLines.push_back({"LEVEL", 140.0f, 1.0f, labelColor}); + statLines.push_back({levelStr, 165.0f, 0.9f, valueColor}); + statLines.push_back({"NEXT LVL", 200.0f, 1.0f, labelColor}); + statLines.push_back({nextStr, 225.0f, 0.9f, nextColor}); + statLines.push_back({"TIME", 265.0f, 1.0f, labelColor}); + statLines.push_back({timeStr, 290.0f, 0.9f, valueColor}); + } if (debugEnabled) { SDL_Color debugLabelColor{150, 150, 150, 255}; diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index d873037..2cb608e 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -442,7 +442,7 @@ void MenuState::handleEvent(const SDL_Event& e) { case SDL_SCANCODE_LEFT: case SDL_SCANCODE_UP: { - const int total = 6; + const int total = 7; selectedButton = (selectedButton + total - 1) % total; // brief bright flash on navigation buttonFlash = 1.0; @@ -451,7 +451,7 @@ void MenuState::handleEvent(const SDL_Event& e) { case SDL_SCANCODE_RIGHT: case SDL_SCANCODE_DOWN: { - const int total = 6; + const int total = 7; selectedButton = (selectedButton + 1) % total; // brief bright flash on navigation buttonFlash = 1.0; @@ -465,9 +465,19 @@ void MenuState::handleEvent(const SDL_Event& e) { } switch (selectedButton) { case 0: + // Endless play + if (ctx.game) ctx.game->setMode(GameMode::Endless); triggerPlay(); break; case 1: + // Start challenge run at level 1 + if (ctx.game) { + ctx.game->setMode(GameMode::Challenge); + ctx.game->startChallengeRun(1); + } + triggerPlay(); + break; + case 2: // Toggle inline level selector HUD (show/hide) if (!levelPanelVisible && !levelPanelAnimating) { levelPanelAnimating = true; @@ -479,7 +489,7 @@ void MenuState::handleEvent(const SDL_Event& e) { levelDirection = -1; // hide } break; - case 2: + case 3: // Toggle the options panel with an animated slide-in/out. if (!optionsVisible && !optionsAnimating) { optionsAnimating = true; @@ -489,7 +499,7 @@ void MenuState::handleEvent(const SDL_Event& e) { optionsDirection = -1; // hide } break; - case 3: + case 4: // Toggle the inline HELP HUD (show/hide) if (!helpPanelVisible && !helpPanelAnimating) { helpPanelAnimating = true; @@ -500,7 +510,7 @@ void MenuState::handleEvent(const SDL_Event& e) { helpDirection = -1; // hide } break; - case 4: + case 5: // Toggle the inline ABOUT HUD (show/hide) if (!aboutPanelVisible && !aboutPanelAnimating) { aboutPanelAnimating = true; @@ -510,7 +520,7 @@ void MenuState::handleEvent(const SDL_Event& e) { aboutDirection = -1; } break; - case 5: + case 6: // Show the inline exit HUD if (!exitPanelVisible && !exitPanelAnimating) { exitPanelAnimating = true; diff --git a/src/states/PlayingState.cpp b/src/states/PlayingState.cpp index f055ffb..4796494 100644 --- a/src/states/PlayingState.cpp +++ b/src/states/PlayingState.cpp @@ -18,12 +18,18 @@ PlayingState::PlayingState(StateContext& ctx) : State(ctx) {} void PlayingState::onEnter() { SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Entering Playing state"); - // Initialize the game with the selected starting level - if (ctx.game && ctx.startLevelSelection) { - SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Resetting game with level %d", *ctx.startLevelSelection); - ctx.game->reset(*ctx.startLevelSelection); - } + // Initialize the game based on mode: endless uses chosen start level, challenge keeps its run state if (ctx.game) { + if (ctx.game->getMode() == GameMode::Endless) { + if (ctx.startLevelSelection) { + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[PLAYING] Resetting game with level %d", *ctx.startLevelSelection); + ctx.game->reset(*ctx.startLevelSelection); + } + } else { + // Challenge run is prepared before entering; ensure gameplay is unpaused + ctx.game->setPaused(false); + } + s_lastPieceSequence = ctx.game->getCurrentPieceSequence(); s_pendingTransport = false; } diff --git a/src/ui/BottomMenu.cpp b/src/ui/BottomMenu.cpp index 1910ca4..884c07d 100644 --- a/src/ui/BottomMenu.cpp +++ b/src/ui/BottomMenu.cpp @@ -22,11 +22,12 @@ BottomMenu buildBottomMenu(const MenuLayoutParams& params, int startLevel) { std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel); menu.buttons[0] = Button{ BottomMenuItem::Play, rects[0], "PLAY", false }; - menu.buttons[1] = Button{ BottomMenuItem::Level, rects[1], levelBtnText, true }; - menu.buttons[2] = Button{ BottomMenuItem::Options, rects[2], "OPTIONS", true }; - menu.buttons[3] = Button{ BottomMenuItem::Help, rects[3], "HELP", true }; - menu.buttons[4] = Button{ BottomMenuItem::About, rects[4], "ABOUT", true }; - menu.buttons[5] = Button{ BottomMenuItem::Exit, rects[5], "EXIT", true }; + menu.buttons[1] = Button{ BottomMenuItem::Challenge, rects[1], "CHALLENGE", false }; + menu.buttons[2] = Button{ BottomMenuItem::Level, rects[2], levelBtnText, true }; + menu.buttons[3] = Button{ BottomMenuItem::Options, rects[3], "OPTIONS", true }; + menu.buttons[4] = Button{ BottomMenuItem::Help, rects[4], "HELP", true }; + menu.buttons[5] = Button{ BottomMenuItem::About, rects[5], "ABOUT", true }; + menu.buttons[6] = Button{ BottomMenuItem::Exit, rects[6], "EXIT", true }; return menu; } @@ -60,8 +61,15 @@ void renderBottomMenu(SDL_Renderer* renderer, const double aMul = std::clamp(baseMul + (playIsActive ? flashMul : 0.0), 0.0, 1.0); if (!b.textOnly) { + const bool isPlay = (i == 0); + const bool isChallenge = (i == 1); SDL_Color bgCol{ 18, 22, 28, static_cast(std::round(180.0 * aMul)) }; SDL_Color bdCol{ 255, 200, 70, static_cast(std::round(220.0 * aMul)) }; + if (isChallenge) { + // Give Challenge a teal accent to distinguish from Play + bgCol = SDL_Color{ 18, 36, 36, static_cast(std::round(190.0 * aMul)) }; + bdCol = SDL_Color{ 120, 255, 220, static_cast(std::round(230.0 * aMul)) }; + } UIRenderer::drawButton(renderer, font, cx, cy, r.w, r.h, b.label, isHovered, isSelected, bgCol, bdCol, false, nullptr); @@ -74,14 +82,14 @@ void renderBottomMenu(SDL_Renderer* renderer, } } - // '+' separators between the bottom HUD buttons (indices 1..last) + // '+' separators between the bottom HUD buttons (indices 2..last) { SDL_BlendMode prevBlend = SDL_BLENDMODE_NONE; SDL_GetRenderDrawBlendMode(renderer, &prevBlend); SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); SDL_SetRenderDrawColor(renderer, 120, 220, 255, static_cast(std::round(180.0 * baseMul))); - const int firstSmall = 1; + const int firstSmall = 2; const int lastSmall = MENU_BTN_COUNT - 1; float y = menu.buttons[firstSmall].rect.y + menu.buttons[firstSmall].rect.h * 0.5f; for (int i = firstSmall; i < lastSmall; ++i) { diff --git a/src/ui/BottomMenu.h b/src/ui/BottomMenu.h index 3f54fbe..3d1d1ee 100644 --- a/src/ui/BottomMenu.h +++ b/src/ui/BottomMenu.h @@ -15,11 +15,12 @@ namespace ui { enum class BottomMenuItem : int { Play = 0, - Level = 1, - Options = 2, - Help = 3, - About = 4, - Exit = 5, + Challenge = 1, + Level = 2, + Options = 3, + Help = 4, + About = 5, + Exit = 6, }; struct Button { diff --git a/src/ui/MenuLayout.cpp b/src/ui/MenuLayout.cpp index 5cf6560..34e6148 100644 --- a/src/ui/MenuLayout.cpp +++ b/src/ui/MenuLayout.cpp @@ -12,28 +12,32 @@ std::array computeMenuButtonRects(const MenuLayoutPar float contentOffsetY = (p.winH - LOGICAL_H * p.logicalScale) * 0.5f / p.logicalScale; // Cockpit HUD layout (matches main_screen art): - // - A big centered PLAY button - // - A second row of 5 smaller buttons: LEVEL / OPTIONS / HELP / ABOUT / EXIT + // - Top row: PLAY and CHALLENGE (big buttons) + // - Second row: LEVEL / OPTIONS / HELP / ABOUT / EXIT (smaller buttons) const float marginX = std::max(24.0f, LOGICAL_W * 0.03f); const float marginBottom = std::max(26.0f, LOGICAL_H * 0.03f); const float availableW = std::max(120.0f, LOGICAL_W - marginX * 2.0f); - float playW = std::min(230.0f, availableW * 0.27f); - float playH = 35.0f; - float smallW = std::min(220.0f, availableW * 0.23f); + float playW = std::min(220.0f, availableW * 0.25f); + float playH = 36.0f; + float bigGap = 28.0f; + float smallW = std::min(210.0f, availableW * 0.22f); float smallH = 34.0f; - float smallSpacing = 28.0f; + float smallSpacing = 26.0f; // Scale down for narrow windows so nothing goes offscreen. - const int smallCount = MENU_BTN_COUNT - 1; + const int smallCount = MENU_BTN_COUNT - 2; float smallTotal = smallW * static_cast(smallCount) + smallSpacing * static_cast(smallCount - 1); - if (smallTotal > availableW) { - float s = availableW / smallTotal; + float topRowTotal = playW * 2.0f + bigGap; + if (smallTotal > availableW || topRowTotal > availableW) { + float s = availableW / std::max(std::max(smallTotal, topRowTotal), 1.0f); smallW *= s; smallH *= s; smallSpacing *= s; + playW *= s; + playH = std::max(26.0f, playH * std::max(0.75f, s)); + bigGap *= s; playW = std::min(playW, availableW); - playH *= std::max(0.75f, s); } float centerX = LOGICAL_W * 0.5f + contentOffsetX; @@ -44,7 +48,11 @@ std::array computeMenuButtonRects(const MenuLayoutPar float playCY = smallCY - smallH * 0.5f - rowGap - playH * 0.5f; std::array rects{}; - rects[0] = SDL_FRect{ centerX - playW * 0.5f, playCY - playH * 0.5f, playW, playH }; + // Top row big buttons + float playLeft = centerX - (playW + bigGap * 0.5f); + float challengeLeft = centerX + bigGap * 0.5f; + rects[0] = SDL_FRect{ playLeft, playCY - playH * 0.5f, playW, playH }; + rects[1] = SDL_FRect{ challengeLeft, playCY - playH * 0.5f, playW, playH }; float rowW = smallW * static_cast(smallCount) + smallSpacing * static_cast(smallCount - 1); float left = centerX - rowW * 0.5f; @@ -55,7 +63,7 @@ std::array computeMenuButtonRects(const MenuLayoutPar for (int i = 0; i < smallCount; ++i) { float x = left + i * (smallW + smallSpacing); - rects[i + 1] = SDL_FRect{ x, smallCY - smallH * 0.5f, smallW, smallH }; + rects[i + 2] = SDL_FRect{ x, smallCY - smallH * 0.5f, smallW, smallH }; } return rects; } diff --git a/src/ui/UIConstants.h b/src/ui/UIConstants.h index eb7b5c4..def95a5 100644 --- a/src/ui/UIConstants.h +++ b/src/ui/UIConstants.h @@ -1,6 +1,6 @@ #pragma once -static constexpr int MENU_BTN_COUNT = 6; +static constexpr int MENU_BTN_COUNT = 7; static constexpr float MENU_SMALL_THRESHOLD = 700.0f; static constexpr float MENU_BTN_WIDTH_LARGE = 300.0f; static constexpr float MENU_BTN_WIDTH_SMALL_FACTOR = 0.4f; // multiplied by LOGICAL_W