diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f2c8ba..472ae7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # Homebrew: brew install sdl3 find_package(SDL3 CONFIG REQUIRED) find_package(SDL3_ttf CONFIG REQUIRED) +find_package(cpr CONFIG REQUIRED) +find_package(nlohmann_json CONFIG REQUIRED) add_executable(tetris src/main.cpp @@ -82,7 +84,7 @@ if (WIN32) ) endif() -target_link_libraries(tetris PRIVATE SDL3::SDL3 SDL3_ttf::SDL3_ttf) +target_link_libraries(tetris PRIVATE SDL3::SDL3 SDL3_ttf::SDL3_ttf cpr::cpr nlohmann_json::nlohmann_json) if (WIN32) target_link_libraries(tetris PRIVATE mfplat mfreadwrite mfuuid) @@ -156,7 +158,7 @@ if (WIN32) ) endif() -target_link_libraries(tetris_refactored PRIVATE SDL3::SDL3 SDL3_ttf::SDL3_ttf) +target_link_libraries(tetris_refactored PRIVATE SDL3::SDL3 SDL3_ttf::SDL3_ttf cpr::cpr nlohmann_json::nlohmann_json) if (WIN32) target_link_libraries(tetris_refactored PRIVATE mfplat mfreadwrite mfuuid) diff --git a/IMMEDIATE_CLEANUP_COMPLETED.md b/IMMEDIATE_CLEANUP_COMPLETED.md deleted file mode 100644 index e94f540..0000000 --- a/IMMEDIATE_CLEANUP_COMPLETED.md +++ /dev/null @@ -1,198 +0,0 @@ -# Immediate Code Cleanup - COMPLETED - -## Overview -Successfully completed all immediate code cleanup tasks from Phase 1 of the Tetris refactoring project. This addressed the critical technical debt around global variables, magic numbers, and scattered constants that were hindering maintainability. - -## โœ… Completed Tasks - -### 1. Global Variables Removal -**Status**: โœ… **COMPLETED** - -#### Before (Scattered Global State) -- Static variables scattered throughout `main.cpp` -- Global texture and font variables mixed with business logic -- Global state flags without centralized management -- Static function declarations cluttering the main file - -#### After (Centralized Management) -- **GlobalState.h/cpp**: Singleton pattern for centralized state management - - Fireworks animation system - - Game state flags (pause, menu states) - - Loading progress tracking - - Cleanup and reset functionality - -- **AssetManager Integration**: All texture and font variables now managed through AssetManager -- **Clean main.cpp**: Removed static variables and moved to appropriate managers - -### 2. Constants Configuration System -**Status**: โœ… **COMPLETED** - -#### Before (Magic Numbers Everywhere) -```cpp -// Scattered throughout main.cpp -SDL_CreateWindow("Tetris", 100, 100, 1200, 700, flags); -if (das_time >= 10) { /* ... */ } -if (arr_time >= 2) { /* ... */ } -// 50+ magic numbers throughout the codebase -``` - -#### After (Organized Config Namespace) -```cpp -// Config.h - Organized by functional area -namespace Config { - namespace Window { - constexpr int DEFAULT_WIDTH = 1200; - constexpr int DEFAULT_HEIGHT = 700; - constexpr const char* TITLE = "Tetris"; - } - - namespace Gameplay { - constexpr int DAS_DELAY = 10; - constexpr int ARR_RATE = 2; - constexpr int SOFT_DROP_MULTIPLIER = 8; - } - - namespace UI { - constexpr int BUTTON_HEIGHT = 50; - constexpr int MAIN_FONT_SIZE = 24; - constexpr int PIXEL_FONT_SIZE = 16; - } - - // ... 12 organized namespaces total -} -``` - -## ๐ŸŽฏ Key Achievements - -### 1. **Code Organization** -- **Centralized Configuration**: All constants moved to `Config.h` with logical namespace organization -- **State Management**: GlobalState singleton replacing scattered static variables -- **Clean Separation**: Clear boundaries between configuration, state, and business logic - -### 2. **Maintainability Improvements** -- **Single Source of Truth**: Constants defined once in Config namespace -- **Easy Modification**: Change window size, timing, or UI layout in one place -- **Type Safety**: Constexpr constants with proper types instead of magic numbers - -### 3. **Integration Success** -- **ApplicationManager Updated**: Now uses Config constants and GlobalState -- **Build System Updated**: CMakeLists.txt includes GlobalState.cpp in both targets -- **SDL3 Compatibility**: Fixed function naming issues (`SDL_SetTextureAlphaMod`) - -### 4. **Testing Verified** -- **Successful Build**: Both `tetris.exe` and `tetris_refactored.exe` compile successfully -- **Runtime Testing**: Refactored executable runs correctly with all systems working -- **Clean Shutdown**: Proper cleanup and resource management verified - -## ๐Ÿ“ Files Created/Modified - -### New Files -- `src/core/Config.h` - Centralized constants configuration -- `src/core/GlobalState.h` - Global state management interface -- `src/core/GlobalState.cpp` - Global state implementation with fireworks system - -### Modified Files -- `src/core/ApplicationManager.h/cpp` - Integration with Config and GlobalState -- `CMakeLists.txt` - Added GlobalState.cpp to build targets -- `REFACTORING_TODO.md` - Updated completion status - -## ๐Ÿ”ง Technical Implementation Details - -### Config Namespace Structure -```cpp -namespace Config { - namespace Window { /* Display settings */ } - namespace Logical { /* Logical coordinate system */ } - namespace Gameplay { /* Game mechanics timing */ } - namespace UI { /* User interface layout */ } - namespace Loading { /* Asset loading parameters */ } - namespace Animation { /* Animation timing */ } - namespace Grid { /* Visual grid system */ } - namespace Performance { /* Performance settings */ } - namespace Input { /* Input handling */ } - namespace Audio { /* Audio system */ } - namespace Particles { /* Particle effects */ } - namespace Debug { /* Debug settings */ } -} -``` - -### GlobalState Management -```cpp -class GlobalState { -public: - static GlobalState& getInstance(); - - // Fireworks system - void updateFireworks(float deltaTime); - void triggerFireworks(); - void renderFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex); - - // State management - void reset(); - bool isPaused() const; - void setPaused(bool paused); - - // Resource cleanup - void shutdown(); -}; -``` - -## ๐ŸŽฏ Impact and Benefits - -### For Developers -- **Easier Onboarding**: Clear configuration structure for new developers -- **Faster Changes**: Modify game parameters without hunting through code -- **Better Testing**: Centralized state makes unit testing feasible - -### For Maintenance -- **Reduced Bugs**: No more magic number typos or inconsistent values -- **Easier Debugging**: Centralized state management for easier problem tracking -- **Performance Tuning**: Game timing and performance parameters in one place - -### for Future Development -- **Extensibility**: Easy to add new configuration categories -- **Refactoring Support**: Clean foundation for further architectural improvements -- **Configuration Files**: Config namespace can easily be backed by external files - -## โœ… Verification Results - -### Build Test -```bash -cmake --build build-msvc --config Debug -# Result: โœ… SUCCESS - Both executables built successfully -``` - -### Runtime Test -```bash -.\tetris_refactored.exe -# Result: โœ… SUCCESS - Game starts, loads assets, runs main loop, shuts down cleanly -``` - -### Integration Test -- โœ… Config constants properly used throughout ApplicationManager -- โœ… GlobalState singleton initializes and shuts down correctly -- โœ… AssetManager integration working with new architecture -- โœ… SDL3 function compatibility resolved - -## ๐Ÿš€ Next Steps - -With immediate cleanup completed, the codebase is now ready for: - -1. **Phase 2 Enhancements**: State system improvements and UI rendering separation -2. **Configuration Files**: External configuration file support using Config namespace -3. **Service Container**: Dependency injection system building on clean foundation -4. **Performance Optimization**: Now possible with centralized configuration system - -## ๐Ÿ“Š Metrics - -- **Files Refactored**: 6 files modified/created -- **Magic Numbers Eliminated**: 50+ constants moved to Config namespace -- **Global Variables Removed**: 15+ static variables centralized in GlobalState -- **Build Targets**: Both tetris.exe and tetris_refactored.exe working -- **Code Quality**: Significant improvement in maintainability and organization - ---- - -**Status**: โœ… **IMMEDIATE CODE CLEANUP PHASE COMPLETED SUCCESSFULLY** - -The foundation is now clean and organized, ready for the next phase of architectural improvements. diff --git a/I_PIECE_SPAWN_FIX.md b/I_PIECE_SPAWN_FIX.md deleted file mode 100644 index 7575ea7..0000000 --- a/I_PIECE_SPAWN_FIX.md +++ /dev/null @@ -1,178 +0,0 @@ -# I-Piece Spawn Position Fix - -## Overview - -Fixed the I-piece (straight line tetromino) to spawn one row higher than other pieces, addressing the issue where the I-piece only occupied one line when spawning vertically, making its entry appear less natural. - -## Problem Analysis - -### Issue Identified - -The I-piece has unique spawn behavior because: - -- **Vertical I-piece**: When spawning in vertical orientation (rotation 0), it only occupies one row -- **Visual Impact**: This made the I-piece appear to "pop in" at the top grid line rather than naturally entering -- **Player Experience**: Less time to react and plan placement compared to other pieces - -### Other Pieces - -All other pieces (O, T, S, Z, J, L) were working perfectly at their current spawn position (`y = -1`) and should remain unchanged. - -## Solution Implemented - -### Targeted I-Piece Fix - -Instead of changing all pieces, implemented piece-type-specific spawn logic: - -```cpp -// I-piece spawns higher due to its unique height characteristics -int spawnY = (pieceType == I) ? -2 : -1; -``` - -### Code Changes - -#### 1. Updated spawn() function - -```cpp -void Game::spawn() { - if (bag.empty()) refillBag(); - PieceType pieceType = bag.back(); - - // I-piece needs to start one row higher due to its height when vertical - int spawnY = (pieceType == I) ? -2 : -1; - - cur = Piece{ pieceType, 0, 3, spawnY }; - bag.pop_back(); - blockCounts[cur.type]++; - canHold = true; - - // Prepare next piece with same logic - if (bag.empty()) refillBag(); - PieceType nextType = bag.back(); - int nextSpawnY = (nextType == I) ? -2 : -1; - nextPiece = Piece{ nextType, 0, 3, nextSpawnY }; -} -``` - -#### 2. Updated holdCurrent() function - -```cpp -// Apply I-piece specific positioning in hold mechanics -int holdSpawnY = (hold.type == I) ? -2 : -1; -int currentSpawnY = (temp.type == I) ? -2 : -1; -``` - -## Technical Benefits - -### 1. Piece-Specific Optimization - -- **I-Piece**: Spawns at `y = -2` for natural vertical entry -- **Other Pieces**: Remain at `y = -1` for optimal gameplay feel -- **Consistency**: Each piece type gets appropriate spawn positioning - -### 2. Enhanced Gameplay - -- **I-Piece Visibility**: More natural entry animation and player reaction time -- **Preserved Balance**: Other pieces maintain their optimized spawn positions -- **Strategic Depth**: I-piece placement becomes more strategic with better entry timing - -### 3. Code Quality - -- **Targeted Solution**: Minimal code changes addressing specific issue -- **Maintainable Logic**: Clear piece-type conditionals -- **Extensible Design**: Easy to adjust other pieces if needed in future - -## Spawn Position Matrix - -```text -Piece Type | Spawn Y | Reasoning ------------|---------|------------------------------------------ -I-piece | -2 | Vertical orientation needs extra height -O-piece | -1 | Perfect 2x2 square positioning -T-piece | -1 | Optimal T-shape entry timing -S-piece | -1 | Natural S-shape appearance -Z-piece | -1 | Natural Z-shape appearance -J-piece | -1 | Optimal L-shape entry -L-piece | -1 | Optimal L-shape entry -``` - -## Visual Comparison - -### I-Piece Spawn Behavior - -```text -BEFORE (y = -1): AFTER (y = -2): -[ ] [ ] -[ โ–ˆ ] โ† I-piece here [ ] -[โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ] [ โ–ˆ ] โ† I-piece here -[โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ] [โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ] - -Problem: Abrupt entry Solution: Natural entry -``` - -### Other Pieces (Unchanged) - -```text -T-Piece Example (y = -1): -[ ] -[ โ–ˆโ–ˆโ–ˆ ] โ† T-piece entry (perfect) -[โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ] -[โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ] - -Status: No change needed โœ… -``` - -## Testing Results - -### 1. I-Piece Verification - -โœ… **Vertical Spawn**: I-piece now appears naturally from above -โœ… **Entry Animation**: Smooth transition into visible grid area -โœ… **Player Reaction**: More time to plan I-piece placement -โœ… **Hold Function**: I-piece maintains correct positioning when held/swapped - -### 2. Other Pieces Validation - -โœ… **O-Piece**: Maintains perfect 2x2 positioning -โœ… **T-Piece**: Optimal T-shape entry preserved -โœ… **S/Z-Pieces**: Natural zigzag entry unchanged -โœ… **J/L-Pieces**: L-shape entry timing maintained - -### 3. Game Mechanics - -โœ… **Spawn Consistency**: Each piece type uses appropriate spawn height -โœ… **Hold System**: Piece-specific positioning applied correctly -โœ… **Bag Randomizer**: Next piece preview uses correct spawn height -โœ… **Game Flow**: Smooth progression for all piece types - -## Benefits Summary - -### 1. Improved I-Piece Experience - -- **Natural Entry**: I-piece now enters the play area smoothly -- **Better Timing**: More reaction time for strategic placement -- **Visual Polish**: Professional appearance matching commercial Tetris games - -### 2. Preserved Gameplay Balance - -- **Other Pieces Unchanged**: Maintain optimal spawn positions for 6 other piece types -- **Consistent Feel**: Each piece type gets appropriate treatment -- **Strategic Depth**: I-piece becomes more strategic without affecting other pieces - -### 3. Technical Excellence - -- **Minimal Changes**: Targeted fix without broad system changes -- **Future-Proof**: Easy to adjust individual piece spawn behavior -- **Code Quality**: Clear, maintainable piece-type logic - -## Status: โœ… COMPLETED - -- **I-Piece**: Now spawns at y = -2 for natural vertical entry -- **Other Pieces**: Remain at y = -1 for optimal gameplay -- **Hold System**: Updated to handle piece-specific spawn positions -- **Next Piece**: Preview system uses correct spawn heights -- **Testing**: Validated all piece types work correctly - -## Conclusion - -The I-piece now provides a much more natural gameplay experience with proper entry timing, while all other pieces maintain their optimal spawn positions. This targeted fix addresses the specific issue without disrupting the carefully balanced gameplay of other tetrominos. diff --git a/LAYOUT_IMPROVEMENTS.md b/LAYOUT_IMPROVEMENTS.md deleted file mode 100644 index be9f0b0..0000000 --- a/LAYOUT_IMPROVEMENTS.md +++ /dev/null @@ -1,165 +0,0 @@ -# Gameplay Layout Improvements - Enhancement Report - -## Overview - -Enhanced the gameplay state visual layout by repositioning the next piece preview higher above the main grid and adding subtle grid lines to improve gameplay visibility. - -## Changes Made - -### 1. Next Piece Preview Repositioning - -**Problem**: The next piece preview was positioned too close to the main game grid, causing visual overlap and crowded appearance. - -**Solution**: - -```cpp -// BEFORE: Limited space for next piece -const float NEXT_PIECE_HEIGHT = 80.0f; // Space reserved for next piece preview - -// AFTER: More breathing room -const float NEXT_PIECE_HEIGHT = 120.0f; // Space reserved for next piece preview (increased) -``` - -### Result - -- โœ… **50% more space** above the main grid (80px โ†’ 120px) -- โœ… **Clear separation** between next piece and main game area -- โœ… **Better visual hierarchy** in the gameplay layout - -### 2. Main Grid Visual Enhancement - -**Problem**: The main game grid lacked visual cell boundaries, making it difficult to precisely judge piece placement. - -**Solution**: Added subtle grid lines to show cell boundaries - -```cpp -// Draw grid lines (subtle lines to show cell boundaries) -SDL_SetRenderDrawColor(renderer, 40, 45, 60, 255); // Slightly lighter than background - -// Vertical grid lines -for (int x = 1; x < Game::COLS; ++x) { - float lineX = gridX + x * finalBlockSize + contentOffsetX; - SDL_RenderLine(renderer, lineX, gridY + contentOffsetY, lineX, gridY + GRID_H + contentOffsetY); -} - -// Horizontal grid lines -for (int y = 1; y < Game::ROWS; ++y) { - float lineY = gridY + y * finalBlockSize + contentOffsetY; - SDL_RenderLine(renderer, gridX + contentOffsetX, lineY, gridX + GRID_W + contentOffsetX, lineY); -} -``` - -### Grid Line Properties - -- **Color**: `RGB(40, 45, 60)` - Barely visible, subtle enhancement -- **Coverage**: Complete 10ร—20 grid with proper cell boundaries -- **Performance**: Minimal overhead with simple line drawing -- **Visual Impact**: Clear cell separation without visual noise - -## Technical Details - -### Layout Calculation System - -The responsive layout system maintains perfect centering while accommodating the increased next piece space: - -```cpp -// Layout Components: -- Top Margin: 60px (UI spacing) -- Next Piece Area: 120px (increased from 80px) -- Main Grid: Dynamic based on window size -- Bottom Margin: 60px (controls text) -- Side Panels: 180px each (stats and score) -``` - -### Grid Line Implementation - -- **Rendering Order**: Background โ†’ Grid Lines โ†’ Game Blocks โ†’ UI Elements -- **Line Style**: Single pixel lines with subtle contrast -- **Integration**: Seamlessly integrated with existing rendering pipeline -- **Responsiveness**: Automatically scales with dynamic block size - -## Visual Benefits - -### 1. Improved Gameplay Precision - -- **Grid Boundaries**: Clear visual cell separation for accurate piece placement -- **Alignment Reference**: Easy to judge piece positioning and rotation -- **Professional Appearance**: Matches modern Tetris game standards - -### 2. Enhanced Layout Flow - -- **Next Piece Visibility**: Higher positioning prevents overlap with main game -- **Visual Balance**: Better proportions between UI elements -- **Breathing Room**: More comfortable spacing throughout the interface - -### 3. Accessibility Improvements - -- **Visual Clarity**: Subtle grid helps players with visual impairments -- **Reduced Eye Strain**: Better element separation reduces visual confusion -- **Gameplay Flow**: Smoother visual transitions between game areas - -## Comparison: Before vs After - -### Next Piece Positioning - -```text -BEFORE: [Next Piece] - [Main Grid ] โ† Too close, visual overlap - -AFTER: [Next Piece] - - [Main Grid ] โ† Proper separation, clear hierarchy -``` - -### Grid Visibility - -```text -BEFORE: Solid background, no cell boundaries - โ– โ– โ– โ– โ– โ– โ– โ– โ– โ–  - โ– โ– โ– โ– โ– โ– โ– โ– โ– โ–  โ† Difficult to judge placement - -AFTER: Subtle grid lines show cell boundaries - โ”Œโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ” - โ”œโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ค โ† Easy placement reference - โ””โ”€โ”ดโ”€โ”ดโ”€โ”ดโ”€โ”ดโ”€โ”ดโ”€โ”ดโ”€โ”ดโ”€โ”ดโ”€โ”ดโ”€โ”˜ -``` - -## Testing Results - -โœ… **Build Success**: Clean compilation with no errors -โœ… **Visual Layout**: Next piece properly positioned above grid -โœ… **Grid Lines**: Subtle, barely visible lines enhance gameplay -โœ… **Responsive Design**: All improvements work across window sizes -โœ… **Performance**: No measurable impact on frame rate -โœ… **Game Logic**: All existing functionality preserved - -## User Experience Impact - -### 1. Gameplay Improvements - -- **Precision**: Easier to place pieces exactly where intended -- **Speed**: Faster visual assessment of game state -- **Confidence**: Clear visual references reduce placement errors - -### 2. Visual Polish - -- **Professional**: Matches commercial Tetris game standards -- **Modern**: Clean, organized interface layout -- **Consistent**: Maintains established visual design language - -### 3. Accessibility - -- **Clarity**: Better visual separation for all users -- **Readability**: Improved layout hierarchy -- **Comfort**: Reduced visual strain during extended play - -## Conclusion - -The layout improvements successfully address the visual overlap issue with the next piece preview and add essential grid lines for better gameplay precision. The changes maintain the responsive design system while providing a more polished and professional gaming experience. - -## Status: โœ… COMPLETED - -- Next piece repositioned higher above main grid -- Subtle grid lines added to main game board -- Layout maintains responsive design and perfect centering -- All existing functionality preserved with enhanced visual clarity diff --git a/LOADING_FIX_SUMMARY.md b/LOADING_FIX_SUMMARY.md deleted file mode 100644 index 877fc05..0000000 --- a/LOADING_FIX_SUMMARY.md +++ /dev/null @@ -1,29 +0,0 @@ -# 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/LOADING_PROGRESS_FIX.md b/LOADING_PROGRESS_FIX.md deleted file mode 100644 index 9f23312..0000000 --- a/LOADING_PROGRESS_FIX.md +++ /dev/null @@ -1,138 +0,0 @@ -# Loading Progress Fix - Issue Resolution - -## Problem Identified -The loading progress was reaching 157% instead of stopping at 100%. This was caused by a mismatch between: -- **Expected tracks**: 11 (hardcoded `totalTracks = 11`) -- **Actual music files**: 24 total files (but only 11 numbered music tracks) - -## Root Cause Analysis - -### File Structure in `assets/music/`: -``` -Numbered Music Tracks (Background Music): -- music001.mp3 through music011.mp3 (11 files) - -Sound Effect Files: -- amazing.mp3, boom_tetris.mp3, great_move.mp3, impressive.mp3 -- keep_that_ryhtm.mp3, lets_go.mp3, nice_combo.mp3, smooth_clear.mp3 -- triple_strike.mp3, well_played.mp3, wonderful.mp3, you_fire.mp3 -(13 sound effect files) -``` - -### Issue Details: -1. **Hardcoded Count**: `totalTracks` was fixed at 11 -2. **Audio Loading**: The system was actually loading more than 11 files -3. **Progress Calculation**: `currentTrackLoading / totalTracks * 0.7` exceeded 0.7 when `currentTrackLoading > 11` -4. **Result**: Progress went beyond 100% (up to ~157%) - -## Solution Implemented - -### 1. Dynamic Track Counting -```cpp -// BEFORE: Fixed count -const int totalTracks = 11; - -// AFTER: Dynamic detection -int totalTracks = 0; // Will be set dynamically based on actual files -``` - -### 2. File Detection Logic -```cpp -// Count actual numbered music files (music001.mp3, music002.mp3, etc.) -totalTracks = 0; -for (int i = 1; i <= 100; ++i) { - char buf[64]; - std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i); - - // Check if file exists - SDL_IOStream* file = SDL_IOFromFile(buf, "rb"); - if (file) { - SDL_CloseIO(file); - totalTracks++; - } else { - break; // No more consecutive files - } -} -``` - -### 3. Progress Calculation Safety -```cpp -// BEFORE: Could exceed 100% -double musicProgress = musicLoaded ? 0.7 : (double)currentTrackLoading / totalTracks * 0.7; - -// AFTER: Capped at maximum values -double musicProgress = 0.0; -if (totalTracks > 0) { - musicProgress = musicLoaded ? 0.7 : std::min(0.7, (double)currentTrackLoading / totalTracks * 0.7); -} - -// Additional safety check -loadingProgress = std::min(1.0, loadingProgress); -``` - -## Technical Verification - -### Test Results: -โœ… **Track Detection**: Correctly identifies 11 numbered music tracks -โœ… **Progress Calculation**: 0/11 โ†’ 11/11 (never exceeds denominator) -โœ… **Loading Phases**: 20% (assets) + 70% (music) + 10% (init) = 100% max -โœ… **Safety Bounds**: `std::min(1.0, loadingProgress)` prevents overflow -โœ… **Game Launch**: Smooth transition from loading to menu at exactly 100% - -### Debug Output (Removed in Final): -``` -Found 11 music tracks to load -Loading progress: 0/11 tracks loaded -... -Loading progress: 11/11 tracks loaded -All music tracks loaded successfully! -``` - -## Benefits of the Fix - -### 1. Accurate Progress Display -- **Before**: Could show 157% (confusing and broken) -- **After**: Always stops exactly at 100% (professional and accurate) - -### 2. Dynamic Adaptability -- **Before**: Hardcoded for exactly 11 tracks -- **After**: Automatically adapts to any number of numbered music tracks - -### 3. Asset Separation -- **Music Tracks**: Only numbered files (`music001.mp3` - `music011.mp3`) for background music -- **Sound Effects**: Named files (`amazing.mp3`, `boom_tetris.mp3`, etc.) handled separately - -### 4. Robust Error Handling -- **File Detection**: Safe file existence checking with proper resource cleanup -- **Progress Bounds**: Multiple safety checks prevent mathematical overflow -- **Loading Logic**: Graceful handling of missing or incomplete file sequences - -## Code Quality Improvements - -### 1. Resource Management -```cpp -SDL_IOStream* file = SDL_IOFromFile(buf, "rb"); -if (file) { - SDL_CloseIO(file); // Proper cleanup - totalTracks++; -} -``` - -### 2. Mathematical Safety -```cpp -loadingProgress = std::min(1.0, loadingProgress); // Never exceed 100% -``` - -### 3. Clear Phase Separation -```cpp -// Phase 1: Assets (20%) + Phase 2: Music (70%) + Phase 3: Init (10%) = 100% -``` - -## Conclusion -The loading progress now correctly shows 0% โ†’ 100% progression, with proper file detection, safe mathematical calculations, and clean separation between background music tracks and sound effect files. The system is now robust and will adapt automatically if music tracks are added or removed. - -## Status: โœ… RESOLVED -- Loading progress fixed: Never exceeds 100% -- Dynamic track counting: Adapts to actual file count -- Code quality: Improved safety and resource management -- User experience: Professional loading screen with accurate progress diff --git a/REFACTORING_ANALYSIS.md b/REFACTORING_ANALYSIS.md deleted file mode 100644 index 81f9925..0000000 --- a/REFACTORING_ANALYSIS.md +++ /dev/null @@ -1,429 +0,0 @@ -# Tetris C++ SDL3 - Code Refactoring Analysis - -## Executive Summary - -The `main.cpp` file currently serves as both the application entry point and contains a significant amount of game logic, rendering code, and UI management. This violates several SOLID principles and makes the codebase difficult to maintain, test, and extend. This analysis provides a comprehensive refactoring plan to improve code organization, maintainability, and adherence to best practices. - -## Current Issues Analysis - -### 1. **Single Responsibility Principle (SRP) Violations** - -**Current State:** The `main()` function handles: -- SDL initialization and cleanup -- Asset loading (textures, fonts, sounds) -- Game loop management -- Event handling for multiple states -- Rendering logic for all game states -- State management -- Input handling (keyboard, mouse) -- Audio management -- Background effects management - -**Impact:** -- Function is ~1,678 lines long -- Difficult to test individual components -- High coupling between unrelated functionality -- Hard to debug specific issues - -### 2. **Open/Closed Principle (OCP) Violations** - -**Current Issues:** -- Adding new game states requires modifying the main loop -- New input handling requires changing the main event handling code -- New rendering features require modifying the massive switch statement -- Hard-coded state transitions throughout the main function - -### 3. **Dependency Inversion Principle (DIP) Violations** - -**Current Issues:** -- Direct instantiation of concrete classes in main -- Tight coupling to SDL-specific implementations -- No abstraction layer for rendering, audio, or input systems -- Global state variables scattered throughout - -### 4. **Code Organization Issues** - -**Current Problems:** -- Mixing of high-level application logic with low-level SDL code -- Inline lambda functions making code hard to read -- Global static variables and functions -- No clear separation between initialization, game loop, and cleanup -- Duplicated code in event handling -- Magic numbers and constants scattered throughout - -## Proposed Refactoring Strategy - -### Phase 1: Extract Core Managers - -#### 1.1 Application Manager -Create an `ApplicationManager` class to handle the application lifecycle: - -```cpp -class ApplicationManager { -public: - bool initialize(); - void run(); - void shutdown(); - -private: - std::unique_ptr m_renderManager; - std::unique_ptr m_inputManager; - std::unique_ptr m_stateManager; - std::unique_ptr m_assetManager; - std::unique_ptr m_audioManager; -}; -``` - -#### 1.2 Render Manager -Extract all rendering logic: - -```cpp -class RenderManager { -public: - bool initialize(int width, int height); - void beginFrame(); - void endFrame(); - void setViewport(const Viewport& viewport); - void renderTexture(SDL_Texture* texture, const Rect& src, const Rect& dst); - void renderRect(const Rect& rect, const Color& color); - -private: - SDL_Window* m_window; - SDL_Renderer* m_renderer; - Viewport m_currentViewport; -}; -``` - -#### 1.3 Input Manager -Centralize input handling: - -```cpp -class InputManager { -public: - void processEvents(); - bool isKeyPressed(SDL_Scancode key) const; - bool isKeyHeld(SDL_Scancode key) const; - Point getMousePosition() const; - bool isMouseButtonPressed(int button) const; - - void registerKeyHandler(SDL_Scancode key, std::function handler); - void registerMouseHandler(int button, std::function handler); - -private: - std::unordered_map m_keyStates; - std::unordered_map m_previousKeyStates; - Point m_mousePosition; - std::unordered_map m_mouseStates; -}; -``` - -#### 1.4 Asset Manager -Manage all game assets: - -```cpp -class AssetManager { -public: - bool loadTexture(const std::string& name, const std::string& path); - bool loadFont(const std::string& name, const std::string& path, int size); - bool loadSound(const std::string& name, const std::string& path); - - SDL_Texture* getTexture(const std::string& name) const; - FontAtlas* getFont(const std::string& name) const; - // etc. - -private: - std::unordered_map> m_textures; - std::unordered_map> m_fonts; - // etc. -}; -``` - -### Phase 2: Improve State Management - -#### 2.1 Enhanced State Pattern -Improve the current state system: - -```cpp -class IGameState { -public: - virtual ~IGameState() = default; - virtual void onEnter() = 0; - virtual void onExit() = 0; - virtual void update(float deltaTime) = 0; - virtual void render(RenderManager& renderer) = 0; - virtual void handleEvent(const SDL_Event& event) = 0; - virtual AppState getType() const = 0; -}; - -class StateManager { -public: - void pushState(std::unique_ptr state); - void popState(); - void changeState(std::unique_ptr state); - void update(float deltaTime); - void render(RenderManager& renderer); - void handleEvent(const SDL_Event& event); - -private: - std::stack> m_states; -}; -``` - -#### 2.2 State Factory -Create states using a factory pattern: - -```cpp -class StateFactory { -public: - static std::unique_ptr createState( - AppState type, - const StateContext& context - ); - -private: - static std::unordered_map(const StateContext&)>> m_creators; -}; -``` - -### Phase 3: Separate Rendering Systems - -#### 3.1 UI Renderer -Extract UI rendering to separate class: - -```cpp -class UIRenderer { -public: - void renderButton(const Button& button); - void renderPopup(const Popup& popup); - void renderProgressBar(const ProgressBar& progressBar); - void renderMenu(const Menu& menu); - -private: - RenderManager& m_renderManager; - AssetManager& m_assetManager; -}; -``` - -#### 3.2 Game Renderer -Separate game-specific rendering: - -```cpp -class GameRenderer { -public: - void renderGameBoard(const Game& game, const GameLayout& layout); - void renderGamePiece(const Game::Piece& piece, const Transform& transform); - void renderBackground(BackgroundType type, int level); - void renderHUD(const GameStats& stats, const HUDLayout& layout); - -private: - RenderManager& m_renderManager; - AssetManager& m_assetManager; -}; -``` - -### Phase 4: Configuration Management - -#### 4.1 Configuration System -Create a centralized configuration system: - -```cpp -class ConfigManager { -public: - static ConfigManager& getInstance(); - - template - T getValue(const std::string& key, const T& defaultValue = T{}) const; - - template - void setValue(const std::string& key, const T& value); - - bool loadFromFile(const std::string& filename); - bool saveToFile(const std::string& filename); - -private: - std::unordered_map m_values; -}; -``` - -#### 4.2 Constants Organization -Move magic numbers to configuration: - -```cpp -namespace Config { - namespace Window { - constexpr int DEFAULT_WIDTH = 1200; - constexpr int DEFAULT_HEIGHT = 1000; - } - - namespace Gameplay { - constexpr double DAS_DELAY = 170.0; - constexpr double ARR_RATE = 40.0; - constexpr float LEVEL_FADE_DURATION = 3500.0f; - } - - namespace UI { - constexpr float MIN_MARGIN = 40.0f; - constexpr float PANEL_WIDTH = 180.0f; - constexpr float PANEL_SPACING = 30.0f; - } -} -``` - -### Phase 5: Event System - -#### 5.1 Event Bus -Implement a decoupled event system: - -```cpp -template -class EventBus { -public: - using Handler = std::function; - using HandlerId = uint32_t; - - HandlerId subscribe(Handler handler); - void unsubscribe(HandlerId id); - void publish(const EventType& event); - -private: - std::unordered_map m_handlers; - HandlerId m_nextId = 1; -}; -``` - -#### 5.2 Game Events -Define specific game events: - -```cpp -struct GameEvents { - struct LevelUp { int newLevel; }; - struct LineCleared { int linesCleared; std::vector clearedRows; }; - struct GameOver { int finalScore; int totalLines; int finalLevel; }; - struct PieceSpawned { PieceType type; }; - struct PieceLocked { Game::Piece piece; }; -}; -``` - -### Phase 6: Dependency Injection - -#### 6.1 Service Container -Implement a simple service container: - -```cpp -class ServiceContainer { -public: - template - void registerService(std::shared_ptr service); - - template - std::shared_ptr getService() const; - - template - bool hasService() const; - -private: - std::unordered_map> m_services; -}; -``` - -## Implementation Priority - -### High Priority (Critical Issues) -1. **Extract Application Manager** - Reduce main() function complexity -2. **Separate Render Manager** - Abstract SDL rendering code -3. **Improve State Management** - Make states truly independent -4. **Extract Input Manager** - Centralize input handling - -### Medium Priority (Maintainability) -5. **Configuration System** - Remove magic numbers -6. **Asset Manager** - Centralize resource management -7. **UI Renderer** - Separate UI from game rendering -8. **Event System** - Decouple components - -### Low Priority (Quality of Life) -9. **Service Container** - Improve dependency management -10. **Logging System** - Better debugging support -11. **Performance Profiler** - Identify bottlenecks -12. **Unit Testing Framework** - Ensure code quality - -## Benefits of Refactoring - -### Code Quality -- **Reduced Complexity**: Main function becomes ~50 lines instead of 1,678 -- **Better Testability**: Each component can be unit tested in isolation -- **Improved Maintainability**: Changes to one system don't affect others -- **Enhanced Readability**: Clear separation of concerns - -### Development Velocity -- **Easier Feature Addition**: New states/features don't require main() changes -- **Parallel Development**: Multiple developers can work on different systems -- **Faster Debugging**: Issues isolated to specific components -- **Simplified Integration**: Clean interfaces between systems - -### System Reliability -- **Reduced Coupling**: Changes have limited blast radius -- **Better Error Handling**: Each system can handle its own errors -- **Resource Management**: Clear ownership and lifecycle management -- **Memory Safety**: RAII patterns throughout - -## Migration Strategy - -### Phase 1: Foundation (Week 1-2) -1. Create basic manager interfaces -2. Extract ApplicationManager with minimal functionality -3. Move SDL initialization/cleanup to RenderManager -4. Basic integration testing - -### Phase 2: Core Systems (Week 3-4) -1. Implement InputManager -2. Enhance StateManager -3. Create AssetManager -4. Update existing states to use new managers - -### Phase 3: Rendering (Week 5-6) -1. Extract UI rendering logic -2. Separate game rendering -3. Implement background management -4. Optimize rendering pipeline - -### Phase 4: Configuration (Week 7) -1. Implement configuration system -2. Move constants to config files -3. Add runtime configuration updates - -### Phase 5: Events (Week 8) -1. Implement event bus -2. Convert callbacks to events -3. Decouple audio system - -### Phase 6: Polish (Week 9-10) -1. Add service container -2. Implement comprehensive logging -3. Add performance monitoring -4. Complete documentation - -## Testing Strategy - -### Unit Tests -- Test each manager class in isolation -- Mock dependencies using interfaces -- Test edge cases and error conditions -- Achieve >90% code coverage - -### Integration Tests -- Test manager interactions -- Verify state transitions -- Test complete game scenarios -- Performance regression testing - -### System Tests -- Full application testing -- User interaction scenarios -- Cross-platform compatibility -- Memory leak detection - -## Conclusion - -This refactoring plan addresses the major architectural issues in the current codebase while maintaining compatibility with existing functionality. The modular approach allows for incremental implementation without breaking the existing game. The end result will be a maintainable, testable, and extensible codebase that follows modern C++ best practices and SOLID principles. - -The investment in refactoring will pay dividends in reduced development time, fewer bugs, and easier feature implementation in the future. diff --git a/REFACTORING_TODO.md b/REFACTORING_TODO.md deleted file mode 100644 index e30ad8b..0000000 --- a/REFACTORING_TODO.md +++ /dev/null @@ -1,287 +0,0 @@ -# Tetris Refactoring TODO List - -## ๐Ÿš€ Phase 1: Foundation (Week 1-2) - CRITICAL โœ… COMPLETED - -### Core Architecture Setup - -- [x] Create `ApplicationManager` class - - [x] Design interface and basic structure - - [x] Implement initialize(), run(), shutdown() methods - - [x] Move SDL initialization from main() to ApplicationManager - - [x] Add proper error handling and cleanup - - [x] Test basic application lifecycle - -- [x] Extract `RenderManager` class - - [x] Create rendering abstraction layer - - [x] Move SDL_Window and SDL_Renderer management - - [x] Implement viewport and scaling logic - - [x] Add texture and primitive rendering methods - - [x] Test rendering pipeline isolation - -- [x] Implement basic `StateManager` improvements - - [x] Enhance current StateManager with stack support - - [x] Add state validation and error handling - - [x] Implement proper state lifecycle management - - [x] Test state transitions thoroughly - -**โœ… MILESTONE ACHIEVED**: Core architecture foundation is complete! We now have: -- Clean separation between main() and application logic -- Abstracted rendering system -- Enhanced state management with proper lifecycle -- Working refactored application (`tetris_refactored.exe`) alongside original -- Proper error handling and logging throughout - -### Immediate Code Cleanup โœ… **COMPLETED** - -- [x] Remove global variables from main.cpp - - [x] Move static variables to appropriate managers (GlobalState) - - [x] Remove global texture and font variables (AssetManager) - - [x] Eliminate global state flags (GlobalState singleton) - - [x] Clean up static function declarations - -- [x] Extract constants to configuration - - [x] Create Config namespace with all constants - - [x] Remove magic numbers from main.cpp - - [x] Define window size constants - - [x] Set up gameplay timing constants - -## ๐Ÿ”ง Phase 2: Core Systems (Week 3-4) - HIGH PRIORITY โœ… **COMPLETED** - -### Input Management โœ… **COMPLETED** -- [x] Create `InputManager` class - - [x] Implement keyboard state tracking - - [x] Add mouse input handling - - [x] Create event handler registration system - - [x] Implement DAS/ARR logic in InputManager - - [x] Test input responsiveness and accuracy - -- [x] Refactor event handling in main() - - [x] Move SDL event polling to InputManager - - [x] Replace inline event handlers with registered callbacks - - [x] Simplify main loop event processing - - [x] Test all input scenarios (keyboard, mouse, gamepad) - -### Asset Management โœ… **COMPLETED** - -- [x] Create `AssetManager` class - - [x] Design resource loading interface - - [x] Implement texture management - - [x] Add font loading and management - - [x] Create sound asset handling - - [x] Add resource cleanup and error handling - -- [x] Migrate existing asset loading - - [x] Move texture loading from main() to AssetManager - - [x] Convert font initialization to use AssetManager - - [x] Update StateContext to use AssetManager - - [x] Test asset loading and memory management - -### State System Enhancement -- [ ] Implement `IGameState` interface - - [ ] Define clear state contract - - [ ] Add update(), render(), handleEvent() methods - - [ ] Implement proper state lifecycle - - [ ] Add state type identification - -- [ ] Create `StateFactory` pattern - - [ ] Design state creation interface - - [ ] Implement factory registration system - - [ ] Add state parameter passing - - [ ] Test dynamic state creation - -## ๐ŸŽจ Phase 3: Rendering Systems (Week 5-6) - MEDIUM PRIORITY - -### UI Rendering Separation -- [ ] Create `UIRenderer` class - - [ ] Extract button rendering logic - - [ ] Implement popup rendering system - - [ ] Add progress bar rendering - - [ ] Create menu rendering components - - [ ] Test UI rendering consistency - -- [ ] Refactor state rendering - - [ ] Update LoadingState to use UIRenderer - - [ ] Modify MenuState rendering - - [ ] Enhance LevelSelectorState visuals - - [ ] Test all state rendering paths - -### Game Rendering Optimization -- [ ] Create `GameRenderer` class - - [ ] Extract game board rendering - - [ ] Implement piece rendering system - - [ ] Add background management - - [ ] Create HUD rendering components - - [ ] Optimize rendering performance - -- [ ] Enhance visual effects - - [ ] Improve line clearing effects - - [ ] Add piece drop animations - - [ ] Enhance particle systems - - [ ] Implement screen transitions - -### Background System -- [ ] Create `BackgroundManager` class - - [ ] Implement level-based backgrounds - - [ ] Add background caching system - - [ ] Create smooth transitions - - [ ] Test background loading performance - -## โš™๏ธ Phase 4: Configuration (Week 7) - MEDIUM PRIORITY - -### Configuration System -- [ ] Implement `ConfigManager` class - - [ ] Design configuration storage - - [ ] Add file loading/saving - - [ ] Implement runtime configuration updates - - [ ] Add configuration validation - - [ ] Test configuration persistence - -- [ ] Migrate constants - - [ ] Move window size settings - - [ ] Convert gameplay constants - - [ ] Update UI layout values - - [ ] Set up default configurations - -### Settings Management -- [ ] Create settings persistence - - [ ] Save audio preferences - - [ ] Store control mappings - - [ ] Remember window settings - - [ ] Test settings across restarts - -## ๐Ÿ“ก Phase 5: Event System (Week 8) - MEDIUM PRIORITY - -### Event Bus Implementation -- [ ] Create `EventBus` template class - - [ ] Implement subscription system - - [ ] Add event publishing mechanism - - [ ] Create handler management - - [ ] Test event delivery reliability - -- [ ] Define Game Events - - [ ] Create LevelUp event - - [ ] Implement LineCleared event - - [ ] Add GameOver event - - [ ] Define PieceSpawned event - - [ ] Test event system integration - -### Audio System Decoupling -- [ ] Convert audio callbacks to events - - [ ] Replace sound callbacks with event handlers - - [ ] Implement music management events - - [ ] Add audio configuration events - - [ ] Test audio system responsiveness - -## ๐Ÿ”Œ Phase 6: Dependency Injection (Week 9) - LOW PRIORITY - -### Service Container -- [ ] Implement `ServiceContainer` class - - [ ] Design service registration - - [ ] Add dependency resolution - - [ ] Implement service lifecycle - - [ ] Test service dependencies - -- [ ] Migrate to dependency injection - - [ ] Update manager constructors - - [ ] Implement service interfaces - - [ ] Add service configuration - - [ ] Test dependency resolution - -## ๐Ÿงช Phase 7: Testing & Quality (Week 10) - LOW PRIORITY - -### Unit Testing Setup -- [ ] Set up testing framework - - [ ] Configure Google Test or Catch2 - - [ ] Create test project structure - - [ ] Add basic test utilities - - [ ] Set up automated test running - -- [ ] Write core tests - - [ ] Test ApplicationManager lifecycle - - [ ] Validate StateManager transitions - - [ ] Test InputManager functionality - - [ ] Verify AssetManager operations - - [ ] Test ConfigManager persistence - -### Integration Testing -- [ ] Create integration test suite - - [ ] Test manager interactions - - [ ] Validate state transitions - - [ ] Test complete game scenarios - - [ ] Run performance regression tests - -### Code Quality -- [ ] Add static analysis - - [ ] Configure clang-tidy - - [ ] Set up cppcheck - - [ ] Add memory leak detection - - [ ] Run code coverage analysis - -## ๐Ÿ”ง Optimization Tasks - ONGOING - -### Performance Optimization -- [ ] Profile rendering performance - - [ ] Identify rendering bottlenecks - - [ ] Optimize texture usage - - [ ] Improve draw call batching - - [ ] Test frame rate consistency - -- [ ] Memory optimization - - [ ] Analyze memory usage patterns - - [ ] Optimize asset loading - - [ ] Implement memory pooling where needed - - [ ] Test for memory leaks - -### Code Quality Improvements -- [ ] Documentation - - [ ] Add comprehensive class documentation - - [ ] Document API interfaces - - [ ] Create usage examples - - [ ] Write development guidelines - -- [ ] Error Handling - - [ ] Implement consistent error handling - - [ ] Add logging system - - [ ] Create error recovery mechanisms - - [ ] Test error scenarios - -### Platform Compatibility -- [ ] Cross-platform testing - - [ ] Test on Windows - - [ ] Validate Linux compatibility - - [ ] Check macOS support - - [ ] Test different screen resolutions - -## ๐Ÿ“Š Progress Tracking - -### Metrics to Monitor -- [ ] **Code Complexity**: Reduce main() from 1,678 to <100 lines -- [ ] **Test Coverage**: Achieve >90% code coverage -- [ ] **Build Time**: Maintain fast compilation -- [ ] **Performance**: No regression in frame rate -- [ ] **Memory Usage**: Stable memory consumption - -### Milestones -- [ ] **Milestone 1**: Basic managers extracted and working -- [ ] **Milestone 2**: State system fully refactored -- [ ] **Milestone 3**: Rendering system modularized -- [ ] **Milestone 4**: Configuration system implemented -- [ ] **Milestone 5**: Event system integrated -- [ ] **Milestone 6**: All tests passing, full documentation - -### Success Criteria -- [ ] Main function reduced to application setup only -- [ ] All components unit testable -- [ ] Easy to add new features without modifying core code -- [ ] Clear separation of concerns throughout codebase -- [ ] Comprehensive documentation and examples - ---- - -## ๐Ÿ“ Notes - -- **Priority levels**: CRITICAL = Must complete, HIGH = Important for maintainability, MEDIUM = Quality improvements, LOW = Nice to have -- **Estimated timeline**: 10 weeks for complete refactoring -- **Risk mitigation**: Each phase should maintain working application -- **Testing strategy**: Test after each major change to ensure no regressions -- **Documentation**: Update as you go, don't leave it for the end diff --git a/SOUND_EFFECTS_IMPLEMENTATION.md b/SOUND_EFFECTS_IMPLEMENTATION.md deleted file mode 100644 index dd4c072..0000000 --- a/SOUND_EFFECTS_IMPLEMENTATION.md +++ /dev/null @@ -1,118 +0,0 @@ -# Sound Effects Implementation - -## Overview -This document describes the sound effects system implemented in the SDL C++ Tetris project, ported from the JavaScript version. - -## Sound Effects Implemented - -### 1. Line Clear Sounds -- **Basic Line Clear**: `clear_line.wav` - Plays for all line clears (1-4 lines) -- **Voice Feedback**: Plays after the basic sound with a slight delay - -### 2. Voice Lines by Line Count - -#### Single Line Clear -- No specific voice lines (only basic clear sound plays) - -#### Double Line Clear (2 lines) -- `nice_combo.mp3` - "Nice combo" -- `you_fire.mp3` - "You're on fire" -- `well_played.mp3` - "Well played" -- `keep_that_ryhtm.mp3` - "Keep that rhythm" (note: typo preserved from original) - -#### Triple Line Clear (3 lines) -- `great_move.mp3` - "Great move" -- `smooth_clear.mp3` - "Smooth clear" -- `impressive.mp3` - "Impressive" -- `triple_strike.mp3` - "Triple strike" - -#### Tetris (4 lines) -- `amazing.mp3` - "Amazing" -- `you_re_unstoppable.mp3` - "You're unstoppable" -- `boom_tetris.mp3` - "Boom! Tetris!" -- `wonderful.mp3` - "Wonderful" - -### 3. Level Up Sound -- `lets_go.mp3` - "Let's go" - Plays when the player advances to a new level - -## Implementation Details - -### Core Classes -1. **SoundEffect**: Handles individual sound file loading and playback - - Supports both WAV and MP3 formats - - Uses SDL3 audio streams for playback - - Volume control per sound effect - -2. **SoundEffectManager**: Manages all sound effects - - Singleton pattern for global access - - Random selection from sound groups - - Master volume and enable/disable controls - -### Audio Pipeline -1. **Loading**: Sound files are loaded during game initialization - - WAV files use SDL's native loading - - MP3 files use Windows Media Foundation (Windows only) - - All audio is converted to 16-bit stereo 44.1kHz - -2. **Playback**: Uses SDL3 audio streams - - Each sound effect can be played independently - - Volume mixing with master volume control - - Non-blocking playback for game responsiveness - -### Integration with Game Logic -- **Line Clear Callback**: Game class triggers sound effects when lines are cleared -- **Level Up Callback**: Triggered when player advances levels -- **Random Selection**: Multiple voice lines for same event are randomly selected - -### Controls -- **M Key**: Toggle background music on/off -- **S Key**: Toggle sound effects on/off -- Settings popup shows current status of both music and sound effects - -### JavaScript Compatibility -The implementation matches the JavaScript version exactly: -- Same sound files used -- Same triggering conditions (line counts, level ups) -- Same random selection behavior for voice lines -- Same volume levels and mixing - -## Audio Files Structure -``` -assets/music/ -โ”œโ”€โ”€ clear_line.wav # Basic line clear sound -โ”œโ”€โ”€ nice_combo.mp3 # Double line voice -โ”œโ”€โ”€ you_fire.mp3 # Double line voice -โ”œโ”€โ”€ well_played.mp3 # Double line voice -โ”œโ”€โ”€ keep_that_ryhtm.mp3 # Double line voice (typo preserved) -โ”œโ”€โ”€ great_move.mp3 # Triple line voice -โ”œโ”€โ”€ smooth_clear.mp3 # Triple line voice -โ”œโ”€โ”€ impressive.mp3 # Triple line voice -โ”œโ”€โ”€ triple_strike.mp3 # Triple line voice -โ”œโ”€โ”€ amazing.mp3 # Tetris voice -โ”œโ”€โ”€ you_re_unstoppable.mp3 # Tetris voice -โ”œโ”€โ”€ boom_tetris.mp3 # Tetris voice -โ”œโ”€โ”€ wonderful.mp3 # Tetris voice -โ””โ”€โ”€ lets_go.mp3 # Level up sound -``` - -## Technical Notes - -### Platform Support -- **Windows**: Full MP3 support via Windows Media Foundation -- **Other platforms**: WAV support only (MP3 requires additional libraries) - -### Performance -- All sounds are pre-loaded during initialization -- Minimal CPU overhead during gameplay -- SDL3 handles audio mixing and buffering - -### Memory Usage -- Sound effects are kept in memory for instant playback -- Total memory usage approximately 50-100MB for all effects -- Memory is freed on application shutdown - -## Future Enhancements -- Add sound effects for piece placement/movement -- Implement positional audio for stereo effects -- Add configurable volume levels per sound type -- Support for additional audio formats (OGG, FLAC) diff --git a/SPAWN_AND_FONT_IMPROVEMENTS.md b/SPAWN_AND_FONT_IMPROVEMENTS.md deleted file mode 100644 index e73de29..0000000 --- a/SPAWN_AND_FONT_IMPROVEMENTS.md +++ /dev/null @@ -1,182 +0,0 @@ -# Piece Spawning and Font Enhancement - Implementation Report - -## Overview -Fixed piece spawning position to appear within the grid boundaries and updated the gameplay UI to consistently use the PressStart2P retro pixel font for authentic Tetris aesthetics. - -## Changes Made - -### 1. Piece Spawning Position Fix - -**Problem**: New pieces were spawning at `y = -2`, causing them to appear above the visible grid area, which is non-standard for Tetris gameplay. - -**Solution**: Updated spawn position to `y = 0` (top of the grid) - -#### Files Modified: -- **`src/Game.cpp`** - `spawn()` function -- **`src/Game.cpp`** - `holdCurrent()` function - -#### Code Changes: -```cpp -// BEFORE: Pieces spawn above grid -cur = Piece{ bag.back(), 0, 3, -2 }; -nextPiece = Piece{ bag.back(), 0, 3, -2 }; - -// AFTER: Pieces spawn within grid -cur = Piece{ bag.back(), 0, 3, 0 }; // Spawn at top of visible grid -nextPiece = Piece{ bag.back(), 0, 3, 0 }; -``` - -#### Hold Function Updates: -```cpp -// BEFORE: Hold pieces reset above grid -hold.x = 3; hold.y = -2; hold.rot = 0; -cur.x = 3; cur.y = -2; cur.rot = 0; - -// AFTER: Hold pieces reset within grid -hold.x = 3; hold.y = 0; hold.rot = 0; // Within grid boundaries -cur.x = 3; cur.y = 0; cur.rot = 0; -``` - -### 2. Font System Enhancement - -**Goal**: Replace FreeSans font with PressStart2P for authentic retro gaming experience - -#### Updated UI Elements: -- **Next Piece Preview**: "NEXT" label -- **Statistics Panel**: "BLOCKS" header and piece counts -- **Score Panel**: "SCORE", "LINES", "LEVEL" headers and values -- **Progress Indicators**: "NEXT LVL" and line count -- **Time Display**: "TIME" header and timer -- **Hold System**: "HOLD" label -- **Pause Screen**: "PAUSED" text and resume instructions - -#### Font Scale Adjustments: -```cpp -// Optimized scales for PressStart2P readability -Headers (SCORE, LINES, etc.): 1.0f scale -Values (numbers, counts): 0.8f scale -Small labels: 0.7f scale -Pause text: 2.0f scale -``` - -#### Before vs After: -``` -BEFORE (FreeSans): AFTER (PressStart2P): -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ SCORE โ”‚ โ”‚ SCORE โ”‚ โ† Retro pixel font -โ”‚ 12,400 โ”‚ โ”‚ 12,400 โ”‚ โ† Monospace numbers -โ”‚ LINES โ”‚ โ”‚ LINES โ”‚ โ† Consistent styling -โ”‚ 042 โ”‚ โ”‚ 042 โ”‚ โ† Authentic feel -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -## Technical Benefits - -### 1. Standard Tetris Behavior -- **Proper Spawning**: Pieces now appear at the standard Tetris spawn position -- **Visible Entry**: Players can see pieces entering the game area -- **Collision Detection**: Improved accuracy for top-of-grid scenarios -- **Game Over Logic**: Clearer indication when pieces can't spawn - -### 2. Enhanced Visual Consistency -- **Unified Typography**: All gameplay elements use the same retro font -- **Authentic Aesthetics**: Matches classic arcade Tetris appearance -- **Professional Polish**: Consistent branding throughout the game -- **Improved Readability**: Monospace numbers for better score tracking - -### 3. Gameplay Improvements -- **Predictable Spawning**: Pieces always appear in the expected location -- **Strategic Planning**: Players can plan for pieces entering at the top -- **Reduced Confusion**: No more pieces appearing from above the visible area -- **Standard Experience**: Matches expectations from other Tetris games - -## Implementation Details - -### Spawn Position Logic -```cpp -// Standard Tetris spawning behavior: -// - X position: 3 (center of 10-wide grid) -// - Y position: 0 (top row of visible grid) -// - Rotation: 0 (default orientation) - -Piece newPiece = { pieceType, 0, 3, 0 }; -``` - -### Font Rendering Optimization -```cpp -// Consistent retro UI with optimized scales -pixelFont.draw(renderer, x, y, "SCORE", 1.0f, goldColor); // Headers -pixelFont.draw(renderer, x, y, "12400", 0.8f, whiteColor); // Values -pixelFont.draw(renderer, x, y, "5 LINES", 0.7f, whiteColor); // Details -``` - -## Testing Results - -### 1. Spawn Position Verification -โœ… **New pieces appear at grid top**: Visible within game boundaries -โœ… **Hold functionality**: Swapped pieces spawn correctly -โœ… **Game over detection**: Proper collision when grid is full -โœ… **Visual clarity**: No confusion about piece entry point - -### 2. Font Rendering Validation -โœ… **PressStart2P loading**: Font loads correctly from assets -โœ… **Text readability**: All UI elements clearly visible -โœ… **Scale consistency**: Proper proportions across different text sizes -โœ… **Color preservation**: Maintains original color scheme -โœ… **Performance**: No rendering performance impact - -### 3. User Experience Testing -โœ… **Gameplay flow**: Natural piece entry feels intuitive -โœ… **Visual appeal**: Retro aesthetic enhances game experience -โœ… **Information clarity**: Statistics and scores easily readable -โœ… **Professional appearance**: Polished, consistent UI design - -## Visual Comparison - -### Piece Spawning: -``` -BEFORE: [ โ– โ–  ] โ† Pieces appear above grid (confusing) - [ ] - [โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ] โ† Actual game grid - [โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ] - -AFTER: [ โ– โ–  ] โ† Pieces appear within grid (clear) - [โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ] โ† Visible entry point - [โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ] -``` - -### Font Aesthetics: -``` -BEFORE (FreeSans): AFTER (PressStart2P): -Modern, clean font 8-bit pixel perfect font -Variable spacing Monospace alignment -Smooth curves Sharp pixel edges -Generic appearance Authentic retro feel -``` - -## Benefits Summary - -### 1. Gameplay Standards -- **Tetris Compliance**: Matches standard Tetris piece spawning behavior -- **Player Expectations**: Familiar experience for Tetris players -- **Strategic Depth**: Proper visibility of incoming pieces - -### 2. Visual Enhancement -- **Retro Authenticity**: True 8-bit arcade game appearance -- **Consistent Branding**: Unified typography throughout gameplay -- **Professional Polish**: Commercial-quality visual presentation - -### 3. User Experience -- **Clarity**: Clear piece entry and movement -- **Immersion**: Enhanced retro gaming atmosphere -- **Accessibility**: Improved text readability and information hierarchy - -## Status: โœ… COMPLETED -- Piece spawning fixed: Y position changed from -2 to 0 -- Font system updated: PressStart2P implemented for gameplay UI -- Hold functionality: Updated to use correct spawn positions -- Visual consistency: All gameplay text uses retro pixel font -- Testing validated: Proper spawning behavior and enhanced aesthetics - -## Conclusion -The game now provides a proper Tetris experience with pieces spawning within the visible grid and a consistently retro visual presentation that enhances the classic arcade gaming atmosphere. diff --git a/SPAWN_AND_GRID_FIXES.md b/SPAWN_AND_GRID_FIXES.md deleted file mode 100644 index 6e5a4dc..0000000 --- a/SPAWN_AND_GRID_FIXES.md +++ /dev/null @@ -1,167 +0,0 @@ -# Spawn Position and Grid Line Alignment Fix - -## Overview -Fixed piece spawning to start one line higher (in the 2nd visible row) and corrected grid line alignment issues that occurred during window resizing and fullscreen mode. - -## Issues Resolved - -### 1. Piece Spawn Position Adjustment - -**Problem**: Pieces were spawning at the very top of the grid, user requested them to start one line higher (in the 2nd visible line). - -**Solution**: Changed spawn Y position from `0` to `-1` - -#### Code Changes: -```cpp -// BEFORE: Pieces spawn at top row (y = 0) -cur = Piece{ bag.back(), 0, 3, 0 }; -nextPiece = Piece{ bag.back(), 0, 3, 0 }; - -// AFTER: Pieces spawn one line higher (y = -1, appears in 2nd line) -cur = Piece{ bag.back(), 0, 3, -1 }; -nextPiece = Piece{ bag.back(), 0, 3, -1 }; -``` - -#### Files Modified: -- **`src/Game.cpp`** - `spawn()` function -- **`src/Game.cpp`** - `holdCurrent()` function - -**Result**: New pieces now appear in the 2nd visible line of the grid, giving players slightly more time to react and plan placement. - -### 2. Grid Line Alignment Fix - -**Problem**: Grid lines were appearing offset (to the right) instead of being properly centered within the game grid, especially noticeable during window resizing and fullscreen mode. - -**Root Cause**: Double application of content offsets - the grid position (`gridX`, `gridY`) already included content offsets, but the grid line drawing code was adding them again. - -#### Before (Incorrect): -```cpp -// Grid lines were offset due to double content offset application -float lineX = gridX + x * finalBlockSize + contentOffsetX; // โŒ contentOffsetX added twice -SDL_RenderLine(renderer, lineX, gridY + contentOffsetY, lineX, gridY + GRID_H + contentOffsetY); -``` - -#### After (Corrected): -```cpp -// Grid lines properly aligned within the grid boundaries -float lineX = gridX + x * finalBlockSize; // โœ… contentOffsetX already in gridX -SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H); -``` - -## Technical Details - -### Spawn Position Logic -```cpp -// Standard Tetris spawning with one-line buffer: -// - X position: 3 (center of 10-wide grid) -// - Y position: -1 (one line above top visible row) -// - Rotation: 0 (default orientation) - -Piece newPiece = { pieceType, 0, 3, -1 }; -``` - -### Grid Line Coordinate System -```cpp -// Proper coordinate calculation: -// gridX and gridY already include contentOffsetX/Y for centering -// Grid lines should be relative to these pre-offset coordinates - -// Vertical lines at each column boundary -for (int x = 1; x < Game::COLS; ++x) { - float lineX = gridX + x * finalBlockSize; - SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H); -} - -// Horizontal lines at each row boundary -for (int y = 1; y < Game::ROWS; ++y) { - float lineY = gridY + y * finalBlockSize; - SDL_RenderLine(renderer, gridX, lineY, gridX + GRID_W, lineY); -} -``` - -## Benefits - -### 1. Improved Gameplay Experience -- **Better Timing**: Pieces appear one line higher, giving players more reaction time -- **Strategic Advantage**: Slightly more space to plan piece placement -- **Standard Feel**: Matches many classic Tetris implementations - -### 2. Visual Consistency -- **Proper Grid Alignment**: Grid lines now perfectly align with cell boundaries -- **Responsive Design**: Grid lines maintain proper alignment during window resize -- **Fullscreen Compatibility**: Grid lines stay centered in fullscreen mode -- **Professional Appearance**: Clean, precise visual grid structure - -### 3. Technical Robustness -- **Coordinate System**: Simplified and corrected coordinate calculations -- **Responsive Layout**: Grid lines properly scale with dynamic block sizes -- **Window Management**: Handles all window states (windowed, maximized, fullscreen) - -## Testing Results - -### 1. Spawn Position Verification -โœ… **Visual Confirmation**: New pieces appear in 2nd visible line -โœ… **Gameplay Feel**: Improved reaction time and strategic planning -โœ… **Hold Function**: Held pieces also spawn at correct position -โœ… **Game Flow**: Natural progression from spawn to placement - -### 2. Grid Line Alignment Testing -โœ… **Windowed Mode**: Grid lines perfectly centered in normal window -โœ… **Resize Behavior**: Grid lines stay aligned during window resize -โœ… **Fullscreen Mode**: Grid lines maintain center alignment in fullscreen -โœ… **Dynamic Scaling**: Grid lines scale correctly with different block sizes - -### 3. Cross-Resolution Validation -โœ… **Multiple Resolutions**: Tested across various window sizes -โœ… **Aspect Ratios**: Maintains alignment in different aspect ratios -โœ… **Scaling Factors**: Proper alignment at all logical scale factors - -## Visual Comparison - -### Spawn Position: -``` -BEFORE: [โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ] โ† Pieces spawn here (top line) - [ ] - [โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ] - -AFTER: [ ] โ† Piece appears here first - [โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ] โ† Then moves into visible grid - [โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ] -``` - -### Grid Line Alignment: -``` -BEFORE (Offset): AFTER (Centered): -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ” -โ”‚ โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”€โ”ฌโ”‚ โ”œโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ค โ† Perfect alignment -โ”‚ โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”‚ โ”œโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ค -โ”‚ โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”€โ”ผโ”‚ โ””โ”€โ”ดโ”€โ”ดโ”€โ”ดโ”€โ”ดโ”€โ”ดโ”€โ”ดโ”€โ”ดโ”€โ”ดโ”€โ”ดโ”€โ”˜ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -โ†‘ Lines offset right โ†‘ Lines perfectly centered -``` - -## Impact on User Experience - -### 1. Gameplay Improvements -- **Reaction Time**: Extra moment to assess and plan piece placement -- **Strategic Depth**: More time for complex piece rotations and positioning -- **Difficulty Balance**: Slightly more forgiving spawn timing - -### 2. Visual Polish -- **Professional Grid**: Clean, precise cell boundaries -- **Consistent Alignment**: Grid maintains perfection across all window states -- **Enhanced Readability**: Clear visual reference for piece placement - -### 3. Technical Quality -- **Responsive Design**: Proper scaling and alignment in all scenarios -- **Code Quality**: Simplified and more maintainable coordinate system -- **Cross-Platform**: Consistent behavior regardless of display configuration - -## Status: โœ… COMPLETED -- Spawn position adjusted: Y coordinate moved from 0 to -1 -- Grid line alignment fixed: Removed duplicate content offset application -- Testing validated: Proper alignment in windowed, resized, and fullscreen modes -- User experience enhanced: Better gameplay timing and visual precision - -## Conclusion -Both issues have been successfully resolved. The game now provides an optimal spawn experience with pieces appearing in the 2nd visible line, while the grid lines maintain perfect alignment regardless of window state or size changes. These improvements enhance both the gameplay experience and visual quality of the Tetris game. diff --git a/assets/music/Every Block You Take.mp3 b/assets/music/Every Block You Take.mp3 new file mode 100644 index 0000000..d8534b5 Binary files /dev/null and b/assets/music/Every Block You Take.mp3 differ diff --git a/src/audio/Audio.cpp b/src/audio/Audio.cpp index 5e4ff42..19b494f 100644 --- a/src/audio/Audio.cpp +++ b/src/audio/Audio.cpp @@ -104,25 +104,52 @@ void Audio::feed(Uint32 bytesWanted, SDL_AudioStream* stream){ std::vector mix(outSamples, 0); // 1) Mix music into buffer (if not muted) - if(!muted && current >= 0){ + // 1) Mix music into buffer (if not muted) + if(!muted && playing){ size_t cursorBytes = 0; while(cursorBytes < bytesWanted){ - if(current < 0) break; - auto &trk = tracks[current]; - size_t samplesAvail = trk.pcm.size() - trk.cursor; // samples (int16) - if(samplesAvail == 0){ nextTrack(); if(current < 0) break; continue; } + AudioTrack* trk = nullptr; + + if (isMenuMusic) { + if (menuTrack.ok) trk = &menuTrack; + } else { + if (current >= 0 && current < (int)tracks.size()) trk = &tracks[current]; + } + + if (!trk) break; + + size_t samplesAvail = trk->pcm.size() - trk->cursor; // samples (int16) + if(samplesAvail == 0){ + if (isMenuMusic) { + trk->cursor = 0; // Loop menu music + continue; + } else { + nextTrack(); + if(current < 0) break; + continue; + } + } + size_t samplesNeeded = (bytesWanted - cursorBytes) / sizeof(int16_t); size_t toCopy = (samplesAvail < samplesNeeded) ? samplesAvail : samplesNeeded; if(toCopy == 0) break; + // Mix add with clamp size_t startSample = cursorBytes / sizeof(int16_t); for(size_t i=0;ipcm[trk->cursor+i]; if(v>32767) v=32767; if(v<-32768) v=-32768; mix[startSample+i] = (int16_t)v; } - trk.cursor += toCopy; + trk->cursor += toCopy; cursorBytes += (Uint32)(toCopy * sizeof(int16_t)); - if(trk.cursor >= trk.pcm.size()) nextTrack(); + + if(trk->cursor >= trk->pcm.size()) { + if (isMenuMusic) { + trk->cursor = 0; // Loop menu music + } else { + nextTrack(); + } + } } } @@ -266,6 +293,39 @@ int Audio::getLoadedTrackCount() const { return loadedCount; } +void Audio::setMenuTrack(const std::string& path) { + menuTrack.path = path; +#ifdef _WIN32 + // Ensure MF is started (might be redundant if init called, but safe) + if(!mfStarted){ if(FAILED(MFStartup(MF_VERSION))) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MFStartup failed"); } else mfStarted=true; } + + if (decodeMP3(path, menuTrack.pcm, menuTrack.rate, menuTrack.channels)) { + menuTrack.ok = true; + } else { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to decode menu track %s", path.c_str()); + } +#else + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MP3 unsupported (stub): %s", path.c_str()); +#endif +} + +void Audio::playMenuMusic() { + isMenuMusic = true; + if (menuTrack.ok) { + menuTrack.cursor = 0; + } + start(); +} + +void Audio::playGameMusic() { + isMenuMusic = false; + // If we were playing menu music, we might want to pick a random track or resume + if (current < 0 && !tracks.empty()) { + nextTrack(); + } + start(); +} + void Audio::shutdown(){ // Stop background loading thread first if (loadingThread.joinable()) { diff --git a/src/audio/Audio.h b/src/audio/Audio.h index 6707529..3506d29 100644 --- a/src/audio/Audio.h +++ b/src/audio/Audio.h @@ -43,6 +43,12 @@ public: void shuffle(); // randomize order void start(); // begin playback void toggleMute(); + + // Menu music support + void setMenuTrack(const std::string& path); + void playMenuMusic(); + void playGameMusic(); + // Queue a sound effect to mix over the music (pcm can be mono/stereo, any rate; will be converted) void playSfx(const std::vector& pcm, int channels, int rate, float volume); void shutdown(); @@ -54,7 +60,10 @@ private: bool ensureStream(); void backgroundLoadingThread(); // background thread function - std::vector tracks; int current=-1; bool playing=false; bool muted=false; std::mt19937 rng{std::random_device{}()}; + std::vector tracks; + AudioTrack menuTrack; + bool isMenuMusic = false; + int current=-1; bool playing=false; bool muted=false; std::mt19937 rng{std::random_device{}()}; SDL_AudioStream* audioStream=nullptr; SDL_AudioSpec outSpec{}; int outChannels=2; int outRate=44100; bool mfStarted=false; // Threading support diff --git a/src/core/GravityManager.cpp b/src/core/GravityManager.cpp index 7bfc238..e3b10b1 100644 --- a/src/core/GravityManager.cpp +++ b/src/core/GravityManager.cpp @@ -14,19 +14,19 @@ double GravityManager::getGlobalMultiplier() const { return globalMultiplier; } void GravityManager::setLevelMultiplier(int level, double m) { if (level < 0) return; - int idx = level >= 29 ? 29 : level; + int idx = level >= 19 ? 19 : level; levelMultipliers[idx] = std::clamp(m, 0.01, 100.0); } double GravityManager::getLevelMultiplier(int level) const { - int idx = level < 0 ? 0 : (level >= 29 ? 29 : level); + int idx = level < 0 ? 0 : (level >= 19 ? 19 : level); return levelMultipliers[idx]; } double GravityManager::getMsForLevel(int level) const { - int idx = level < 0 ? 0 : (level >= 29 ? 29 : level); - double frames = static_cast(FRAMES_TABLE[idx]) * levelMultipliers[idx]; - double result = frames * FRAME_MS * globalMultiplier; + int idx = level < 0 ? 0 : (level >= 19 ? 19 : level); + double baseMs = LEVEL_SPEEDS_MS[idx]; + double result = baseMs * levelMultipliers[idx] * globalMultiplier; return std::max(1.0, result); } diff --git a/src/core/GravityManager.h b/src/core/GravityManager.h index a2c05e8..e75d767 100644 --- a/src/core/GravityManager.h +++ b/src/core/GravityManager.h @@ -18,14 +18,11 @@ public: double getFpsForLevel(int level) const; private: - static constexpr double NES_FPS = 60.0988; - static constexpr double FRAME_MS = 1000.0 / NES_FPS; - static constexpr int FRAMES_TABLE[30] = { - 48,43,38,33,28,23,18,13,8,6, - 5,5,5,4,4,4,3,3,3,2, - 2,2,2,2,2,2,2,2,2,1 + static constexpr double LEVEL_SPEEDS_MS[20] = { + 1000.0, 920.0, 840.0, 760.0, 680.0, 600.0, 520.0, 440.0, 360.0, 280.0, + 200.0, 160.0, 160.0, 120.0, 120.0, 100.0, 100.0, 80.0, 80.0, 60.0 }; double globalMultiplier{1.0}; - std::array levelMultipliers{}; // default 1.0 + std::array levelMultipliers{}; // default 1.0 }; diff --git a/src/main.cpp b/src/main.cpp index 7d49fbb..8378d82 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -29,6 +29,7 @@ #include "states/LevelSelectorState.h" #include "states/PlayingState.h" #include "audio/MenuWrappers.h" +#include "graphics/renderers/GameRenderer.h" // Debug logging removed: no-op in this build (previously LOG_DEBUG) @@ -278,6 +279,8 @@ static bool showSettingsPopup = false; static bool showExitConfirmPopup = false; static bool musicEnabled = true; static int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings +static bool isNewHighScore = false; +static std::string playerName = ""; // ----------------------------------------------------------------------------- // Tetris Block Fireworks for intro animation (block particles) @@ -437,7 +440,10 @@ int main(int, char **) pixelFont.init("assets/fonts/PressStart2P-Regular.ttf", 16); ScoreManager scores; - scores.load(); + // Load scores asynchronously to prevent startup hang due to network request + std::thread([&scores]() { + scores.load(); + }).detach(); Starfield starfield; starfield.init(200, LOGICAL_W, LOGICAL_H); Starfield3D starfield3D; @@ -726,6 +732,37 @@ int main(int, char **) SDL_SetWindowFullscreen(window, isFullscreen ? SDL_WINDOW_FULLSCREEN : 0); } } + + // Text input for high score + if (state == AppState::GameOver && isNewHighScore && e.type == SDL_EVENT_TEXT_INPUT) { + if (playerName.length() < 12) { + playerName += e.text.text; + } + } + + if (state == AppState::GameOver && e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { + if (isNewHighScore) { + if (e.key.scancode == SDL_SCANCODE_BACKSPACE && !playerName.empty()) { + playerName.pop_back(); + } else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) { + if (playerName.empty()) playerName = "PLAYER"; + scores.submit(game.score(), game.lines(), game.level(), game.elapsed(), playerName); + isNewHighScore = false; + SDL_StopTextInput(window); + } + } else { + if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) { + // Restart + game.reset(startLevelSelection); + state = AppState::Playing; + stateMgr.setState(state); + } else if (e.key.scancode == SDL_SCANCODE_ESCAPE) { + // Menu + state = AppState::Menu; + stateMgr.setState(state); + } + } + } // Mouse handling remains in main loop for UI interactions if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN) @@ -923,7 +960,15 @@ int main(int, char **) } if (game.isGameOver()) { - scores.submit(game.score(), game.lines(), game.level(), game.elapsed()); + // Always allow name entry if score > 0 + if (game.score() > 0) { + isNewHighScore = true; // Reuse flag to trigger input mode + playerName = ""; + SDL_StartTextInput(window); + } else { + isNewHighScore = false; + scores.submit(game.score(), game.lines(), game.level(), game.elapsed()); + } state = AppState::GameOver; stateMgr.setState(state); } @@ -1006,12 +1051,38 @@ int main(int, char **) { if (!musicStarted && musicLoaded) { - // Music tracks are already loaded during loading screen, just start playback - Audio::instance().start(); + // Load menu track once on first menu entry (in background to avoid blocking) + static bool menuTrackLoaded = false; + if (!menuTrackLoaded) { + std::thread([]() { + Audio::instance().setMenuTrack("assets/music/Every Block You Take.mp3"); + }).detach(); + menuTrackLoaded = true; + } + + // Start appropriate music based on state + if (state == AppState::Menu) { + Audio::instance().playMenuMusic(); + } else { + Audio::instance().playGameMusic(); + } musicStarted = true; } } + // Handle music transitions between states + static AppState previousState = AppState::Loading; + if (state != previousState && musicStarted) { + if (state == AppState::Menu && previousState == AppState::Playing) { + // Switched from game to menu + Audio::instance().playMenuMusic(); + } else if (state == AppState::Playing && previousState == AppState::Menu) { + // Switched from menu to game + Audio::instance().playGameMusic(); + } + } + previousState = state; + // Update starfields based on current state if (state == AppState::Loading) { starfield3D.update(float(frameMs / 1000.0f)); @@ -1242,409 +1313,141 @@ int main(int, char **) } break; case AppState::Playing: - { - // Calculate actual content area (centered within the window) - float contentScale = logicalScale; - float contentW = LOGICAL_W * contentScale; - float contentH = LOGICAL_H * contentScale; - float contentOffsetX = (winW - contentW) * 0.5f / contentScale; - float contentOffsetY = (winH - contentH) * 0.5f / contentScale; - - // Draw the game with layout matching the JavaScript version - auto drawRect = [&](float x, float y, float w, float h, SDL_Color c) + GameRenderer::renderPlayingState( + renderer, + &game, + &pixelFont, + &lineEffect, + blocksTex, + (float)LOGICAL_W, + (float)LOGICAL_H, + logicalScale, + (float)winW, + (float)winH, + showExitConfirmPopup + ); + break; + case AppState::GameOver: + // Draw the game state in the background + GameRenderer::renderPlayingState( + renderer, + &game, + &pixelFont, + &lineEffect, + blocksTex, + (float)LOGICAL_W, + (float)LOGICAL_H, + logicalScale, + (float)winW, + (float)winH, + false // No exit popup in Game Over + ); + + // Draw Game Over Overlay { - SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a); - SDL_FRect fr{x + contentOffsetX, y + contentOffsetY, w, h}; - SDL_RenderFillRect(renderer, &fr); - }; - - // Responsive layout that scales with window size while maintaining margins - // Calculate available space considering UI panels and margins - const float MIN_MARGIN = 40.0f; // Minimum margin from edges - const float TOP_MARGIN = 60.0f; // Extra top margin for better spacing - const float PANEL_WIDTH = 180.0f; // Width of side panels - const float PANEL_SPACING = 30.0f; // Space between grid and panels - const float NEXT_PIECE_HEIGHT = 120.0f; // Space reserved for next piece preview (increased) - const float BOTTOM_MARGIN = 60.0f; // Space for controls text at bottom - - // Available width = Total width - margins - left panel - right panel - spacing - const float availableWidth = LOGICAL_W - (MIN_MARGIN * 2) - (PANEL_WIDTH * 2) - (PANEL_SPACING * 2); - const float availableHeight = LOGICAL_H - TOP_MARGIN - BOTTOM_MARGIN - NEXT_PIECE_HEIGHT; - - // Calculate block size based on available space (maintain 10:20 aspect ratio) - const float maxBlockSizeW = availableWidth / Game::COLS; - const float maxBlockSizeH = availableHeight / Game::ROWS; - const float BLOCK_SIZE = std::min(maxBlockSizeW, maxBlockSizeH); - - // Ensure minimum and maximum block sizes - const float finalBlockSize = std::max(20.0f, std::min(BLOCK_SIZE, 40.0f)); - - const float GRID_W = Game::COLS * finalBlockSize; - const float GRID_H = Game::ROWS * finalBlockSize; - - // Calculate vertical position with proper top margin - const float totalContentHeight = NEXT_PIECE_HEIGHT + GRID_H; - const float availableVerticalSpace = LOGICAL_H - TOP_MARGIN - BOTTOM_MARGIN; - const float verticalCenterOffset = (availableVerticalSpace - totalContentHeight) * 0.5f; - const float contentStartY = TOP_MARGIN + verticalCenterOffset; - - // Perfect horizontal centering - center the entire layout (grid + panels) in the window - const float totalLayoutWidth = PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + PANEL_WIDTH; - const float layoutStartX = (LOGICAL_W - totalLayoutWidth) * 0.5f; - - // Calculate panel and grid positions from the centered layout - const float statsX = layoutStartX + contentOffsetX; - const float gridX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + contentOffsetX; - const float scoreX = layoutStartX + PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + contentOffsetX; - - // Position grid with proper top spacing - const float gridY = contentStartY + NEXT_PIECE_HEIGHT + contentOffsetY; - - // Panel dimensions and positions - const float statsY = gridY; - const float statsW = PANEL_WIDTH; - const float statsH = GRID_H; - - const float scoreY = gridY; - const float scoreW = PANEL_WIDTH; - - // Next piece preview (above grid, centered) - const float nextW = finalBlockSize * 4 + 20; - const float nextH = finalBlockSize * 2 + 20; - const float nextX = gridX + (GRID_W - nextW) * 0.5f; - const float nextY = contentStartY + contentOffsetY; - - // Handle line clearing effects (now that we have grid coordinates) - if (game.hasCompletedLines() && !lineEffect.isActive()) { - auto completedLines = game.getCompletedLines(); - lineEffect.startLineClear(completedLines, static_cast(gridX), static_cast(gridY), static_cast(finalBlockSize)); - } - - // Draw panels with borders (like JS version) - - // Game grid border - drawRect(gridX - 3 - contentOffsetX, gridY - 3 - contentOffsetY, GRID_W + 6, GRID_H + 6, {100, 120, 200, 255}); // Outer border - drawRect(gridX - 1 - contentOffsetX, gridY - 1 - contentOffsetY, GRID_W + 2, GRID_H + 2, {60, 80, 160, 255}); // Inner border - drawRect(gridX - contentOffsetX, gridY - contentOffsetY, GRID_W, GRID_H, {20, 25, 35, 255}); // Background - - // Left panel background (BLOCKS panel) - translucent, slightly shorter height - { - SDL_SetRenderDrawColor(renderer, 10, 15, 25, 160); - SDL_FRect lbg{statsX - 16, gridY - 10, statsW + 32, GRID_H + 20}; - SDL_RenderFillRect(renderer, &lbg); - } - // Right panel background (SCORE/LINES/LEVEL etc) - translucent - { - SDL_SetRenderDrawColor(renderer, 10, 15, 25, 160); - SDL_FRect rbg{scoreX - 16, gridY - 16, scoreW + 32, GRID_H + 32}; - SDL_RenderFillRect(renderer, &rbg); - } - - // Draw grid lines (subtle lines to show cell boundaries) - SDL_SetRenderDrawColor(renderer, 40, 45, 60, 255); // Slightly lighter than background - - // Vertical grid lines - for (int x = 1; x < Game::COLS; ++x) { - float lineX = gridX + x * finalBlockSize; // Remove duplicate contentOffsetX - SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H); - } - - // Horizontal grid lines - for (int y = 1; y < Game::ROWS; ++y) { - float lineY = gridY + y * finalBlockSize; // Remove duplicate contentOffsetY - SDL_RenderLine(renderer, gridX, lineY, gridX + GRID_W, lineY); - } - - // Block statistics panel - drawRect(statsX - 3 - contentOffsetX, statsY - 3 - contentOffsetY, statsW + 6, statsH + 6, {100, 120, 200, 255}); - drawRect(statsX - contentOffsetX, statsY - contentOffsetY, statsW, statsH, {30, 35, 50, 255}); - - // Next piece preview panel - drawRect(nextX - 3 - contentOffsetX, nextY - 3 - contentOffsetY, nextW + 6, nextH + 6, {100, 120, 200, 255}); - drawRect(nextX - contentOffsetX, nextY - contentOffsetY, nextW, nextH, {30, 35, 50, 255}); - - // Draw the game board - const auto &board = game.boardRef(); - for (int y = 0; y < Game::ROWS; ++y) - { - for (int x = 0; x < Game::COLS; ++x) - { - int v = board[y * Game::COLS + x]; - if (v > 0) { - float bx = gridX + x * finalBlockSize; - float by = gridY + y * finalBlockSize; - drawBlockTexture(renderer, blocksTex, bx, by, finalBlockSize, v - 1); - } - } - } - - // Draw ghost piece (where current piece will land) - if (!game.isPaused()) { - Game::Piece ghostPiece = game.current(); - // Find landing position - while (true) { - Game::Piece testPiece = ghostPiece; - testPiece.y++; - bool collision = false; - - // Simple collision check - for (int cy = 0; cy < 4; ++cy) { - for (int cx = 0; cx < 4; ++cx) { - if (Game::cellFilled(testPiece, cx, cy)) { - int gx = testPiece.x + cx; - int gy = testPiece.y + cy; - if (gy >= Game::ROWS || gx < 0 || gx >= Game::COLS || - (gy >= 0 && board[gy * Game::COLS + gx] != 0)) { - collision = true; - break; - } - } - } - if (collision) break; - } - - if (collision) break; - ghostPiece = testPiece; - } - - // Draw ghost piece - drawPiece(renderer, blocksTex, ghostPiece, gridX, gridY, finalBlockSize, true); - } - - // Draw the falling piece - if (!game.isPaused()) { - drawPiece(renderer, blocksTex, game.current(), gridX, gridY, finalBlockSize, false); - } - - // Handle line clearing effects - if (game.hasCompletedLines() && !lineEffect.isActive()) { - lineEffect.startLineClear(game.getCompletedLines(), static_cast(gridX), static_cast(gridY), static_cast(finalBlockSize)); - } - - // Draw line clearing effects - if (lineEffect.isActive()) { - lineEffect.render(renderer, blocksTex, static_cast(gridX), static_cast(gridY), static_cast(finalBlockSize)); - } - - // Draw next piece preview - pixelFont.draw(renderer, nextX + 10, nextY - 20, "NEXT", 1.0f, {255, 220, 0, 255}); - if (game.next().type < PIECE_COUNT) { - drawSmallPiece(renderer, blocksTex, game.next().type, nextX + 10, nextY + 10, finalBlockSize * 0.6f); - } - - // Draw block statistics (left panel) - pixelFont.draw(renderer, statsX + 10, statsY + 10, "BLOCKS", 1.0f, {255, 220, 0, 255}); - - const auto& blockCounts = game.getBlockCounts(); - int totalBlocks = 0; for (int i = 0; i < PIECE_COUNT; ++i) totalBlocks += blockCounts[i]; - const char* pieceNames[] = {"I", "O", "T", "S", "Z", "J", "L"}; - // Dynamic vertical cursor so bars sit below blocks cleanly - float yCursor = statsY + 52; - for (int i = 0; i < PIECE_COUNT; ++i) { - // Baseline for this entry - float py = yCursor; - - // Draw small piece icon (top of entry) - float previewSize = finalBlockSize * 0.55f; - drawSmallPiece(renderer, blocksTex, static_cast(i), statsX + 18, py, previewSize); - - // Compute preview height in tiles (rotation 0) - int maxCy = -1; - { - Game::Piece prev; prev.type = static_cast(i); prev.rot = 0; prev.x = 0; prev.y = 0; - for (int cy = 0; cy < 4; ++cy) { - for (int cx = 0; cx < 4; ++cx) { - if (Game::cellFilled(prev, cx, cy)) maxCy = std::max(maxCy, cy); - } - } - } - int tilesHigh = (maxCy >= 0 ? maxCy + 1 : 1); - float previewHeight = tilesHigh * previewSize; - - // Count on the right, near the top (aligned with blocks) - int count = blockCounts[i]; - char countStr[16]; - snprintf(countStr, sizeof(countStr), "%d", count); - pixelFont.draw(renderer, statsX + statsW - 20, py + 6, countStr, 1.1f, {240, 240, 245, 255}); - - // Percentage and bar BELOW the blocks - int perc = (totalBlocks > 0) ? int(std::round(100.0 * double(count) / double(totalBlocks))) : 0; - char percStr[16]; - snprintf(percStr, sizeof(percStr), "%d%%", perc); - - float barX = statsX + 12; - float barY = py + previewHeight + 18.0f; - float barW = statsW - 24; - float barH = 6; - - // Percent text just above the bar (left) - pixelFont.draw(renderer, barX, barY - 16, percStr, 0.8f, {230, 230, 235, 255}); - - // Track - SDL_SetRenderDrawColor(renderer, 170, 170, 175, 200); - SDL_FRect track{barX, barY, barW, barH}; - SDL_RenderFillRect(renderer, &track); - // Fill (piece color) - SDL_Color pc = COLORS[i + 1]; - SDL_SetRenderDrawColor(renderer, pc.r, pc.g, pc.b, 230); - float fillW = barW * (perc / 100.0f); - if (fillW < 0) fillW = 0; if (fillW > barW) fillW = barW; - SDL_FRect fill{barX, barY, fillW, barH}; - SDL_RenderFillRect(renderer, &fill); - - // Advance cursor: bar bottom + spacing - yCursor = barY + barH + 18.0f; - } - - // Draw score panel (right side), centered vertically in grid - // Compute content vertical centering based on known offsets - const float contentTopOffset = 0.0f; - const float contentBottomOffset = 290.0f; // last line (time value) - const float contentPad = 36.0f; - float scoreContentH = (contentBottomOffset - contentTopOffset) + contentPad; - float baseY = gridY + (GRID_H - scoreContentH) * 0.5f; - - pixelFont.draw(renderer, scoreX, baseY + 0, "SCORE", 1.0f, {255, 220, 0, 255}); - char scoreStr[32]; - snprintf(scoreStr, sizeof(scoreStr), "%d", game.score()); - pixelFont.draw(renderer, scoreX, baseY + 25, scoreStr, 0.9f, {255, 255, 255, 255}); - - pixelFont.draw(renderer, scoreX, baseY + 70, "LINES", 1.0f, {255, 220, 0, 255}); - char linesStr[16]; - snprintf(linesStr, sizeof(linesStr), "%03d", game.lines()); - pixelFont.draw(renderer, scoreX, baseY + 95, linesStr, 0.9f, {255, 255, 255, 255}); - - pixelFont.draw(renderer, scoreX, baseY + 140, "LEVEL", 1.0f, {255, 220, 0, 255}); - char levelStr[16]; - snprintf(levelStr, sizeof(levelStr), "%02d", game.level()); - pixelFont.draw(renderer, scoreX, baseY + 165, levelStr, 0.9f, {255, 255, 255, 255}); - - // Next level progress - // JS rules: first threshold = (startLevel+1)*10; afterwards every +10 - int startLv = game.startLevelBase(); // 0-based - int firstThreshold = (startLv + 1) * 10; - int linesDone = game.lines(); - int nextThreshold = 0; - if (linesDone < firstThreshold) { - nextThreshold = firstThreshold; - } else { - int blocksPast = linesDone - firstThreshold; - nextThreshold = firstThreshold + ((blocksPast / 10) + 1) * 10; - } - int linesForNext = std::max(0, nextThreshold - linesDone); - pixelFont.draw(renderer, scoreX, baseY + 200, "NEXT LVL", 1.0f, {255, 220, 0, 255}); - char nextStr[32]; - snprintf(nextStr, sizeof(nextStr), "%d LINES", linesForNext); - pixelFont.draw(renderer, scoreX, baseY + 225, nextStr, 0.9f, {80, 255, 120, 255}); - - // Time - pixelFont.draw(renderer, scoreX, baseY + 265, "TIME", 1.0f, {255, 220, 0, 255}); - int totalSecs = static_cast(game.elapsed()); - int mins = totalSecs / 60; - int secs = totalSecs % 60; - char timeStr[16]; - snprintf(timeStr, sizeof(timeStr), "%02d:%02d", mins, secs); - pixelFont.draw(renderer, scoreX, baseY + 290, timeStr, 0.9f, {255, 255, 255, 255}); - - // --- Gravity HUD: show current gravity in ms and equivalent fps (top-right) --- - { - char gms[64]; - double gms_val = game.getGravityMs(); - double gfps = gms_val > 0.0 ? (1000.0 / gms_val) : 0.0; - snprintf(gms, sizeof(gms), "GRAV: %.0f ms (%.2f fps)", gms_val, gfps); - pixelFont.draw(renderer, LOGICAL_W - 260, 10, gms, 0.9f, {200, 200, 220, 255}); - } - - // Hold piece (if implemented) - if (game.held().type < PIECE_COUNT) { - pixelFont.draw(renderer, statsX + 10, statsY + statsH - 80, "HOLD", 1.0f, {255, 220, 0, 255}); - drawSmallPiece(renderer, blocksTex, game.held().type, statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f); - } - - // Pause overlay: don't draw pause UI when the exit-confirm popup is showing - if (game.isPaused() && !showExitConfirmPopup) { - // Semi-transparent overlay - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); - SDL_FRect pauseOverlay{0, 0, LOGICAL_W, LOGICAL_H}; - SDL_RenderFillRect(renderer, &pauseOverlay); - - // Pause text - pixelFont.draw(renderer, LOGICAL_W * 0.5f - 80, LOGICAL_H * 0.5f - 20, "PAUSED", 2.0f, {255, 255, 255, 255}); - pixelFont.draw(renderer, LOGICAL_W * 0.5f - 120, LOGICAL_H * 0.5f + 30, "Press P to resume", 0.8f, {200, 200, 220, 255}); - } - - // Exit confirmation popup (modal) - if (showExitConfirmPopup) { - // Compute content offsets for consistent placement across window sizes - float contentW = LOGICAL_W * logicalScale; - float contentH = LOGICAL_H * logicalScale; - float contentOffsetX = (winW - contentW) * 0.5f / logicalScale; - float contentOffsetY = (winH - contentH) * 0.5f / logicalScale; - - float popupW = 420, popupH = 180; - float popupX = (LOGICAL_W - popupW) / 2; - float popupY = (LOGICAL_H - popupH) / 2; - - // Dim entire window (use window coordinates so it always covers 100% of the target) - SDL_SetRenderViewport(renderer, nullptr); - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 200); + // 1. Dim the background + SDL_SetRenderViewport(renderer, nullptr); // Use window coordinates for full screen dim + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180); // Dark semi-transparent SDL_FRect fullWin{0.f, 0.f, (float)winW, (float)winH}; SDL_RenderFillRect(renderer, &fullWin); - // Restore logical viewport for drawing content-local popup + + // Restore logical viewport SDL_SetRenderViewport(renderer, &logicalVP); + SDL_SetRenderScale(renderer, logicalScale, logicalScale); - // Draw popup box (drawRect will apply contentOffset internally) - drawRect(popupX - 4, popupY - 4, popupW + 8, popupH + 8, {60, 70, 90, 255}); - drawRect(popupX, popupY, popupW, popupH, {20, 22, 28, 240}); + // 2. Calculate content offsets (same as in GameRenderer) + float contentScale = logicalScale; + float contentW = LOGICAL_W * contentScale; + float contentH = LOGICAL_H * contentScale; + float contentOffsetX = (winW - contentW) * 0.5f / contentScale; + float contentOffsetY = (winH - contentH) * 0.5f / contentScale; - // Center title and body text inside popup (use pixelFont for retro P2 font) - const std::string title = "Exit game?"; - const std::string line1 = "Are you sure you want to"; - const std::string line2 = "leave the current game?"; + // 3. Draw Game Over Box + float boxW = 500.0f; + float boxH = 350.0f; + float boxX = (LOGICAL_W - boxW) * 0.5f; + float boxY = (LOGICAL_H - boxH) * 0.5f; - int wTitle=0,hTitle=0; pixelFont.measure(title, 1.6f, wTitle, hTitle); - int wL1=0,hL1=0; pixelFont.measure(line1, 0.9f, wL1, hL1); - int wL2=0,hL2=0; pixelFont.measure(line2, 0.9f, wL2, hL2); + // Draw box background + SDL_SetRenderDrawColor(renderer, 20, 25, 35, 255); + SDL_FRect boxRect{boxX + contentOffsetX, boxY + contentOffsetY, boxW, boxH}; + SDL_RenderFillRect(renderer, &boxRect); + + // Draw box border + SDL_SetRenderDrawColor(renderer, 100, 120, 200, 255); + SDL_FRect borderRect{boxX + contentOffsetX - 3, boxY + contentOffsetY - 3, boxW + 6, boxH + 6}; + SDL_RenderFillRect(renderer, &borderRect); // Use FillRect for border background effect + SDL_SetRenderDrawColor(renderer, 20, 25, 35, 255); + SDL_RenderFillRect(renderer, &boxRect); // Redraw background on top of border rect - float titleX = popupX + (popupW - (float)wTitle) * 0.5f; - float l1X = popupX + (popupW - (float)wL1) * 0.5f; - float l2X = popupX + (popupW - (float)wL2) * 0.5f; + // 4. Draw Text + // 4. Draw Text + // Title + bool realHighScore = scores.isHighScore(game.score()); + const char* title = realHighScore ? "NEW HIGH SCORE!" : "GAME OVER"; + int tW=0, tH=0; pixelFont.measure(title, 2.0f, tW, tH); + pixelFont.draw(renderer, boxX + (boxW - tW) * 0.5f + contentOffsetX, boxY + 40 + contentOffsetY, title, 2.0f, realHighScore ? SDL_Color{255, 220, 0, 255} : SDL_Color{255, 60, 60, 255}); - pixelFont.draw(renderer, titleX + contentOffsetX, popupY + contentOffsetY + 20, title, 1.6f, {255, 220, 0, 255}); - pixelFont.draw(renderer, l1X + contentOffsetX, popupY + contentOffsetY + 60, line1, 0.9f, SDL_Color{220,220,230,255}); - pixelFont.draw(renderer, l2X + contentOffsetX, popupY + contentOffsetY + 84, line2, 0.9f, SDL_Color{220,220,230,255}); + // Score + char scoreStr[64]; + snprintf(scoreStr, sizeof(scoreStr), "SCORE: %d", game.score()); + int sW=0, sH=0; pixelFont.measure(scoreStr, 1.2f, sW, sH); + pixelFont.draw(renderer, boxX + (boxW - sW) * 0.5f + contentOffsetX, boxY + 100 + contentOffsetY, scoreStr, 1.2f, {255, 255, 255, 255}); - // Buttons (center labels inside buttons) - use pixelFont for labels - float btnW = 140, btnH = 46; - float yesX = popupX + popupW * 0.25f - btnW/2.0f; - float noX = popupX + popupW * 0.75f - btnW/2.0f; - float btnY = popupY + popupH - 60; + if (isNewHighScore) { + // Name Entry + const char* enterName = "ENTER NAME:"; + int enW=0, enH=0; pixelFont.measure(enterName, 1.0f, enW, enH); + pixelFont.draw(renderer, boxX + (boxW - enW) * 0.5f + contentOffsetX, boxY + 160 + contentOffsetY, enterName, 1.0f, {200, 200, 220, 255}); - drawRect(yesX - 2, btnY - 2, btnW + 4, btnH + 4, {100, 120, 140, 255}); - drawRect(yesX, btnY, btnW, btnH, {200, 60, 60, 255}); - const std::string yes = "YES"; - int wy=0,hy=0; pixelFont.measure(yes, 1.0f, wy, hy); - pixelFont.draw(renderer, yesX + (btnW - (float)wy) * 0.5f + contentOffsetX, btnY + (btnH - (float)hy) * 0.5f + contentOffsetY, yes, 1.0f, {255,255,255,255}); + // Input box + float inputW = 300.0f; + float inputH = 40.0f; + float inputX = boxX + (boxW - inputW) * 0.5f; + float inputY = boxY + 200.0f; + + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_FRect inputRect{inputX + contentOffsetX, inputY + contentOffsetY, inputW, inputH}; + SDL_RenderFillRect(renderer, &inputRect); + + SDL_SetRenderDrawColor(renderer, 255, 220, 0, 255); + SDL_RenderRect(renderer, &inputRect); - drawRect(noX - 2, btnY - 2, btnW + 4, btnH + 4, {100, 120, 140, 255}); - drawRect(noX, btnY, btnW, btnH, {80, 140, 80, 255}); - const std::string no = "NO"; - int wn=0,hn=0; pixelFont.measure(no, 1.0f, wn, hn); - pixelFont.draw(renderer, noX + (btnW - (float)wn) * 0.5f + contentOffsetX, btnY + (btnH - (float)hn) * 0.5f + contentOffsetY, no, 1.0f, {255,255,255,255}); + // Player Name (with cursor) + std::string display = playerName; + if ((SDL_GetTicks() / 500) % 2 == 0) display += "_"; // Blink cursor + + int nW=0, nH=0; pixelFont.measure(display, 1.2f, nW, nH); + pixelFont.draw(renderer, inputX + (inputW - nW) * 0.5f + contentOffsetX, inputY + (inputH - nH) * 0.5f + contentOffsetY, display, 1.2f, {255, 255, 255, 255}); + + // Hint + const char* hint = "PRESS ENTER TO SUBMIT"; + int hW=0, hH=0; pixelFont.measure(hint, 0.8f, hW, hH); + pixelFont.draw(renderer, boxX + (boxW - hW) * 0.5f + contentOffsetX, boxY + 280 + contentOffsetY, hint, 0.8f, {150, 150, 150, 255}); + + } else { + // Lines + char linesStr[64]; + snprintf(linesStr, sizeof(linesStr), "LINES: %d", game.lines()); + int lW=0, lH=0; pixelFont.measure(linesStr, 1.2f, lW, lH); + pixelFont.draw(renderer, boxX + (boxW - lW) * 0.5f + contentOffsetX, boxY + 140 + contentOffsetY, linesStr, 1.2f, {255, 255, 255, 255}); + + // Level + char levelStr[64]; + snprintf(levelStr, sizeof(levelStr), "LEVEL: %d", game.level()); + int lvW=0, lvH=0; pixelFont.measure(levelStr, 1.2f, lvW, lvH); + pixelFont.draw(renderer, boxX + (boxW - lvW) * 0.5f + contentOffsetX, boxY + 180 + contentOffsetY, levelStr, 1.2f, {255, 255, 255, 255}); + + // Instructions + const char* instr = "PRESS ENTER TO RESTART"; + int iW=0, iH=0; pixelFont.measure(instr, 0.9f, iW, iH); + pixelFont.draw(renderer, boxX + (boxW - iW) * 0.5f + contentOffsetX, boxY + 260 + contentOffsetY, instr, 0.9f, {255, 220, 0, 255}); + + const char* instr2 = "PRESS ESC FOR MENU"; + int iW2=0, iH2=0; pixelFont.measure(instr2, 0.9f, iW2, iH2); + pixelFont.draw(renderer, boxX + (boxW - iW2) * 0.5f + contentOffsetX, boxY + 290 + contentOffsetY, instr2, 0.9f, {255, 220, 0, 255}); + } } - - // Controls hint at bottom - font.draw(renderer, 20, LOGICAL_H - 30, "ARROWS=Move Z/X=Rotate C=Hold SPACE=Drop P=Pause ESC=Menu", 1.0f, {150, 150, 170, 255}); - } - break; - case AppState::GameOver: - font.draw(renderer, LOGICAL_W * 0.5f - 120, 140, "GAME OVER", 3.0f, SDL_Color{255, 80, 60, 255}); - { - char buf[128]; - std::snprintf(buf, sizeof(buf), "SCORE %d LINES %d LEVEL %d", game.score(), game.lines(), game.level()); - font.draw(renderer, LOGICAL_W * 0.5f - 120, 220, buf, 1.2f, SDL_Color{220, 220, 230, 255}); - } - font.draw(renderer, LOGICAL_W * 0.5f - 120, 270, "PRESS ENTER / SPACE", 1.2f, SDL_Color{200, 200, 220, 255}); break; } diff --git a/src/persistence/Scores.cpp b/src/persistence/Scores.cpp index fafd4ca..7ffe2fe 100644 --- a/src/persistence/Scores.cpp +++ b/src/persistence/Scores.cpp @@ -1,9 +1,19 @@ -// Scores.cpp - Implementation of ScoreManager (copied into src/persistence) +// Scores.cpp - Implementation of ScoreManager with Firebase Sync #include "Scores.h" #include #include #include #include +#include +#include +#include +#include +#include + +using json = nlohmann::json; + +// Firebase Realtime Database URL +const std::string FIREBASE_URL = "https://tetris-90139.firebaseio.com/scores.json"; ScoreManager::ScoreManager(size_t maxScores) : maxEntries(maxScores) {} @@ -16,6 +26,52 @@ std::string ScoreManager::filePath() const { void ScoreManager::load() { scores.clear(); + + // Try to load from Firebase first + try { + cpr::Response r = cpr::Get(cpr::Url{FIREBASE_URL}, cpr::Timeout{2000}); // 2s timeout + if (r.status_code == 200 && !r.text.empty() && r.text != "null") { + auto j = json::parse(r.text); + + // Firebase returns a map of auto-generated IDs to objects + if (j.is_object()) { + for (auto& [key, value] : j.items()) { + ScoreEntry e; + if (value.contains("score")) e.score = value["score"]; + if (value.contains("lines")) e.lines = value["lines"]; + if (value.contains("level")) e.level = value["level"]; + if (value.contains("timeSec")) e.timeSec = value["timeSec"]; + if (value.contains("name")) e.name = value["name"]; + scores.push_back(e); + } + } + // Or it might be an array if keys are integers (unlikely for Firebase push) + else if (j.is_array()) { + for (auto& value : j) { + ScoreEntry e; + if (value.contains("score")) e.score = value["score"]; + if (value.contains("lines")) e.lines = value["lines"]; + if (value.contains("level")) e.level = value["level"]; + if (value.contains("timeSec")) e.timeSec = value["timeSec"]; + if (value.contains("name")) e.name = value["name"]; + scores.push_back(e); + } + } + + // Sort and keep top scores + std::sort(scores.begin(), scores.end(), [](auto&a,auto&b){return a.score>b.score;}); + if (scores.size() > maxEntries) scores.resize(maxEntries); + + // Save to local cache + save(); + return; + } + } catch (...) { + // Ignore network errors and fall back to local file + std::cerr << "Failed to load from Firebase, falling back to local file." << std::endl; + } + + // Fallback to local file std::ifstream f(filePath()); if (!f) { // Create sample high scores if file doesn't exist @@ -56,11 +112,43 @@ void ScoreManager::save() const { } } -void ScoreManager::submit(int score, int lines, int level, double timeSec) { - scores.push_back(ScoreEntry{score,lines,level,timeSec}); +void ScoreManager::submit(int score, int lines, int level, double timeSec, const std::string& name) { + // Add to local list + scores.push_back(ScoreEntry{score,lines,level,timeSec, name}); std::sort(scores.begin(), scores.end(), [](auto&a,auto&b){return a.score>b.score;}); if (scores.size()>maxEntries) scores.resize(maxEntries); save(); + + // Submit to Firebase + // Run in a detached thread to avoid blocking the UI? + // For simplicity, we'll do it blocking for now, or rely on short timeout. + // Ideally this should be async. + + json j; + j["score"] = score; + j["lines"] = lines; + j["level"] = level; + j["timeSec"] = timeSec; + j["name"] = name; + j["timestamp"] = std::time(nullptr); // Add timestamp + + // Fire and forget (async) would be better, but for now let's just try to send + // We can use std::thread to make it async + std::thread([j]() { + try { + cpr::Post(cpr::Url{FIREBASE_URL}, + cpr::Body{j.dump()}, + cpr::Header{{"Content-Type", "application/json"}}, + cpr::Timeout{5000}); + } catch (...) { + // Ignore errors + } + }).detach(); +} + +bool ScoreManager::isHighScore(int score) const { + if (scores.size() < maxEntries) return true; + return score > scores.back().score; } void ScoreManager::createSampleScores() { diff --git a/src/persistence/Scores.h b/src/persistence/Scores.h index 2f69da9..1fede86 100644 --- a/src/persistence/Scores.h +++ b/src/persistence/Scores.h @@ -10,7 +10,8 @@ public: explicit ScoreManager(size_t maxScores = 12); void load(); void save() const; - void submit(int score, int lines, int level, double timeSec); + void submit(int score, int lines, int level, double timeSec, const std::string& name = "PLAYER"); + bool isHighScore(int score) const; const std::vector& all() const { return scores; } private: std::vector scores; diff --git a/vcpkg.json b/vcpkg.json index 0de6b55..255341f 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,7 +1,9 @@ { "dependencies": [ - "sdl3", - "sdl3-ttf", - "catch2" + "sdl3", + "sdl3-ttf", + "catch2", + "cpr", + "nlohmann-json" ] -} +} \ No newline at end of file