Merge branch 'feature/RefactoringMain' into develop

This commit is contained in:
2025-11-25 07:25:23 +01:00
124 changed files with 10259 additions and 1827 deletions

View File

@ -25,22 +25,34 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Homebrew: brew install sdl3
find_package(SDL3 CONFIG REQUIRED)
find_package(SDL3_ttf CONFIG REQUIRED)
find_package(SDL3_image CONFIG REQUIRED)
find_package(cpr CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
add_executable(tetris
src/main.cpp
src/gameplay/Game.cpp
src/gameplay/core/Game.cpp
src/core/GravityManager.cpp
src/core/StateManager.cpp
src/core/state/StateManager.cpp
# New core architecture classes
src/core/application/ApplicationManager.cpp
src/core/input/InputManager.cpp
src/core/assets/AssetManager.cpp
src/core/GlobalState.cpp
src/core/Settings.cpp
src/graphics/renderers/RenderManager.cpp
src/persistence/Scores.cpp
src/graphics/Starfield.cpp
src/graphics/Starfield3D.cpp
src/graphics/Font.cpp
src/graphics/effects/Starfield.cpp
src/graphics/effects/Starfield3D.cpp
src/graphics/ui/Font.cpp
src/graphics/renderers/GameRenderer.cpp
src/audio/Audio.cpp
src/gameplay/LineEffect.cpp
src/gameplay/effects/LineEffect.cpp
src/audio/SoundEffect.cpp
# State implementations (new)
src/states/LoadingState.cpp
src/states/MenuState.cpp
src/states/OptionsState.cpp
src/states/LevelSelectorState.cpp
src/states/PlayingState.cpp
)
@ -75,7 +87,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 SDL3_image::SDL3_image cpr::cpr nlohmann_json::nlohmann_json)
if (WIN32)
target_link_libraries(tetris PRIVATE mfplat mfreadwrite mfuuid)
@ -108,4 +120,6 @@ target_include_directories(tetris PRIVATE
${CMAKE_SOURCE_DIR}/src/gameplay
${CMAKE_SOURCE_DIR}/src/graphics
${CMAKE_SOURCE_DIR}/src/persistence
${CMAKE_SOURCE_DIR}/src/core
${CMAKE_SOURCE_DIR}/src/states
)

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 617 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Binary file not shown.

View File

@ -65,8 +65,15 @@ if exist "FreeSans.ttf" copy "FreeSans.ttf" "dist\TetrisGame\"
REM Copy SDL DLLs (if available) - SDL_image no longer needed
echo Copying dependencies...
if exist "vcpkg_installed\x64-windows\bin\SDL3.dll" copy "vcpkg_installed\x64-windows\bin\SDL3.dll" "dist\TetrisGame\"
if exist "vcpkg_installed\x64-windows\bin\SDL3_ttf.dll" copy "vcpkg_installed\x64-windows\bin\SDL3_ttf.dll" "dist\TetrisGame\"
set "PackageDir=dist\TetrisGame"
set "copiedDependencies=0"
call :CopyDependencyDir "build-release\vcpkg_installed\x64-windows\bin"
call :CopyDependencyDir "vcpkg_installed\x64-windows\bin"
if "%copiedDependencies%"=="0" (
echo Warning: No dependency DLLs were copied. Ensure vcpkg release binaries exist.
)
REM Create launcher batch file
echo @echo off > "dist\TetrisGame\Launch-Tetris.bat"
@ -84,3 +91,21 @@ echo The game is ready for distribution!
echo Users can run tetris.exe or Launch-Tetris.bat
echo.
pause
goto :eof
:CopyDependencyDir
set "dllDir=%~1"
if not exist "%dllDir%" goto :eof
echo Scanning %dllDir% for DLLs...
for %%F in ("%dllDir%\*.dll") do (
if exist "%%~fF" (
if exist "%PackageDir%\%%~nxF" (
copy /Y "%%~fF" "%PackageDir%\%%~nxF" >nul
) else (
copy "%%~fF" "%PackageDir%\" >nul
)
echo Copied %%~nxF
set "copiedDependencies=1"
)
)
goto :eof

View File

@ -155,32 +155,28 @@ foreach ($font in $FontFiles) {
}
# Step 7: Copy dependencies (DLLs)
Write-Info "Copying SDL3 dependencies..."
$VcpkgInstalled = Join-Path "vcpkg_installed" "x64-windows"
if (Test-Path $VcpkgInstalled) {
$DllPaths = @(
Join-Path $VcpkgInstalled "bin\SDL3.dll",
Join-Path $VcpkgInstalled "bin\SDL3_ttf.dll"
)
$CopiedDlls = 0
foreach ($dll in $DllPaths) {
if (Test-Path $dll) {
Copy-Item $dll $PackageDir
$dllName = Split-Path $dll -Leaf
Write-Success "Copied $dllName"
$CopiedDlls++
} else {
Write-Warning "Warning: $dll not found"
Write-Info "Copying runtime dependencies..."
$buildVcpkgBin = Join-Path (Join-Path $BuildDir "vcpkg_installed") "x64-windows/bin"
$repoVcpkgBin = "vcpkg_installed/x64-windows/bin"
$DependencyDirs = @($buildVcpkgBin, $repoVcpkgBin) | Where-Object { $_ } | Select-Object -Unique
$copiedNames = @{}
foreach ($dir in $DependencyDirs) {
if (!(Test-Path $dir)) { continue }
Write-Info "Scanning $dir for DLLs..."
$dlls = Get-ChildItem -Path $dir -Filter "*.dll" -ErrorAction SilentlyContinue
foreach ($dll in $dlls) {
$dest = Join-Path $PackageDir $dll.Name
Copy-Item $dll.FullName $dest -Force
if (-not $copiedNames.ContainsKey($dll.Name)) {
Write-Success "Copied $($dll.Name)"
$copiedNames[$dll.Name] = $true
}
}
if ($CopiedDlls -eq 0) {
Write-Warning "No SDL DLLs found in vcpkg installation"
Write-Warning "You may need to manually copy SDL3 DLLs to the package"
}
} else {
Write-Warning "vcpkg installation not found. SDL DLLs must be manually copied."
}
if ($copiedNames.Count -eq 0) {
Write-Warning "No dependency DLLs were copied. Please ensure vcpkg has been built for Release."
}
# Step 8: Create README and batch file for easy launching
@ -216,7 +212,7 @@ Tetris SDL3 Game - Release $Version
## Files Included
- tetris.exe - Main game executable
- SDL3.dll, SDL3_ttf.dll - Required libraries
- SDL3.dll, SDL3_ttf.dll, SDL3_image.dll - Required libraries
- assets/ - Game assets (images, music, fonts)
- FreeSans.ttf - Main font file

5
check_events.cpp Normal file
View File

@ -0,0 +1,5 @@
#include <SDL3/SDL.h>
#include <iostream>
int main() { std::cout << \
SDL_EVENT_QUIT:
\ << SDL_EVENT_QUIT << std::endl; return 0; }

425
docs/CODE_ORGANIZATION.md Normal file
View File

@ -0,0 +1,425 @@
# Code Organization & Structure Improvements
## ✅ Progress Tracker
### Phase 1: Core Reorganization - IN PROGRESS ⚠️
**✅ Completed:**
- ✅ Created new directory structure (interfaces/, application/, assets/, input/, state/, memory/)
- ✅ Created core interfaces (IRenderer.h, IAudioSystem.h, IAssetLoader.h, IInputHandler.h, IGameRules.h)
- ✅ Created ServiceContainer for dependency injection
- ✅ Moved ApplicationManager to core/application/
- ✅ Moved AssetManager to core/assets/
- ✅ Moved InputManager to core/input/
- ✅ Moved StateManager to core/state/
- ✅ Moved Game files to gameplay/core/
- ✅ Moved Font files to graphics/ui/
- ✅ Moved Starfield files to graphics/effects/
- ✅ Moved RenderManager and GameRenderer to graphics/renderers/
- ✅ Moved LineEffect to gameplay/effects/
- ✅ Cleaned up duplicate files
- ✅ Audio and Scores files already properly located
**⚠️ Currently In Progress:**
- ✅ Updated critical include paths in main.cpp, state files, graphics renderers
- ✅ Fixed RenderManager duplicate method declarations
- ✅ Resolved GameRenderer.h and LoadingState.cpp include paths
- ⚠️ Still fixing remaining include path issues (ongoing)
- ⚠️ Still debugging Game.h redefinition errors (ongoing)
**❌ Next Steps:**
- ❌ Complete all remaining #include statement updates
- ❌ Resolve Game.h redefinition compilation errors
- ❌ Test successful compilation of both tetris and tetris_refactored targets
- ❌ Update documentation
- ❌ Begin Phase 2 - Interface implementation
### Phase 2: Interface Extraction - NOT STARTED ❌
### Phase 3: Module Separation - NOT STARTED ❌
### Phase 4: Documentation & Standards - NOT STARTED ❌
## Current Structure Analysis
### Strengths
- ✅ Clear domain separation (core/, gameplay/, graphics/, audio/, etc.)
- ✅ Consistent naming conventions
- ✅ Modern C++ header organization
- ✅ Proper forward declarations
### Areas for Improvement
- ⚠️ Some files in root src/ should be moved to appropriate subdirectories
- ⚠️ Missing interfaces/contracts
- ⚠️ Some circular dependencies
- ⚠️ CMakeLists.txt has duplicate entries
## Proposed Directory Restructure
```text
src/
├── core/ # Core engine systems
│ ├── interfaces/ # Abstract interfaces (NEW)
│ │ ├── IRenderer.h
│ │ ├── IAudioSystem.h
│ │ ├── IAssetLoader.h
│ │ ├── IInputHandler.h
│ │ └── IGameRules.h
│ ├── application/ # Application lifecycle (NEW)
│ │ ├── ApplicationManager.cpp/h
│ │ ├── ServiceContainer.cpp/h
│ │ └── SystemCoordinator.cpp/h
│ ├── assets/ # Asset management
│ │ ├── AssetManager.cpp/h
│ │ └── AssetLoader.cpp/h
│ ├── input/ # Input handling
│ │ └── InputManager.cpp/h
│ ├── state/ # State management
│ │ └── StateManager.cpp/h
│ ├── memory/ # Memory management (NEW)
│ │ ├── ObjectPool.h
│ │ └── MemoryTracker.h
│ └── Config.h
│ └── GlobalState.cpp/h
│ └── GravityManager.cpp/h
├── gameplay/ # Game logic
│ ├── core/ # Core game mechanics
│ │ ├── Game.cpp/h
│ │ ├── Board.cpp/h # Extract from Game.cpp
│ │ ├── Piece.cpp/h # Extract from Game.cpp
│ │ └── PieceFactory.cpp/h # Extract from Game.cpp
│ ├── rules/ # Game rules (NEW)
│ │ ├── ClassicTetrisRules.cpp/h
│ │ ├── ModernTetrisRules.cpp/h
│ │ └── ScoringSystem.cpp/h
│ ├── effects/ # Visual effects
│ │ └── LineEffect.cpp/h
│ └── mechanics/ # Game mechanics (NEW)
│ ├── RotationSystem.cpp/h
│ ├── KickTable.cpp/h
│ └── BagRandomizer.cpp/h
├── graphics/ # Rendering and visual
│ ├── renderers/ # Different renderers
│ │ ├── RenderManager.cpp/h
│ │ ├── GameRenderer.cpp/h
│ │ ├── UIRenderer.cpp/h # Extract from various places
│ │ └── EffectRenderer.cpp/h # New
│ ├── effects/ # Visual effects
│ │ ├── Starfield.cpp/h
│ │ ├── Starfield3D.cpp/h
│ │ └── ParticleSystem.cpp/h # New
│ ├── ui/ # UI components
│ │ ├── Font.cpp/h
│ │ ├── Button.cpp/h # New
│ │ ├── Panel.cpp/h # New
│ │ └── ScoreDisplay.cpp/h # New
│ └── resources/ # Graphics resources
│ ├── TextureAtlas.cpp/h # New
│ └── SpriteManager.cpp/h # New
├── audio/ # Audio system
│ ├── Audio.cpp/h
│ ├── SoundEffect.cpp/h
│ ├── MusicManager.cpp/h # New
│ └── AudioMixer.cpp/h # New
├── persistence/ # Data persistence
│ ├── Scores.cpp/h
│ ├── Settings.cpp/h # New
│ ├── SaveGame.cpp/h # New
│ └── Serialization.cpp/h # New
├── states/ # Game states
│ ├── State.h # Base interface
│ ├── LoadingState.cpp/h
│ ├── MenuState.cpp/h
│ ├── LevelSelectorState.cpp/h
│ ├── PlayingState.cpp/h
│ ├── PausedState.cpp/h # New
│ ├── GameOverState.cpp/h # New
│ └── SettingsState.cpp/h # New
├── network/ # Future: Multiplayer (NEW)
│ ├── NetworkManager.h
│ ├── Protocol.h
│ └── MultiplayerGame.h
├── utils/ # Utilities (NEW)
│ ├── Logger.cpp/h
│ ├── Timer.cpp/h
│ ├── MathUtils.h
│ └── StringUtils.h
├── platform/ # Platform-specific (NEW)
│ ├── Platform.h
│ ├── Windows/
│ ├── Linux/
│ └── macOS/
└── main.cpp # Keep original main
└── main_new.cpp # Refactored main
```
## Module Dependencies
### Clean Dependency Graph
```text
Application Layer: main.cpp → ApplicationManager
Core Layer: ServiceContainer → All Managers
Gameplay Layer: Game → Rules → Mechanics
Graphics Layer: RenderManager → Renderers → Resources
Audio Layer: AudioSystem → Concrete Implementations
Persistence Layer: SaveSystem → Serialization
Platform Layer: Platform Abstraction (lowest level)
```
### Dependency Rules
1. **No circular dependencies**
2. **Higher layers can depend on lower layers only**
3. **Use interfaces for cross-layer communication**
4. **Platform layer has no dependencies on other layers**
## Header Organization
### 1. Consistent Header Structure
```cpp
// Standard template for all headers
#pragma once
// System includes
#include <vector>
#include <memory>
// External library includes
#include <SDL3/SDL.h>
// Internal includes (from most general to most specific)
#include "core/interfaces/IRenderer.h"
#include "graphics/resources/Texture.h"
#include "MyClass.h"
// Forward declarations
class GameRenderer;
class TextureAtlas;
// Class definition
class MyClass {
// Public interface first
public:
// Constructors/Destructors
MyClass();
~MyClass();
// Core functionality
void update(double deltaTime);
void render();
// Getters/Setters
int getValue() const { return value; }
void setValue(int v) { value = v; }
// Private implementation
private:
// Member variables
int value{0};
std::unique_ptr<GameRenderer> renderer;
// Private methods
void initializeRenderer();
};
```
### 2. Include Guards and PCH
```cpp
// PrecompiledHeaders.h (NEW)
#pragma once
// Standard library
#include <vector>
#include <memory>
#include <string>
#include <unordered_map>
#include <chrono>
#include <functional>
#include <algorithm>
// External libraries (stable)
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
// Common project headers
#include "core/Config.h"
#include "core/interfaces/IRenderer.h"
```
## Code Style Improvements
### 1. Consistent Naming Conventions
```cpp
// Classes: PascalCase
class GameRenderer;
class TextureAtlas;
// Functions/Methods: camelCase
void updateGameLogic();
bool isValidPosition();
// Variables: camelCase
int currentScore;
double deltaTime;
// Constants: UPPER_SNAKE_CASE
const int MAX_LEVEL = 30;
const double GRAVITY_MULTIPLIER = 1.0;
// Private members: camelCase with suffix
class MyClass {
private:
int memberVariable_; // or m_memberVariable
static int staticCounter_;
};
```
### 2. Documentation Standards
```cpp
/**
* @brief Manages the core game state and logic for Tetris
*
* The Game class handles piece movement, rotation, line clearing,
* and scoring according to classic Tetris rules.
*
* @example
* ```cpp
* Game game(startLevel);
* game.reset(0);
* game.move(-1); // Move left
* game.rotate(1); // Rotate clockwise
* ```
*/
class Game {
public:
/**
* @brief Moves the current piece horizontally
* @param dx Direction to move (-1 for left, +1 for right)
* @return true if the move was successful, false if blocked
*/
bool move(int dx);
/**
* @brief Gets the current score
* @return Current score value
* @note Score never decreases during gameplay
*/
int score() const noexcept { return score_; }
};
```
## CMake Improvements
### 1. Modular CMakeLists.txt
```cmake
# CMakeLists.txt (main)
cmake_minimum_required(VERSION 3.20)
project(tetris_sdl3 LANGUAGES CXX)
# Global settings
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find packages
find_package(SDL3 CONFIG REQUIRED)
find_package(SDL3_ttf CONFIG REQUIRED)
# Add subdirectories
add_subdirectory(src/core)
add_subdirectory(src/gameplay)
add_subdirectory(src/graphics)
add_subdirectory(src/audio)
add_subdirectory(src/persistence)
add_subdirectory(src/states)
# Main executable
add_executable(tetris src/main.cpp)
target_link_libraries(tetris PRIVATE
tetris::core
tetris::gameplay
tetris::graphics
tetris::audio
tetris::persistence
tetris::states
)
# Tests
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
```
### 2. Module CMakeLists.txt
```cmake
# src/core/CMakeLists.txt
add_library(tetris_core
ApplicationManager.cpp
StateManager.cpp
InputManager.cpp
AssetManager.cpp
GlobalState.cpp
GravityManager.cpp
)
add_library(tetris::core ALIAS tetris_core)
target_include_directories(tetris_core
PUBLIC ${CMAKE_SOURCE_DIR}/src
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(tetris_core
PUBLIC SDL3::SDL3 SDL3_ttf::SDL3_ttf
)
# Export for use by other modules
target_compile_features(tetris_core PUBLIC cxx_std_20)
```
## Implementation Timeline
### Phase 1: Core Reorganization (Week 1-2)
1. Create new directory structure
2. Move files to appropriate locations
3. Update CMakeLists.txt files
4. Fix include paths
### Phase 2: Interface Extraction (Week 3-4)
1. Create interface headers
2. Update implementations to use interfaces
3. Add dependency injection container
### Phase 3: Module Separation (Week 5-6)
1. Split large classes (Game, ApplicationManager)
2. Create separate CMake modules
3. Establish clean dependency graph
### Phase 4: Documentation & Standards (Week 7-8)
1. Add comprehensive documentation
2. Implement coding standards
3. Add static analysis tools
4. Update build scripts
## Benefits
1. **Maintainability**: Clear module boundaries and responsibilities
2. **Testability**: Easy to mock and test individual components
3. **Scalability**: Easy to add new features without affecting existing code
4. **Team Development**: Multiple developers can work on different modules
5. **Code Reuse**: Modular design enables component reuse

View File

@ -0,0 +1,163 @@
# Performance Optimization Recommendations
## Current Performance Analysis
### Memory Management
- **Good**: Proper RAII patterns, smart pointers
- **Improvement**: Object pooling for frequently created/destroyed objects
### Rendering Performance
- **Current**: SDL3 with immediate mode rendering
- **Optimization Opportunities**: Batch rendering, texture atlasing
### Game Logic Performance
- **Current**: Simple collision detection, adequate for Tetris
- **Good**: Efficient board representation using flat array
## Specific Optimizations
### 1. Object Pooling for Game Pieces
```cpp
// src/gameplay/PiecePool.h
class PiecePool {
private:
std::vector<std::unique_ptr<Piece>> available;
std::vector<std::unique_ptr<Piece>> inUse;
public:
std::unique_ptr<Piece> acquire(PieceType type);
void release(std::unique_ptr<Piece> piece);
void preAllocate(size_t count);
};
```
### 2. Texture Atlas for UI Elements
```cpp
// src/graphics/TextureAtlas.h
class TextureAtlas {
private:
SDL_Texture* atlasTexture;
std::unordered_map<std::string, SDL_Rect> regions;
public:
void loadAtlas(const std::string& atlasPath, const std::string& configPath);
SDL_Rect getRegion(const std::string& name) const;
SDL_Texture* getTexture() const { return atlasTexture; }
};
```
### 3. Batch Rendering System
```cpp
// src/graphics/BatchRenderer.h
class BatchRenderer {
private:
struct RenderCommand {
SDL_Texture* texture;
SDL_Rect srcRect;
SDL_Rect dstRect;
};
std::vector<RenderCommand> commands;
public:
void addSprite(SDL_Texture* texture, const SDL_Rect& src, const SDL_Rect& dst);
void flush();
void clear();
};
```
### 4. Memory-Efficient Board Representation
```cpp
// Current: std::array<int, COLS*ROWS> board (40 integers = 160 bytes)
// Optimized: Bitset representation for filled/empty + color array for occupied cells
class OptimizedBoard {
private:
std::bitset<COLS * ROWS> occupied; // 25 bytes (200 bits)
std::array<uint8_t, COLS * ROWS> colors; // 200 bytes, but only for occupied cells
public:
bool isOccupied(int x, int y) const;
uint8_t getColor(int x, int y) const;
void setCell(int x, int y, uint8_t color);
void clearCell(int x, int y);
};
```
### 5. Cache-Friendly Data Structures
```cpp
// Group related data together for better cache locality
struct GameState {
// Hot data (frequently accessed)
std::array<uint8_t, COLS * ROWS> board;
Piece currentPiece;
int score;
int level;
int lines;
// Cold data (less frequently accessed)
std::vector<PieceType> bag;
Piece holdPiece;
bool gameOver;
bool paused;
};
```
## Performance Measurement
### 1. Add Profiling Infrastructure
```cpp
// src/core/Profiler.h
class Profiler {
private:
std::unordered_map<std::string, std::chrono::high_resolution_clock::time_point> startTimes;
std::unordered_map<std::string, double> averageTimes;
public:
void beginTimer(const std::string& name);
void endTimer(const std::string& name);
void printStats();
};
// Usage:
// profiler.beginTimer("GameLogic");
// game.update(deltaTime);
// profiler.endTimer("GameLogic");
```
### 2. Frame Rate Optimization
```cpp
// Target 60 FPS with consistent frame timing
class FrameRateManager {
private:
std::chrono::high_resolution_clock::time_point lastFrame;
double targetFrameTime = 1000.0 / 60.0; // 16.67ms
public:
void beginFrame();
void endFrame();
double getDeltaTime() const;
bool shouldSkipFrame() const;
};
```
## Expected Performance Gains
1. **Object Pooling**: 30-50% reduction in allocation overhead
2. **Texture Atlas**: 20-30% improvement in rendering performance
3. **Batch Rendering**: 40-60% reduction in draw calls
4. **Optimized Board**: 60% reduction in memory usage
5. **Cache Optimization**: 10-20% improvement in game logic performance
## Implementation Priority
1. **High Impact, Low Effort**: Profiling infrastructure, frame rate management
2. **Medium Impact, Medium Effort**: Object pooling, optimized board representation
3. **High Impact, High Effort**: Texture atlas, batch rendering system

View File

@ -0,0 +1,128 @@
# SOLID Principles Refactoring Plan
## Current Architecture Issues
### 1. Single Responsibility Principle (SRP) Violations
- `ApplicationManager` handles initialization, coordination, rendering coordination, and asset management
- `Game` class mixes game logic with some presentation concerns
### 2. Open/Closed Principle (OCP) Opportunities
- Hard-coded piece types and behaviors
- Limited extensibility for new game modes or rule variations
### 3. Dependency Inversion Principle (DIP) Missing
- Concrete dependencies instead of interfaces
- Direct instantiation rather than dependency injection
## Proposed Improvements
### 1. Extract Interfaces
```cpp
// src/core/interfaces/IRenderer.h
class IRenderer {
public:
virtual ~IRenderer() = default;
virtual void clear(uint8_t r, uint8_t g, uint8_t b, uint8_t a) = 0;
virtual void present() = 0;
virtual SDL_Renderer* getSDLRenderer() = 0;
};
// src/core/interfaces/IAudioSystem.h
class IAudioSystem {
public:
virtual ~IAudioSystem() = default;
virtual void playSound(const std::string& name) = 0;
virtual void playMusic(const std::string& name) = 0;
virtual void setMasterVolume(float volume) = 0;
};
// src/core/interfaces/IAssetLoader.h
class IAssetLoader {
public:
virtual ~IAssetLoader() = default;
virtual SDL_Texture* loadTexture(const std::string& path) = 0;
virtual void loadFont(const std::string& name, const std::string& path, int size) = 0;
};
```
### 2. Dependency Injection Container
```cpp
// src/core/ServiceContainer.h
class ServiceContainer {
private:
std::unordered_map<std::type_index, std::shared_ptr<void>> services;
public:
template<typename T>
void registerService(std::shared_ptr<T> service) {
services[std::type_index(typeid(T))] = service;
}
template<typename T>
std::shared_ptr<T> getService() {
auto it = services.find(std::type_index(typeid(T)));
if (it != services.end()) {
return std::static_pointer_cast<T>(it->second);
}
return nullptr;
}
};
```
### 3. Break Down ApplicationManager
```cpp
// src/core/ApplicationLifecycle.h
class ApplicationLifecycle {
public:
bool initialize(int argc, char* argv[]);
void run();
void shutdown();
};
// src/core/SystemCoordinator.h
class SystemCoordinator {
public:
void initializeSystems(ServiceContainer& container);
void updateSystems(double deltaTime);
void shutdownSystems();
};
```
### 4. Strategy Pattern for Game Rules
```cpp
// src/gameplay/interfaces/IGameRules.h
class IGameRules {
public:
virtual ~IGameRules() = default;
virtual int calculateScore(int linesCleared, int level) = 0;
virtual double getGravitySpeed(int level) = 0;
virtual bool shouldLevelUp(int lines) = 0;
};
// src/gameplay/rules/ClassicTetrisRules.h
class ClassicTetrisRules : public IGameRules {
public:
int calculateScore(int linesCleared, int level) override;
double getGravitySpeed(int level) override;
bool shouldLevelUp(int lines) override;
};
```
## Implementation Priority
1. **Phase 1**: Extract core interfaces (IRenderer, IAudioSystem)
2. **Phase 2**: Implement dependency injection container
3. **Phase 3**: Break down ApplicationManager responsibilities
4. **Phase 4**: Add strategy patterns for game rules
5. **Phase 5**: Improve testability with mock implementations
## Benefits
- **Testability**: Easy to mock dependencies for unit tests
- **Extensibility**: New features without modifying existing code
- **Maintainability**: Clear responsibilities and loose coupling
- **Flexibility**: Easy to swap implementations (e.g., different renderers)

309
docs/TESTING_STRATEGY.md Normal file
View File

@ -0,0 +1,309 @@
# Testing Strategy Enhancement
## Current Testing State
### Existing Tests
- ✅ GravityTests.cpp - Basic gravity manager testing
- ✅ Catch2 framework integration
- ✅ CTest integration in CMake
### Coverage Gaps
- ❌ Game logic testing (piece movement, rotation, line clearing)
- ❌ Collision detection testing
- ❌ Scoring system testing
- ❌ State management testing
- ❌ Integration tests
- ❌ Performance tests
## Comprehensive Testing Strategy
### 1. Unit Tests Expansion
```cpp
// tests/GameLogicTests.cpp
TEST_CASE("Piece Movement", "[game][movement]") {
Game game(0);
Piece originalPiece = game.current();
SECTION("Move left when possible") {
game.move(-1);
REQUIRE(game.current().x == originalPiece.x - 1);
}
SECTION("Cannot move left at boundary") {
// Move piece to left edge
while (game.current().x > 0) {
game.move(-1);
}
int edgeX = game.current().x;
game.move(-1);
REQUIRE(game.current().x == edgeX); // Should not move further
}
}
// tests/CollisionTests.cpp
TEST_CASE("Collision Detection", "[game][collision]") {
Game game(0);
SECTION("Piece collides with bottom") {
// Force piece to bottom
while (!game.isGameOver()) {
game.hardDrop();
if (game.isGameOver()) break;
}
// Verify collision behavior
}
SECTION("Piece collides with placed blocks") {
// Place a block manually
// Test collision with new piece
}
}
// tests/ScoringTests.cpp
TEST_CASE("Scoring System", "[game][scoring]") {
Game game(0);
int initialScore = game.score();
SECTION("Single line clear") {
// Set up board with almost complete line
// Clear line and verify score increase
}
SECTION("Tetris (4 lines)") {
// Set up board for Tetris
// Verify bonus scoring
}
}
```
### 2. Mock Objects for Testing
```cpp
// tests/mocks/MockRenderer.h
class MockRenderer : public IRenderer {
private:
mutable std::vector<std::string> calls;
public:
void clear(uint8_t r, uint8_t g, uint8_t b, uint8_t a) override {
calls.push_back("clear");
}
void present() override {
calls.push_back("present");
}
SDL_Renderer* getSDLRenderer() override {
return nullptr; // Mock implementation
}
const std::vector<std::string>& getCalls() const { return calls; }
void clearCalls() { calls.clear(); }
};
// tests/mocks/MockAudioSystem.h
class MockAudioSystem : public IAudioSystem {
private:
std::vector<std::string> playedSounds;
public:
void playSound(const std::string& name) override {
playedSounds.push_back(name);
}
void playMusic(const std::string& name) override {
playedSounds.push_back("music:" + name);
}
void setMasterVolume(float volume) override {
// Mock implementation
}
const std::vector<std::string>& getPlayedSounds() const { return playedSounds; }
};
```
### 3. Integration Tests
```cpp
// tests/integration/StateTransitionTests.cpp
TEST_CASE("State Transitions", "[integration][states]") {
ApplicationManager app;
// Mock dependencies
SECTION("Loading to Menu transition") {
// Simulate loading completion
// Verify menu state activation
}
SECTION("Menu to Game transition") {
// Simulate start game action
// Verify game state initialization
}
}
// tests/integration/GamePlayTests.cpp
TEST_CASE("Complete Game Session", "[integration][gameplay]") {
Game game(0);
SECTION("Play until first line clear") {
// Simulate complete game session
// Verify all systems work together
}
}
```
### 4. Performance Tests
```cpp
// tests/performance/PerformanceTests.cpp
TEST_CASE("Game Logic Performance", "[performance]") {
Game game(0);
SECTION("1000 piece drops should complete in reasonable time") {
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 1000; ++i) {
game.hardDrop();
if (game.isGameOver()) {
game.reset(0);
}
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
REQUIRE(duration.count() < 100); // Should complete in under 100ms
}
}
// tests/performance/MemoryTests.cpp
TEST_CASE("Memory Usage", "[performance][memory]") {
SECTION("No memory leaks during gameplay") {
size_t initialMemory = getCurrentMemoryUsage();
{
Game game(0);
// Simulate gameplay
for (int i = 0; i < 100; ++i) {
game.hardDrop();
if (game.isGameOver()) game.reset(0);
}
}
size_t finalMemory = getCurrentMemoryUsage();
REQUIRE(finalMemory <= initialMemory + 1024); // Allow small overhead
}
}
```
### 5. Property-Based Testing
```cpp
// tests/property/PropertyTests.cpp
TEST_CASE("Property: Game state consistency", "[property]") {
Game game(0);
SECTION("Score never decreases") {
int previousScore = game.score();
// Perform random valid actions
for (int i = 0; i < 100; ++i) {
performRandomValidAction(game);
REQUIRE(game.score() >= previousScore);
previousScore = game.score();
}
}
SECTION("Board state remains valid") {
for (int i = 0; i < 1000; ++i) {
performRandomValidAction(game);
REQUIRE(isBoardStateValid(game));
}
}
}
```
### 6. Test Data Management
```cpp
// tests/fixtures/GameFixtures.h
class GameFixtures {
public:
static Game createGameWithAlmostFullLine() {
Game game(0);
// Set up specific board state
return game;
}
static Game createGameNearGameOver() {
Game game(0);
// Fill board almost to top
return game;
}
static std::vector<PieceType> createTetrisPieceSequence() {
return {I, O, T, S, Z, J, L};
}
};
```
## Test Automation & CI
### 1. GitHub Actions Configuration
```yaml
# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
build-type: [Debug, Release]
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: |
# Install vcpkg and dependencies
- name: Configure CMake
run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build-type }}
- name: Build
run: cmake --build build --config ${{ matrix.build-type }}
- name: Test
run: ctest --test-dir build --build-config ${{ matrix.build-type }}
```
### 2. Code Coverage
```cmake
# Add to CMakeLists.txt
option(ENABLE_COVERAGE "Enable code coverage" OFF)
if(ENABLE_COVERAGE)
target_compile_options(tetris PRIVATE --coverage)
target_link_libraries(tetris PRIVATE --coverage)
endif()
```
## Quality Metrics Targets
- **Unit Test Coverage**: > 80%
- **Integration Test Coverage**: > 60%
- **Performance Regression**: < 5% per release
- **Memory Leak Detection**: 0 leaks in test suite
- **Static Analysis**: 0 critical issues
## Implementation Priority
1. **Phase 1**: Core game logic unit tests (movement, rotation, collision)
2. **Phase 2**: Mock objects and dependency injection for testability
3. **Phase 3**: Integration tests for state management
4. **Phase 4**: Performance and memory tests
5. **Phase 5**: Property-based testing and fuzzing
6. **Phase 6**: CI/CD pipeline with automated testing

15
settings.ini Normal file
View File

@ -0,0 +1,15 @@
; Tetris Game Settings
; This file is auto-generated
[Display]
Fullscreen=1
[Audio]
Music=1
Sound=0
[Player]
Name=Player
[Debug]
Enabled=0

View File

@ -20,6 +20,12 @@
#pragma comment(lib, "mfuuid.lib")
#pragma comment(lib, "ole32.lib")
using Microsoft::WRL::ComPtr;
#ifdef max
#undef max
#endif
#ifdef min
#undef min
#endif
#endif
Audio& Audio::instance(){ static Audio inst; return inst; }
@ -63,14 +69,34 @@ bool Audio::ensureStream(){
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] SDL_OpenAudioDeviceStream failed: %s", SDL_GetError());
return false;
}
// Ensure the device is running so SFX can be heard even before music starts
SDL_ResumeAudioStreamDevice(audioStream);
return true;
}
void Audio::start(){ if(!ensureStream()) return; if(!playing){ current=-1; nextTrack(); SDL_ResumeAudioStreamDevice(audioStream); playing=true; } }
void Audio::start(){
if(!ensureStream()) return;
// If no track is selected yet, try to select one now (in case tracks loaded after initial start)
if(current < 0) {
nextTrack();
}
SDL_ResumeAudioStreamDevice(audioStream);
playing = true;
}
void Audio::toggleMute(){ muted=!muted; }
void Audio::setMuted(bool m){ muted=m; }
void Audio::nextTrack(){ if(tracks.empty()) return; for(size_t i=0;i<tracks.size(); ++i){ current = (current + 1) % (int)tracks.size(); if(tracks[current].ok){ tracks[current].cursor=0; return; } } current=-1; }
void Audio::nextTrack(){
if(tracks.empty()) { current = -1; return; }
// Try every track once to find a decodable one
int start = current;
for(size_t i=0;i<tracks.size(); ++i){
current = (current + 1) % (int)tracks.size();
if(tracks[current].ok){ tracks[current].cursor=0; return; }
}
current=-1;
}
void Audio::feed(Uint32 bytesWanted, SDL_AudioStream* stream){
if(bytesWanted==0) return;
@ -79,25 +105,52 @@ void Audio::feed(Uint32 bytesWanted, SDL_AudioStream* stream){
std::vector<int16_t> 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;i<toCopy;++i){
int v = (int)mix[startSample+i] + (int)trk.pcm[trk.cursor+i];
int v = (int)mix[startSample+i] + (int)trk->pcm[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();
}
}
}
}
@ -156,7 +209,16 @@ void Audio::addTrackAsync(const std::string& path) {
}
void Audio::startBackgroundLoading() {
if (loadingThread.joinable()) return; // Already running
// If a previous loading thread exists but has finished, join it so we can start anew
if (loadingThread.joinable()) {
if (loadingComplete) {
loadingThread.join();
} else {
// Already running
return;
}
}
loadingAbort = false;
loadingComplete = false;
loadedCount = 0;
loadingThread = std::thread(&Audio::backgroundLoadingThread, this);
@ -174,14 +236,20 @@ void Audio::backgroundLoadingThread() {
}
#endif
// Copy pending tracks to avoid holding the mutex during processing
std::vector<std::string> tracksToProcess;
{
std::lock_guard<std::mutex> lock(pendingTracksMutex);
tracksToProcess = pendingTracks;
}
for (const std::string& path : tracksToProcess) {
while (true) {
if (loadingAbort.load()) {
break;
}
std::string path;
{
std::lock_guard<std::mutex> lock(pendingTracksMutex);
if (pendingTracks.empty()) break;
path = std::move(pendingTracks.front());
pendingTracks.erase(pendingTracks.begin());
if (loadingAbort.load()) {
break;
}
}
AudioTrack t;
t.path = path;
#ifdef _WIN32
@ -195,15 +263,23 @@ void Audio::backgroundLoadingThread() {
#endif
// Thread-safe addition to tracks
if (loadingAbort.load()) {
break;
}
{
std::lock_guard<std::mutex> lock(tracksMutex);
tracks.push_back(std::move(t));
}
loadedCount++;
loadedCount++;
// Small delay to prevent overwhelming the system
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Small delay to prevent overwhelming the system (unless abort requested)
if (!loadingAbort.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
} else {
break;
}
}
#ifdef _WIN32
@ -233,11 +309,50 @@ 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
loadingAbort = true;
{
std::lock_guard<std::mutex> lock(pendingTracksMutex);
pendingTracks.clear();
}
if (loadingThread.joinable()) {
loadingThread.join();
}
loadingComplete = true;
if(audioStream){ SDL_DestroyAudioStream(audioStream); audioStream=nullptr; }
tracks.clear();
@ -250,3 +365,40 @@ void Audio::shutdown(){
if(mfStarted){ MFShutdown(); mfStarted=false; }
#endif
}
// IAudioSystem interface implementation
void Audio::playSound(const std::string& name) {
// This is a simplified implementation - in a full implementation,
// you would load sound effects by name from assets
// For now, we'll just trigger a generic sound effect
// In practice, this would load a sound file and play it via playSfx
}
void Audio::playMusic(const std::string& name) {
// This is a simplified implementation - in a full implementation,
// you would load music tracks by name
// For now, we'll just start the current playlist
if (!tracks.empty() && !playing) {
start();
}
}
void Audio::stopMusic() {
playing = false;
}
void Audio::setMasterVolume(float volume) {
m_masterVolume = std::max(0.0f, std::min(1.0f, volume));
}
void Audio::setMusicVolume(float volume) {
m_musicVolume = std::max(0.0f, std::min(1.0f, volume));
}
void Audio::setSoundVolume(float volume) {
m_sfxVolume = std::max(0.0f, std::min(1.0f, volume));
}
bool Audio::isMusicPlaying() const {
return playing;
}

View File

@ -8,6 +8,7 @@
#include <thread>
#include <mutex>
#include <atomic>
#include "../core/interfaces/IAudioSystem.h"
struct AudioTrack {
std::string path;
@ -18,9 +19,20 @@ struct AudioTrack {
bool ok = false;
};
class Audio {
class Audio : public IAudioSystem {
public:
static Audio& instance();
// IAudioSystem interface implementation
void playSound(const std::string& name) override;
void playMusic(const std::string& name) override;
void stopMusic() override;
void setMasterVolume(float volume) override;
void setMusicVolume(float volume) override;
void setSoundVolume(float volume) override;
bool isMusicPlaying() const override;
// Existing Audio class methods
bool init(); // initialize backend (MF on Windows)
void addTrack(const std::string& path); // decode MP3 -> PCM16 stereo 44100
void addTrackAsync(const std::string& path); // add track for background loading
@ -31,6 +43,14 @@ public:
void shuffle(); // randomize order
void start(); // begin playback
void toggleMute();
void setMuted(bool m);
bool isMuted() const { return muted; }
// 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<int16_t>& pcm, int channels, int rate, float volume);
void shutdown();
@ -42,7 +62,10 @@ private:
bool ensureStream();
void backgroundLoadingThread(); // background thread function
std::vector<AudioTrack> tracks; int current=-1; bool playing=false; bool muted=false; std::mt19937 rng{std::random_device{}()};
std::vector<AudioTrack> 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
@ -51,10 +74,16 @@ private:
std::mutex tracksMutex;
std::mutex pendingTracksMutex;
std::atomic<bool> loadingComplete{false};
std::atomic<bool> loadingAbort{false};
std::atomic<int> loadedCount{0};
// SFX mixing support
struct SfxPlay { std::vector<int16_t> pcm; size_t cursor=0; };
std::vector<SfxPlay> activeSfx;
std::mutex sfxMutex;
// Volume control
float m_masterVolume = 1.0f;
float m_musicVolume = 1.0f;
float m_sfxVolume = 1.0f;
};

163
src/core/Config.h Normal file
View File

@ -0,0 +1,163 @@
#pragma once
#include <SDL3/SDL.h>
/**
* Config - Centralized configuration constants
*
* Replaces magic numbers and scattered constants throughout main.cpp
* Organized by functional area for easy maintenance and modification
*/
namespace Config {
// Window and Display Settings
namespace Window {
constexpr int DEFAULT_WIDTH = 1200;
constexpr int DEFAULT_HEIGHT = 1000;
constexpr const char* DEFAULT_TITLE = "Tetris (SDL3)";
constexpr bool DEFAULT_VSYNC = true;
}
// Logical rendering dimensions (internal coordinate system)
namespace Logical {
constexpr int WIDTH = 1200;
constexpr int HEIGHT = 1000;
}
// Gameplay constants
namespace Gameplay {
constexpr double DAS_DELAY = 170.0; // Delayed Auto Shift delay in ms
constexpr double ARR_RATE = 40.0; // Auto Repeat Rate in ms
constexpr float LEVEL_FADE_DURATION = 5000.0f; // Level background fade time in ms
constexpr int MAX_LEVELS = 20; // Maximum selectable starting level
// Gravity speed multiplier: 1.0 = normal, 2.0 = 2x slower, 0.5 = 2x faster
constexpr double GRAVITY_SPEED_MULTIPLIER = 1;
}
// UI Layout constants
namespace UI {
constexpr float MIN_MARGIN = 40.0f;
constexpr float PANEL_WIDTH = 180.0f;
constexpr float PANEL_SPACING = 30.0f;
constexpr float BUTTON_HEIGHT_SMALL = 60.0f;
constexpr float BUTTON_HEIGHT_NORMAL = 70.0f;
constexpr float BUTTON_WIDTH_SMALL = 0.4f; // Fraction of screen width
constexpr float BUTTON_WIDTH_NORMAL = 300.0f;
constexpr float SETTINGS_GEAR_SIZE = 50.0f;
constexpr float SETTINGS_GEAR_MARGIN = 10.0f;
// Screen size breakpoints
constexpr float SMALL_SCREEN_BREAKPOINT = 700.0f;
// Menu positioning
constexpr float MENU_BUTTON_Y_OFFSET = 40.0f;
constexpr float MENU_BUTTON_Y_BASE = 0.86f; // Fraction of screen height
}
// Loading screen constants
namespace Loading {
constexpr float LOGO_HEIGHT_FACTOR_LIMITED = 0.25f; // When height < 450
constexpr float LOGO_HEIGHT_FACTOR_NORMAL = 0.4f;
constexpr float LOGO_MAX_WIDTH_FACTOR = 0.9f; // Fraction of screen width
constexpr float LOGO_MAX_WIDTH_ABSOLUTE = 600.0f;
constexpr int LOGO_ORIGINAL_WIDTH = 872;
constexpr int LOGO_ORIGINAL_HEIGHT = 273;
constexpr float LOADING_TEXT_HEIGHT = 20.0f;
constexpr float LOADING_BAR_HEIGHT = 20.0f;
constexpr float LOADING_BAR_PADDING_LIMITED = 15.0f;
constexpr float LOADING_BAR_PADDING_NORMAL = 35.0f;
constexpr float LOADING_PERCENTAGE_HEIGHT = 24.0f;
constexpr float LOADING_ELEMENT_SPACING_LIMITED = 5.0f;
constexpr float LOADING_ELEMENT_SPACING_NORMAL = 15.0f;
constexpr int LIMITED_HEIGHT_THRESHOLD = 450;
}
// Animation constants
namespace Animation {
constexpr double LOGO_ANIM_SPEED = 0.0008; // Logo animation speed multiplier
constexpr float STARFIELD_UPDATE_DIVISOR = 1000.0f; // Convert ms to seconds
}
// HUD and Game Display
namespace HUD {
constexpr float GRAVITY_DISPLAY_X = 260.0f; // Distance from right edge
constexpr float GRAVITY_DISPLAY_Y = 10.0f;
constexpr float SCORE_PANEL_X_OFFSET = 120.0f; // Distance from center
constexpr float SCORE_PANEL_BASE_Y = 220.0f;
constexpr float CONTROLS_HINT_Y_OFFSET = 30.0f; // Distance from bottom
}
// Popup and Modal constants
namespace Popup {
constexpr float EXIT_CONFIRM_WIDTH = 400.0f;
constexpr float EXIT_CONFIRM_HEIGHT = 200.0f;
constexpr float SETTINGS_POPUP_WIDTH = 300.0f;
constexpr float SETTINGS_POPUP_HEIGHT = 250.0f;
}
// Color definitions (commonly used colors)
namespace Colors {
constexpr SDL_Color WHITE = {255, 255, 255, 255};
constexpr SDL_Color BLACK = {0, 0, 0, 255};
constexpr SDL_Color YELLOW_TITLE = {255, 220, 0, 255};
constexpr SDL_Color GRAY_TEXT = {220, 220, 230, 255};
constexpr SDL_Color BLUE_HIGHLIGHT = {200, 240, 255, 255};
constexpr SDL_Color RED_ERROR = {255, 80, 60, 255};
constexpr SDL_Color GREEN_SUCCESS = {0, 255, 0, 255};
constexpr SDL_Color RED_DISABLED = {255, 0, 0, 255};
constexpr SDL_Color CONTROL_HINT = {150, 150, 170, 255};
constexpr SDL_Color PAUSED_TEXT = {255, 255, 255, 255};
constexpr SDL_Color PAUSED_HINT = {200, 200, 220, 255};
constexpr SDL_Color SHADOW = {0, 0, 0, 200};
}
// Font configuration
namespace Fonts {
constexpr int DEFAULT_FONT_SIZE = 24;
constexpr int PIXEL_FONT_SIZE = 16;
constexpr const char* DEFAULT_FONT_PATH = "FreeSans.ttf";
constexpr const char* PIXEL_FONT_PATH = "assets/fonts/PressStart2P-Regular.ttf";
}
// Asset paths
namespace Assets {
constexpr const char* IMAGES_DIR = "assets/images/";
constexpr const char* MUSIC_DIR = "assets/music/";
constexpr const char* FONTS_DIR = "assets/fonts/";
// Specific asset files
constexpr const char* LOGO_BMP = "assets/images/logo.bmp";
constexpr const char* LOGO_SMALL_BMP = "assets/images/logo_small.bmp";
constexpr const char* BACKGROUND_BMP = "assets/images/main_background.bmp";
constexpr const char* BLOCKS_BMP = "assets/images/blocks90px_001.bmp";
}
// Audio settings
namespace Audio {
constexpr float DEFAULT_VOLUME = 1.0f;
constexpr float LETS_GO_VOLUME = 1.0f;
constexpr int MAX_MUSIC_TRACKS = 100; // Maximum number of music files to scan
}
// Input settings
namespace Input {
constexpr int MOUSE_BUTTON_PRIMARY = SDL_BUTTON_LEFT;
constexpr Uint32 MODIFIER_SHIFT = SDL_KMOD_SHIFT;
constexpr Uint32 MODIFIER_CTRL = SDL_KMOD_CTRL;
}
// Performance settings
namespace Performance {
constexpr float MIN_FRAME_TIME = 0.05f; // Cap at 20 FPS minimum (prevent spiral of death)
constexpr int STARFIELD_PARTICLE_COUNT = 200;
constexpr int STARFIELD_3D_PARTICLE_COUNT = 200;
}
// Visual effects settings
namespace Visuals {
constexpr int PAUSE_BLUR_ITERATIONS = 8; // Number of blur passes (higher = more blur)
constexpr int PAUSE_BLUR_OFFSET = 3; // Pixel spread of the blur
constexpr int PAUSE_BLUR_ALPHA = 40; // Alpha intensity of blur layers
}
}

271
src/core/GlobalState.cpp Normal file
View File

@ -0,0 +1,271 @@
#include "GlobalState.h"
#include "Config.h"
#include <SDL3/SDL.h>
#include <algorithm>
#include <random>
GlobalState& GlobalState::instance() {
static GlobalState instance;
return instance;
}
void GlobalState::initialize() {
if (m_initialized) {
return;
}
// Initialize timing
lastMs = SDL_GetTicks();
loadStart = SDL_GetTicks();
// Initialize viewport to logical dimensions
logicalVP = {0, 0, Config::Logical::WIDTH, Config::Logical::HEIGHT};
// Initialize fireworks system
fireworks.clear();
lastFireworkTime = 0;
m_initialized = true;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[GlobalState] Initialized");
}
void GlobalState::shutdown() {
if (!m_initialized) {
return;
}
// Clear fireworks
fireworks.clear();
m_initialized = false;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[GlobalState] Shutdown complete");
}
void GlobalState::updateFireworks(double frameMs) {
const Uint64 currentTime = SDL_GetTicks();
// Check if we have any active fireworks
bool hasActiveFirework = false;
for (const auto& fw : fireworks) {
if (fw.active) {
hasActiveFirework = true;
break;
}
}
// Only create new firework if no active ones exist
if (!hasActiveFirework && currentTime - lastFireworkTime > 1500 + (rand() % 2000)) {
float x = Config::Logical::WIDTH * (0.15f + (rand() % 70) / 100.0f);
float y = Config::Logical::HEIGHT * (0.20f + (rand() % 60) / 100.0f);
createFirework(x, y);
lastFireworkTime = currentTime;
}
// Update existing fireworks
for (auto& firework : fireworks) {
if (!firework.active) continue;
bool hasActiveParticles = false;
std::vector<BlockParticle> newParticles;
newParticles.reserve(20); // Pre-allocate to avoid reallocation
for (auto& particle : firework.particles) {
if (particle.life <= 0) continue;
// Update physics
float dt = float(frameMs / 1000.0f);
particle.x += particle.vx * dt;
particle.y += particle.vy * dt;
particle.vx *= (1.0f - 0.5f * dt);
particle.vy = particle.vy * (1.0f - 0.2f * dt) + 80.0f * dt;
particle.life -= frameMs;
// Update size
float lifeRatio = particle.life / particle.maxLife;
particle.size = (6.0f - particle.generation * 2.0f) + (4.0f - particle.generation) * lifeRatio;
// Only primary particles create secondary explosions (single cascade level)
if (!particle.hasExploded && particle.generation == 0 && lifeRatio < 0.5f) {
particle.hasExploded = true;
// Spawn only 3-4 secondary particles
int secondaryCount = 3 + (rand() % 2);
for (int i = 0; i < secondaryCount; ++i) {
BlockParticle secondary;
secondary.x = particle.x;
secondary.y = particle.y;
secondary.generation = 1; // Only one level of cascade
secondary.hasExploded = true; // Don't cascade further
float angle = (float)(rand() % 360) * 3.14159f / 180.0f;
float speed = 40.0f + (rand() % 40);
secondary.vx = cos(angle) * speed;
secondary.vy = sin(angle) * speed - 20.0f;
secondary.type = 1 + (rand() % 7);
secondary.maxLife = 700.0f + (rand() % 400);
secondary.life = secondary.maxLife;
secondary.size = 3.0f + (rand() % 2);
newParticles.push_back(secondary);
}
}
if (particle.life > 0) {
hasActiveParticles = true;
}
}
// Add secondary particles
for (auto& newParticle : newParticles) {
firework.particles.push_back(newParticle);
hasActiveParticles = true;
}
firework.active = hasActiveParticles;
}
}
void GlobalState::createFirework(float x, float y) {
// Find an inactive firework to reuse
TetrisFirework* firework = nullptr;
for (auto& fw : fireworks) {
if (!fw.active) {
firework = &fw;
break;
}
}
// If no inactive firework found, create a new one
if (!firework) {
fireworks.emplace_back();
firework = &fireworks.back();
}
// Initialize firework
firework->active = true;
firework->particles.clear();
// Create fewer particles for subtle background effect
const int particleCount = 12 + (rand() % 8); // 12-20 particles per explosion
for (int i = 0; i < particleCount; ++i) {
BlockParticle particle;
particle.x = x;
particle.y = y;
particle.generation = 0; // Primary explosion
particle.hasExploded = false;
// Random velocity in all directions
float angle = (float)(rand() % 360) * 3.14159f / 180.0f;
float speed = 80.0f + (rand() % 100); // Moderate speed
particle.vx = cos(angle) * speed;
particle.vy = sin(angle) * speed - 50.0f; // Slight upward bias
particle.type = 1 + (rand() % 7); // Random tetris piece color
particle.maxLife = 1500.0f + (rand() % 1000); // Medium life: ~1.5-2.5 seconds
particle.life = particle.maxLife;
particle.size = 6.0f + (rand() % 5); // Smaller particles
firework->particles.push_back(particle);
}
}
void GlobalState::drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex) {
(void)blocksTex; // Not using texture anymore
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
for (const auto& firework : fireworks) {
if (!firework.active) continue;
for (const auto& particle : firework.particles) {
if (particle.life <= 0) continue;
// Calculate alpha based on remaining life
float lifeRatio = particle.life / particle.maxLife;
Uint8 alpha = (Uint8)(128 * std::min(1.0f, lifeRatio * 1.6f));
// Color based on particle type
SDL_Color colors[] = {
{0, 240, 240, alpha}, // Cyan
{240, 160, 0, alpha}, // Orange
{0, 0, 240, alpha}, // Blue
{240, 240, 0, alpha}, // Yellow
{0, 240, 0, alpha}, // Green
{160, 0, 240, alpha}, // Purple
{240, 0, 0, alpha} // Red
};
SDL_Color color = colors[(particle.type - 1) % 7];
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
// For small particles, just draw a filled rect (much faster)
if (particle.size <= 4.0f) {
SDL_FRect rect{particle.x - particle.size/2, particle.y - particle.size/2, particle.size, particle.size};
SDL_RenderFillRect(renderer, &rect);
} else {
// For larger particles, draw a simple circle approximation
float radius = particle.size / 2.0f;
int r = (int)radius;
for (int dy = -r; dy <= r; ++dy) {
int width = (int)sqrt(radius*radius - dy*dy) * 2;
if (width > 0) {
SDL_FRect line{particle.x - width/2.0f, particle.y + dy, (float)width, 1.0f};
SDL_RenderFillRect(renderer, &line);
}
}
}
}
}
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
}
void GlobalState::resetGameState() {
// Reset game-related state
leftHeld = false;
rightHeld = false;
moveTimerMs = 0.0;
startLevelSelection = 0;
}
void GlobalState::resetUIState() {
// Reset UI state
showSettingsPopup = false;
showExitConfirmPopup = false;
hoveredButton = -1;
}
void GlobalState::resetAnimationState() {
// Reset animation state
logoAnimCounter = 0.0;
fireworks.clear();
lastFireworkTime = 0;
}
void GlobalState::updateLogicalDimensions(int windowWidth, int windowHeight) {
// For now, keep logical dimensions proportional to window size
// You can adjust this logic based on your specific needs
// Option 1: Keep fixed aspect ratio and scale uniformly
const float targetAspect = static_cast<float>(Config::Logical::WIDTH) / static_cast<float>(Config::Logical::HEIGHT);
const float windowAspect = static_cast<float>(windowWidth) / static_cast<float>(windowHeight);
if (windowAspect > targetAspect) {
// Window is wider than target aspect - fit to height
currentLogicalHeight = Config::Logical::HEIGHT;
currentLogicalWidth = static_cast<int>(currentLogicalHeight * windowAspect);
} else {
// Window is taller than target aspect - fit to width
currentLogicalWidth = Config::Logical::WIDTH;
currentLogicalHeight = static_cast<int>(currentLogicalWidth / windowAspect);
}
// Ensure minimum sizes
currentLogicalWidth = std::max(currentLogicalWidth, 800);
currentLogicalHeight = std::max(currentLogicalHeight, 600);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"[GlobalState] Updated logical dimensions: %dx%d (window: %dx%d)",
currentLogicalWidth, currentLogicalHeight, windowWidth, windowHeight);
}

126
src/core/GlobalState.h Normal file
View File

@ -0,0 +1,126 @@
#pragma once
#include <vector>
#include <memory>
#include <SDL3/SDL.h>
// Forward declarations
class FontAtlas;
class Game;
class ScoreManager;
class Starfield;
class Starfield3D;
class LineEffect;
/**
* GlobalState - Centralized management of application-wide state
*
* Replaces global variables scattered throughout main.cpp
* Provides controlled access to shared state between systems
* Will eventually be replaced by proper dependency injection
*/
class GlobalState {
public:
// Singleton access (temporary until dependency injection is implemented)
static GlobalState& instance();
// Initialization and cleanup
void initialize();
void shutdown();
// Application state flags
bool running = true;
bool isFullscreen = false;
bool musicEnabled = true;
bool musicStarted = false;
bool musicLoaded = false;
// UI state flags
bool showSettingsPopup = false;
bool showExitConfirmPopup = false;
int hoveredButton = -1; // -1 = none, 0 = play, 1 = level, 2 = settings
// Input state (will be migrated to InputManager)
bool leftHeld = false;
bool rightHeld = false;
double moveTimerMs = 0.0;
// Loading state
double loadingProgress = 0.0;
int currentTrackLoading = 0;
int totalTracks = 0;
int startLevelSelection = 0;
// Timing
Uint64 lastMs = 0;
Uint64 loadStart = 0;
// Animation state
double logoAnimCounter = 0.0;
// Level background caching
int cachedLevel = -1;
float levelFadeAlpha = 0.0f;
float levelFadeElapsed = 0.0f;
// Viewport and scaling
SDL_Rect logicalVP{0, 0, 1200, 1000}; // Will use Config::Logical constants
float logicalScale = 1.0f;
// Dynamic logical dimensions (computed from window size)
int currentLogicalWidth = 1200;
int currentLogicalHeight = 1000;
// Fireworks system (for menu animation)
struct BlockParticle {
float x, y, vx, vy;
int type;
float life, maxLife;
float size;
int generation = 0; // 0 = primary, 1 = secondary, 2 = tertiary
bool hasExploded = false; // Track if this particle has spawned children
};
struct TetrisFirework {
std::vector<BlockParticle> particles;
bool active = false;
};
std::vector<TetrisFirework> fireworks;
Uint64 lastFireworkTime = 0;
// Fireworks management methods
void updateFireworks(double frameMs);
void createFirework(float x, float y);
void drawFireworks(SDL_Renderer* renderer, SDL_Texture* blocksTex);
// Logical dimensions management
void updateLogicalDimensions(int windowWidth, int windowHeight);
int getLogicalWidth() const { return currentLogicalWidth; }
int getLogicalHeight() const { return currentLogicalHeight; }
// Reset methods for different states
void resetGameState();
void resetUIState();
void resetAnimationState();
private:
GlobalState() = default;
~GlobalState() = default;
GlobalState(const GlobalState&) = delete;
GlobalState& operator=(const GlobalState&) = delete;
bool m_initialized = false;
};
// Convenience accessors (temporary until proper dependency injection)
namespace Globals {
inline GlobalState& state() { return GlobalState::instance(); }
// Quick access to commonly used flags
inline bool& running() { return state().running; }
inline bool& musicEnabled() { return state().musicEnabled; }
inline bool& showSettingsPopup() { return state().showSettingsPopup; }
inline int& hoveredButton() { return state().hoveredButton; }
inline double& logoAnimCounter() { return state().logoAnimCounter; }
}

View File

@ -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<double>(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);
}

View File

@ -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<double,30> levelMultipliers{}; // default 1.0
std::array<double,20> levelMultipliers{}; // default 1.0
};

View File

@ -0,0 +1,92 @@
#pragma once
#include <memory>
#include <unordered_map>
#include <typeindex>
#include <stdexcept>
/**
* @brief Dependency injection container for managing services
*
* Provides a centralized way to register and retrieve services,
* enabling loose coupling and better testability.
*/
class ServiceContainer {
private:
std::unordered_map<std::type_index, std::shared_ptr<void>> services_;
public:
/**
* @brief Register a service instance
* @tparam T Service type
* @param service Shared pointer to service instance
*/
template<typename T>
void registerService(std::shared_ptr<T> service) {
services_[std::type_index(typeid(T))] = service;
}
/**
* @brief Get a service instance
* @tparam T Service type
* @return Shared pointer to service instance
* @throws std::runtime_error if service is not registered
*/
template<typename T>
std::shared_ptr<T> getService() {
auto it = services_.find(std::type_index(typeid(T)));
if (it != services_.end()) {
return std::static_pointer_cast<T>(it->second);
}
throw std::runtime_error("Service not registered: " + std::string(typeid(T).name()));
}
/**
* @brief Get a service instance (const version)
* @tparam T Service type
* @return Shared pointer to service instance
* @throws std::runtime_error if service is not registered
*/
template<typename T>
std::shared_ptr<const T> getService() const {
auto it = services_.find(std::type_index(typeid(T)));
if (it != services_.end()) {
return std::static_pointer_cast<const T>(it->second);
}
throw std::runtime_error("Service not registered: " + std::string(typeid(T).name()));
}
/**
* @brief Check if a service is registered
* @tparam T Service type
* @return true if service is registered, false otherwise
*/
template<typename T>
bool hasService() const {
return services_.find(std::type_index(typeid(T))) != services_.end();
}
/**
* @brief Unregister a service
* @tparam T Service type
*/
template<typename T>
void unregisterService() {
services_.erase(std::type_index(typeid(T)));
}
/**
* @brief Clear all registered services
*/
void clear() {
services_.clear();
}
/**
* @brief Get the number of registered services
* @return Number of registered services
*/
size_t getServiceCount() const {
return services_.size();
}
};

112
src/core/Settings.cpp Normal file
View File

@ -0,0 +1,112 @@
#include "Settings.h"
#include <fstream>
#include <sstream>
#include <SDL3/SDL.h>
// Singleton instance
Settings& Settings::instance() {
static Settings s_instance;
return s_instance;
}
Settings::Settings() {
// Constructor - defaults already set in header
}
std::string Settings::getSettingsPath() {
// Save settings.ini in the game's directory
return "settings.ini";
}
bool Settings::load() {
std::ifstream file(getSettingsPath());
if (!file.is_open()) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Settings file not found, using defaults");
return false;
}
std::string line;
std::string currentSection;
while (std::getline(file, line)) {
// Trim whitespace
size_t start = line.find_first_not_of(" \t\r\n");
size_t end = line.find_last_not_of(" \t\r\n");
if (start == std::string::npos) continue; // Empty line
line = line.substr(start, end - start + 1);
// Skip comments
if (line[0] == ';' || line[0] == '#') continue;
// Check for section header
if (line[0] == '[' && line[line.length() - 1] == ']') {
currentSection = line.substr(1, line.length() - 2);
continue;
}
// Parse key=value
size_t equalsPos = line.find('=');
if (equalsPos == std::string::npos) continue;
std::string key = line.substr(0, equalsPos);
std::string value = line.substr(equalsPos + 1);
// Trim key and value
key.erase(key.find_last_not_of(" \t") + 1);
value.erase(0, value.find_first_not_of(" \t"));
// Parse settings
if (currentSection == "Display") {
if (key == "Fullscreen") {
m_fullscreen = (value == "1" || value == "true" || value == "True");
}
} else if (currentSection == "Audio") {
if (key == "Music") {
m_musicEnabled = (value == "1" || value == "true" || value == "True");
} else if (key == "Sound") {
m_soundEnabled = (value == "1" || value == "true" || value == "True");
}
} else if (currentSection == "Player") {
if (key == "Name") {
m_playerName = value;
}
} else if (currentSection == "Debug") {
if (key == "Enabled") {
m_debugEnabled = (value == "1" || value == "true" || value == "True");
}
}
}
file.close();
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Settings loaded from %s", getSettingsPath().c_str());
return true;
}
bool Settings::save() {
std::ofstream file(getSettingsPath());
if (!file.is_open()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to save settings to %s", getSettingsPath().c_str());
return false;
}
// Write settings in INI format
file << "; Tetris Game Settings\n";
file << "; This file is auto-generated\n\n";
file << "[Display]\n";
file << "Fullscreen=" << (m_fullscreen ? "1" : "0") << "\n\n";
file << "[Audio]\n";
file << "Music=" << (m_musicEnabled ? "1" : "0") << "\n";
file << "Sound=" << (m_soundEnabled ? "1" : "0") << "\n\n";
file << "[Player]\n";
file << "Name=" << m_playerName << "\n\n";
file << "[Debug]\n";
file << "Enabled=" << (m_debugEnabled ? "1" : "0") << "\n";
file.close();
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Settings saved to %s", getSettingsPath().c_str());
return true;
}

49
src/core/Settings.h Normal file
View File

@ -0,0 +1,49 @@
#pragma once
#include <string>
/**
* Settings - Persistent game settings manager
* Handles loading/saving settings to settings.ini
*/
class Settings {
public:
// Singleton access
static Settings& instance();
// Load settings from file (returns true if file existed)
bool load();
// Save settings to file
bool save();
// Settings accessors
bool isFullscreen() const { return m_fullscreen; }
void setFullscreen(bool value) { m_fullscreen = value; }
bool isMusicEnabled() const { return m_musicEnabled; }
void setMusicEnabled(bool value) { m_musicEnabled = value; }
bool isSoundEnabled() const { return m_soundEnabled; }
void setSoundEnabled(bool value) { m_soundEnabled = value; }
bool isDebugEnabled() const { return m_debugEnabled; }
void setDebugEnabled(bool value) { m_debugEnabled = value; }
const std::string& getPlayerName() const { return m_playerName; }
void setPlayerName(const std::string& name) { m_playerName = name; }
// Get the settings file path
static std::string getSettingsPath();
private:
Settings(); // Private constructor for singleton
Settings(const Settings&) = delete;
Settings& operator=(const Settings&) = delete;
// Settings values
bool m_fullscreen = false;
bool m_musicEnabled = true;
bool m_soundEnabled = true;
bool m_debugEnabled = false;
std::string m_playerName = "Player";
};

View File

@ -1,46 +0,0 @@
#include "StateManager.h"
StateManager::StateManager(AppState initial)
: currentState(initial)
{
}
void StateManager::registerHandler(AppState s, EventHandler h) {
handlers[static_cast<int>(s)].push_back(std::move(h));
}
void StateManager::registerOnEnter(AppState s, Hook h) {
onEnter[static_cast<int>(s)].push_back(std::move(h));
}
void StateManager::registerOnExit(AppState s, Hook h) {
onExit[static_cast<int>(s)].push_back(std::move(h));
}
// Overload accepting a no-arg function as handler (wraps it into an EventHandler)
void StateManager::registerHandler(AppState s, std::function<void()> h) {
EventHandler wrapper = [h = std::move(h)](const SDL_Event&) { h(); };
registerHandler(s, std::move(wrapper));
}
void StateManager::setState(AppState s) {
if (s == currentState) return;
// call exit hooks for current
auto it = onExit.find(static_cast<int>(currentState));
if (it != onExit.end()) {
for (auto &h : it->second) h();
}
currentState = s;
auto it2 = onEnter.find(static_cast<int>(currentState));
if (it2 != onEnter.end()) {
for (auto &h : it2->second) h();
}
}
AppState StateManager::getState() const { return currentState; }
void StateManager::handleEvent(const SDL_Event& e) {
auto it = handlers.find(static_cast<int>(currentState));
if (it == handlers.end()) return;
for (auto &h : it->second) h(e);
}

View File

@ -1,42 +0,0 @@
#pragma once
#include <functional>
#include <unordered_map>
#include <vector>
#include <SDL3/SDL.h>
// Application states used across the app
enum class AppState {
Loading,
Menu,
LevelSelector,
Playing,
LevelSelect,
GameOver
};
// State manager used by main to route events and lifecycle hooks
class StateManager {
public:
using EventHandler = std::function<void(const SDL_Event&)>;
using Hook = std::function<void()>;
StateManager(AppState initial);
void registerHandler(AppState s, EventHandler h);
void registerOnEnter(AppState s, Hook h);
void registerOnExit(AppState s, Hook h);
void registerHandler(AppState s, std::function<void()> h); // overload used in some places
void setState(AppState s);
AppState getState() const;
void handleEvent(const SDL_Event& e);
private:
AppState currentState;
std::unordered_map<int, std::vector<EventHandler>> handlers;
std::unordered_map<int, std::vector<Hook>> onEnter;
std::unordered_map<int, std::vector<Hook>> onExit;
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,150 @@
#pragma once
#include "../Config.h"
#include "../../states/State.h"
#include "../container/ServiceContainer.h"
#include <memory>
#include <string>
// Forward declarations
class RenderManager;
class InputManager;
class StateManager;
class AssetManager;
class Game;
class ScoreManager;
class Starfield;
class Starfield3D;
class FontAtlas;
class LineEffect;
// Forward declare state classes (top-level, defined under src/states)
class LoadingState;
class MenuState;
class OptionsState;
class LevelSelectorState;
class PlayingState;
/**
* ApplicationManager - Central coordinator for the entire application lifecycle
*
* Responsibilities:
* - Initialize and shutdown all subsystems
* - Coordinate the main application loop
* - Manage high-level application state
* - Provide clean separation between main() and application logic
*/
class ApplicationManager {
public:
ApplicationManager();
~ApplicationManager();
// Core lifecycle methods
bool initialize(int argc, char* argv[]);
void run();
void shutdown();
// Application state
bool isRunning() const { return m_running; }
void requestShutdown() { m_running = false; }
// Access to managers (for now, will be replaced with dependency injection later)
RenderManager* getRenderManager() const { return m_renderManager.get(); }
InputManager* getInputManager() const { return m_inputManager.get(); }
AssetManager* getAssetManager() const { return m_assetManager.get(); }
StateManager* getStateManager() const { return m_stateManager.get(); }
// Service container access
ServiceContainer& getServiceContainer() { return m_serviceContainer; }
private:
// Helper used by setupStateHandlers (defined in cpp)
static void renderLoading(ApplicationManager* app, RenderManager& renderer);
// Initialization methods
bool initializeSDL();
bool initializeManagers();
bool initializeGame();
void setupStateHandlers();
void registerServices();
// Main loop methods
void processEvents();
void update(float deltaTime);
void render();
// Cleanup methods
void cleanupManagers();
void cleanupSDL();
// Core managers
std::unique_ptr<RenderManager> m_renderManager;
std::unique_ptr<InputManager> m_inputManager;
std::unique_ptr<AssetManager> m_assetManager;
std::unique_ptr<StateManager> m_stateManager;
// Dependency injection container
ServiceContainer m_serviceContainer;
// Visual effects
std::unique_ptr<Starfield3D> m_starfield3D;
std::unique_ptr<Starfield> m_starfield;
// Menu / UI state pieces mirrored from main.cpp
bool m_musicEnabled = true;
int m_startLevelSelection = 0;
int m_hoveredButton = -1;
bool m_showSettingsPopup = false;
bool m_showExitConfirmPopup = false;
int m_exitPopupSelectedButton = 1; // 0 = YES, 1 = NO
bool m_isFullscreen = false;
std::string m_playerName = "PLAYER";
uint64_t m_loadStartTicks = 0;
bool m_musicStarted = false;
bool m_musicLoaded = false;
int m_currentTrackLoading = 0;
int m_totalTracks = 0;
// Persistence (ScoreManager declared at top-level)
std::unique_ptr<ScoreManager> m_scoreManager;
// Gameplay pieces
std::unique_ptr<Game> m_game;
std::unique_ptr<LineEffect> m_lineEffect;
// DAS/ARR movement timing (from original main.cpp)
bool m_leftHeld = false;
bool m_rightHeld = false;
double m_moveTimerMs = 0.0;
static constexpr double DAS = 170.0; // Delayed Auto Shift
static constexpr double ARR = 40.0; // Auto Repeat Rate
// State context (must be a member to ensure lifetime)
StateContext m_stateContext;
// State objects (mirror main.cpp pattern)
std::unique_ptr<LoadingState> m_loadingState;
std::unique_ptr<MenuState> m_menuState;
std::unique_ptr<OptionsState> m_optionsState;
std::unique_ptr<LevelSelectorState> m_levelSelectorState;
std::unique_ptr<PlayingState> m_playingState;
// Application state
bool m_running = false;
bool m_initialized = false;
// Timing
uint64_t m_lastFrameTime = 0;
// Configuration
int m_windowWidth = Config::Window::DEFAULT_WIDTH;
int m_windowHeight = Config::Window::DEFAULT_HEIGHT;
std::string m_windowTitle = Config::Window::DEFAULT_TITLE;
// Animation state
float m_logoAnimCounter = 0.0f;
// Gameplay background (per-level) with fade, mirroring main.cpp behavior
SDL_Texture* m_levelBackgroundTex = nullptr;
SDL_Texture* m_nextLevelBackgroundTex = nullptr; // used during fade transitions
float m_levelFadeAlpha = 0.0f; // 0..1 blend factor
float m_levelFadeElapsed = 0.0f; // ms
int m_cachedBgLevel = -1; // last loaded background level index
};

View File

@ -0,0 +1,489 @@
#include "AssetManager.h"
#include "../../graphics/ui/Font.h"
#include "../../audio/Audio.h"
#include "../../audio/SoundEffect.h"
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <filesystem>
#include "../../utils/ImagePathResolver.h"
AssetManager::AssetManager()
: m_renderer(nullptr)
, m_audioSystem(nullptr)
, m_soundSystem(nullptr)
, m_totalLoadingTasks(0)
, m_completedLoadingTasks(0)
, m_loadingComplete(false)
, m_defaultTexturePath("assets/images/")
, m_defaultFontPath("assets/fonts/")
, m_initialized(false) {
}
AssetManager::~AssetManager() {
shutdown();
}
bool AssetManager::initialize(SDL_Renderer* renderer) {
if (m_initialized) {
logError("AssetManager already initialized");
return false;
}
if (!renderer) {
setError("Invalid renderer provided to AssetManager");
return false;
}
m_renderer = renderer;
// Get references to singleton systems
m_audioSystem = &Audio::instance();
m_soundSystem = &SoundEffectManager::instance();
m_initialized = true;
logInfo("AssetManager initialized successfully");
return true;
}
void AssetManager::shutdown() {
if (!m_initialized) {
return;
}
logInfo("Shutting down AssetManager...");
// Clear loading tasks
clearLoadingTasks();
// Cleanup textures
for (auto& [id, texture] : m_textures) {
if (texture) {
SDL_DestroyTexture(texture);
logInfo("Destroyed texture: " + id);
}
}
m_textures.clear();
// Cleanup fonts (unique_ptr handles destruction)
m_fonts.clear();
// Reset state
m_renderer = nullptr;
m_audioSystem = nullptr;
m_soundSystem = nullptr;
m_initialized = false;
logInfo("AssetManager shutdown complete");
}
SDL_Texture* AssetManager::loadTexture(const std::string& id, const std::string& filepath) {
if (!validateRenderer()) {
return nullptr;
}
// Check if already loaded
auto it = m_textures.find(id);
if (it != m_textures.end()) {
logInfo("Texture already loaded: " + id);
return it->second;
}
// Load new texture
SDL_Texture* texture = loadTextureFromFile(filepath);
if (!texture) {
setError("Failed to load texture: " + filepath);
return nullptr;
}
m_textures[id] = texture;
logInfo("Loaded texture: " + id + " from " + filepath);
return texture;
}
SDL_Texture* AssetManager::getTexture(const std::string& id) const {
auto it = m_textures.find(id);
return (it != m_textures.end()) ? it->second : nullptr;
}
bool AssetManager::unloadTexture(const std::string& id) {
auto it = m_textures.find(id);
if (it == m_textures.end()) {
setError("Texture not found: " + id);
return false;
}
if (it->second) {
SDL_DestroyTexture(it->second);
}
m_textures.erase(it);
logInfo("Unloaded texture: " + id);
return true;
}
bool AssetManager::loadFont(const std::string& id, const std::string& filepath, int baseSize) {
// Check if already loaded
auto it = m_fonts.find(id);
if (it != m_fonts.end()) {
logInfo("Font already loaded: " + id);
return true;
}
// Create new font
auto font = std::make_unique<FontAtlas>();
if (!font->init(filepath, baseSize)) {
setError("Failed to initialize font: " + filepath);
return false;
}
m_fonts[id] = std::move(font);
logInfo("Loaded font: " + id + " from " + filepath + " (size: " + std::to_string(baseSize) + ")");
return true;
}
FontAtlas* AssetManager::getFont(const std::string& id) const {
auto it = m_fonts.find(id);
return (it != m_fonts.end()) ? it->second.get() : nullptr;
}
bool AssetManager::unloadFont(const std::string& id) {
auto it = m_fonts.find(id);
if (it == m_fonts.end()) {
setError("Font not found: " + id);
return false;
}
// Shutdown the font before removing
it->second->shutdown();
m_fonts.erase(it);
logInfo("Unloaded font: " + id);
return true;
}
bool AssetManager::loadMusicTrack(const std::string& filepath) {
if (!m_audioSystem) {
setError("Audio system not available");
return false;
}
if (!fileExists(filepath)) {
setError("Music file not found: " + filepath);
return false;
}
try {
m_audioSystem->addTrackAsync(filepath);
logInfo("Added music track for loading: " + filepath);
return true;
} catch (const std::exception& e) {
setError("Failed to add music track: " + std::string(e.what()));
return false;
}
}
bool AssetManager::loadSoundEffect(const std::string& id, const std::string& filepath) {
if (!m_soundSystem) {
setError("Sound effect system not available");
return false;
}
if (!fileExists(filepath)) {
setError("Sound effect file not found: " + filepath);
return false;
}
if (m_soundSystem->loadSound(id, filepath)) {
logInfo("Loaded sound effect: " + id + " from " + filepath);
return true;
} else {
setError("Failed to load sound effect: " + id + " from " + filepath);
return false;
}
}
bool AssetManager::loadSoundEffectWithFallback(const std::string& id, const std::string& baseName) {
if (!m_soundSystem) {
setError("Sound effect system not available");
return false;
}
// Try WAV first, then MP3 fallback (matching main.cpp pattern)
std::string wavPath = "assets/music/" + baseName + ".wav";
std::string mp3Path = "assets/music/" + baseName + ".mp3";
// Check WAV first
if (fileExists(wavPath)) {
if (m_soundSystem->loadSound(id, wavPath)) {
logInfo("Loaded sound effect: " + id + " from " + wavPath + " (WAV)");
return true;
}
}
// Fallback to MP3
if (fileExists(mp3Path)) {
if (m_soundSystem->loadSound(id, mp3Path)) {
logInfo("Loaded sound effect: " + id + " from " + mp3Path + " (MP3 fallback)");
return true;
}
}
setError("Failed to load sound effect: " + id + " (tried both WAV and MP3)");
return false;
}
void AssetManager::startBackgroundMusicLoading() {
if (!m_audioSystem) {
setError("Audio system not available");
return;
}
m_audioSystem->startBackgroundLoading();
logInfo("Started background music loading");
}
bool AssetManager::isMusicLoadingComplete() const {
if (!m_audioSystem) {
return false;
}
return m_audioSystem->isLoadingComplete();
}
int AssetManager::getLoadedMusicTrackCount() const {
if (!m_audioSystem) {
return 0;
}
return m_audioSystem->getLoadedTrackCount();
}
void AssetManager::addLoadingTask(const LoadingTask& task) {
m_loadingTasks.push_back(task);
logInfo("Added loading task: " + task.id + " (" + task.filepath + ")");
}
void AssetManager::executeLoadingTasks(std::function<void(float)> progressCallback) {
if (m_loadingTasks.empty()) {
m_loadingComplete = true;
if (progressCallback) progressCallback(1.0f);
return;
}
logInfo("Starting progressive loading of " + std::to_string(m_loadingTasks.size()) + " loading tasks...");
m_totalLoadingTasks = m_loadingTasks.size();
m_completedLoadingTasks = 0;
m_currentTaskIndex = 0;
m_loadingComplete = false;
m_isProgressiveLoading = true;
m_lastLoadTime = SDL_GetTicks();
m_musicLoadingStarted = false;
m_musicLoadingProgress = 0.0f;
// Don't execute tasks immediately - let update() handle them progressively
}
void AssetManager::update(float deltaTime) {
if (!m_isProgressiveLoading || m_loadingTasks.empty()) {
// Handle music loading progress simulation if assets are done
if (m_musicLoadingStarted && !m_loadingComplete) {
m_musicLoadingProgress += deltaTime * 0.4f; // Simulate music loading progress
if (m_musicLoadingProgress >= 1.0f) {
m_musicLoadingProgress = 1.0f;
m_loadingComplete = true;
logInfo("Background music loading simulation complete");
}
}
return;
}
Uint64 currentTime = SDL_GetTicks();
// Add minimum delay between loading items (600ms per item for visual effect)
if (currentTime - m_lastLoadTime < 600) {
return;
}
// Load one item at a time
if (m_currentTaskIndex < m_loadingTasks.size()) {
const auto& task = m_loadingTasks[m_currentTaskIndex];
bool success = false;
switch (task.type) {
case LoadingTask::TEXTURE:
success = (loadTexture(task.id, task.filepath) != nullptr);
break;
case LoadingTask::FONT:
success = loadFont(task.id, task.filepath, task.fontSize);
break;
case LoadingTask::MUSIC:
success = loadMusicTrack(task.filepath);
break;
case LoadingTask::SOUND_EFFECT:
success = loadSoundEffect(task.id, task.filepath);
break;
}
if (!success) {
logError("Failed to load asset: " + task.id + " (" + task.filepath + ")");
}
m_currentTaskIndex++;
m_completedLoadingTasks = m_currentTaskIndex;
m_lastLoadTime = currentTime;
logInfo("Asset loading progress: " + std::to_string((float)m_completedLoadingTasks / m_totalLoadingTasks * 100.0f) + "%");
// Check if all asset tasks are complete
if (m_currentTaskIndex >= m_loadingTasks.size()) {
m_isProgressiveLoading = false;
logInfo("Completed " + std::to_string(m_completedLoadingTasks) + "/" + std::to_string(m_totalLoadingTasks) + " loading tasks");
// Start background music loading simulation
m_musicLoadingStarted = true;
m_musicLoadingProgress = 0.0f;
startBackgroundMusicLoading();
}
}
}
void AssetManager::clearLoadingTasks() {
m_loadingTasks.clear();
logInfo("Cleared loading tasks");
}
bool AssetManager::isResourceLoaded(const std::string& id) const {
return (m_textures.find(id) != m_textures.end()) ||
(m_fonts.find(id) != m_fonts.end());
}
std::string AssetManager::getAssetPath(const std::string& relativePath) {
// Simple path construction - could be enhanced with proper path handling
if (relativePath.find("assets/") == 0) {
return relativePath; // Already has assets/ prefix
}
return "assets/" + relativePath;
}
bool AssetManager::fileExists(const std::string& filepath) {
// Use SDL file I/O for consistency with main.cpp pattern
SDL_IOStream* file = SDL_IOFromFile(filepath.c_str(), "rb");
if (file) {
SDL_CloseIO(file);
return true;
}
return false;
}
SDL_Texture* AssetManager::loadTextureFromFile(const std::string& filepath) {
if (!validateRenderer()) {
return nullptr;
}
const std::string resolvedPath = AssetPath::resolveImagePath(filepath);
SDL_Texture* texture = IMG_LoadTexture(m_renderer, resolvedPath.c_str());
if (!texture) {
std::string message = "Failed to load texture from: ";
message += filepath;
message += " (resolved: ";
message += resolvedPath;
message += ") - ";
message += SDL_GetError();
setError(message);
return nullptr;
}
if (resolvedPath != filepath) {
std::string message = "Loaded alternative image path for ";
message += filepath;
message += ": ";
message += resolvedPath;
logInfo(message);
}
return texture;
}
bool AssetManager::validateRenderer() const {
if (!m_initialized) {
const_cast<AssetManager*>(this)->setError("AssetManager not initialized");
return false;
}
if (!m_renderer) {
const_cast<AssetManager*>(this)->setError("Invalid renderer");
return false;
}
return true;
}
void AssetManager::setError(const std::string& error) {
m_lastError = error;
logError(error);
}
void AssetManager::logInfo(const std::string& message) const {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "[AssetManager] %s", message.c_str());
}
void AssetManager::logError(const std::string& message) const {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[AssetManager] %s", message.c_str());
}
// Loading progress tracking methods
bool AssetManager::isLoadingComplete() const {
// Loading is complete when both asset tasks and music loading are done
return m_loadingComplete && (!m_musicLoadingStarted || m_musicLoadingProgress >= 1.0f);
}
float AssetManager::getLoadingProgress() const {
if (m_totalLoadingTasks == 0) {
return 1.0f; // No tasks = complete
}
// Asset loading progress (80% of total progress)
float assetProgress = static_cast<float>(m_completedLoadingTasks) / static_cast<float>(m_totalLoadingTasks) * 0.8f;
// Music loading progress (20% of total progress)
float musicProgress = m_musicLoadingStarted ? m_musicLoadingProgress * 0.2f : 0.0f;
return assetProgress + musicProgress;
}
// IAssetLoader interface implementation
SDL_Texture* AssetManager::loadTextureFromPath(const std::string& path) {
// Use the path as both ID and filepath for the interface implementation
return loadTexture(path, path);
}
bool AssetManager::loadFontAsset(const std::string& name, const std::string& path, int size) {
// Delegate to the existing loadFont method
return loadFont(name, path, size);
}
bool AssetManager::loadAudioAsset(const std::string& name, const std::string& path) {
return loadSoundEffect(name, path);
}
SDL_Texture* AssetManager::getTextureAsset(const std::string& name) {
// Delegate to the existing getTexture method
return getTexture(name);
}
bool AssetManager::hasAsset(const std::string& name) const {
return m_textures.find(name) != m_textures.end() ||
m_fonts.find(name) != m_fonts.end();
}
void AssetManager::unloadAsset(const std::string& name) {
// Try to unload as texture first, then as font
if (!unloadTexture(name)) {
unloadFont(name);
}
}
void AssetManager::unloadAll() {
shutdown();
}

View File

@ -0,0 +1,139 @@
#pragma once
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <string>
#include <unordered_map>
#include <memory>
#include <functional>
#include "../interfaces/IAssetLoader.h"
#include "../interfaces/IAssetLoader.h"
// Forward declarations
class FontAtlas;
class Audio;
class SoundEffectManager;
/**
* AssetManager - Centralized resource management following SOLID principles
*
* Responsibilities:
* - Texture loading and management (BMP, PNG via SDL)
* - Font loading and caching (TTF via FontAtlas)
* - Audio resource coordination (MP3 via Audio, WAV via SoundEffectManager)
* - Resource lifecycle management (loading, caching, cleanup)
* - Error handling and fallback mechanisms
*
* Design Principles:
* - Single Responsibility: Only handles asset loading/management
* - Open/Closed: Easy to extend with new asset types
* - Dependency Inversion: Uses interfaces for audio systems
* - Interface Segregation: Separate methods for different asset types
*/
class AssetManager : public IAssetLoader {
public:
AssetManager();
~AssetManager();
// Lifecycle management
bool initialize(SDL_Renderer* renderer);
void shutdown();
// IAssetLoader interface implementation
SDL_Texture* loadTextureFromPath(const std::string& path) override;
bool loadFontAsset(const std::string& name, const std::string& path, int size) override;
bool loadAudioAsset(const std::string& name, const std::string& path) override;
SDL_Texture* getTextureAsset(const std::string& name) override;
bool hasAsset(const std::string& name) const override;
void unloadAsset(const std::string& name) override;
void unloadAll() override;
// Existing AssetManager methods with specific implementations
SDL_Texture* loadTexture(const std::string& id, const std::string& filepath);
SDL_Texture* getTexture(const std::string& id) const;
bool unloadTexture(const std::string& id);
void setDefaultTexturePath(const std::string& path) { m_defaultTexturePath = path; }
// Font management
bool loadFont(const std::string& id, const std::string& filepath, int baseSize = 24);
FontAtlas* getFont(const std::string& id) const;
bool unloadFont(const std::string& id);
void setDefaultFontPath(const std::string& path) { m_defaultFontPath = path; }
// Audio management (coordinates with existing Audio and SoundEffectManager)
bool loadMusicTrack(const std::string& filepath);
bool loadSoundEffect(const std::string& id, const std::string& filepath);
bool loadSoundEffectWithFallback(const std::string& id, const std::string& baseName);
void startBackgroundMusicLoading();
bool isMusicLoadingComplete() const;
int getLoadedMusicTrackCount() const;
// Batch loading operations
struct LoadingTask {
enum Type { TEXTURE, FONT, MUSIC, SOUND_EFFECT };
Type type;
std::string id;
std::string filepath;
int fontSize = 24; // For fonts only
};
void addLoadingTask(const LoadingTask& task);
void executeLoadingTasks(std::function<void(float)> progressCallback = nullptr);
void clearLoadingTasks();
void update(float deltaTime); // New: Progressive loading update
// Loading progress tracking
bool isLoadingComplete() const;
float getLoadingProgress() const;
size_t getTotalLoadingTasks() const { return m_totalLoadingTasks; }
size_t getCompletedLoadingTasks() const { return m_completedLoadingTasks; }
// Resource queries
size_t getTextureCount() const { return m_textures.size(); }
size_t getFontCount() const { return m_fonts.size(); }
bool isResourceLoaded(const std::string& id) const;
// Error handling
std::string getLastError() const { return m_lastError; }
void clearLastError() { m_lastError.clear(); }
// Asset path utilities
static std::string getAssetPath(const std::string& relativePath);
static bool fileExists(const std::string& filepath);
private:
// Resource storage
std::unordered_map<std::string, SDL_Texture*> m_textures;
std::unordered_map<std::string, std::unique_ptr<FontAtlas>> m_fonts;
std::vector<LoadingTask> m_loadingTasks;
// Loading progress tracking
size_t m_totalLoadingTasks = 0;
size_t m_completedLoadingTasks = 0;
bool m_loadingComplete = false;
// Progressive loading state
bool m_isProgressiveLoading = false;
size_t m_currentTaskIndex = 0;
Uint64 m_lastLoadTime = 0;
bool m_musicLoadingStarted = false;
float m_musicLoadingProgress = 0.0f;
// System references
SDL_Renderer* m_renderer;
Audio* m_audioSystem; // Pointer to singleton
SoundEffectManager* m_soundSystem; // Pointer to singleton
// Configuration
std::string m_defaultTexturePath;
std::string m_defaultFontPath;
std::string m_lastError;
bool m_initialized;
// Helper methods
SDL_Texture* loadTextureFromFile(const std::string& filepath);
bool validateRenderer() const;
void setError(const std::string& error);
void logInfo(const std::string& message) const;
void logError(const std::string& message) const;
};

View File

@ -0,0 +1,93 @@
#pragma once
#include <memory>
#include <unordered_map>
#include <typeindex>
#include <functional>
/**
* @brief Simple dependency injection container
*
* Provides service registration and resolution for dependency injection.
* Supports both singleton and factory-based service creation.
*/
class ServiceContainer {
public:
ServiceContainer() = default;
~ServiceContainer() = default;
/**
* @brief Register a singleton service instance
* @tparam TInterface Interface type
* @tparam TImplementation Implementation type
* @param instance Shared pointer to the service instance
*/
template<typename TInterface, typename TImplementation>
void registerSingleton(std::shared_ptr<TImplementation> instance) {
static_assert(std::is_base_of_v<TInterface, TImplementation>,
"TImplementation must inherit from TInterface");
m_singletons[std::type_index(typeid(TInterface))] = instance;
}
/**
* @brief Register a factory function for service creation
* @tparam TInterface Interface type
* @param factory Factory function that creates the service
*/
template<typename TInterface>
void registerFactory(std::function<std::shared_ptr<TInterface>()> factory) {
m_factories[std::type_index(typeid(TInterface))] = [factory]() -> std::shared_ptr<void> {
return std::static_pointer_cast<void>(factory());
};
}
/**
* @brief Resolve a service by its interface type
* @tparam TInterface Interface type to resolve
* @return Shared pointer to the service instance, nullptr if not found
*/
template<typename TInterface>
std::shared_ptr<TInterface> resolve() {
std::type_index typeIndex(typeid(TInterface));
// Check singletons first
auto singletonIt = m_singletons.find(typeIndex);
if (singletonIt != m_singletons.end()) {
return std::static_pointer_cast<TInterface>(singletonIt->second);
}
// Check factories
auto factoryIt = m_factories.find(typeIndex);
if (factoryIt != m_factories.end()) {
auto instance = factoryIt->second();
return std::static_pointer_cast<TInterface>(instance);
}
return nullptr;
}
/**
* @brief Check if a service is registered
* @tparam TInterface Interface type to check
* @return true if service is registered, false otherwise
*/
template<typename TInterface>
bool isRegistered() const {
std::type_index typeIndex(typeid(TInterface));
return m_singletons.find(typeIndex) != m_singletons.end() ||
m_factories.find(typeIndex) != m_factories.end();
}
/**
* @brief Clear all registered services
*/
void clear() {
m_singletons.clear();
m_factories.clear();
}
private:
std::unordered_map<std::type_index, std::shared_ptr<void>> m_singletons;
std::unordered_map<std::type_index, std::function<std::shared_ptr<void>()>> m_factories;
};

View File

@ -0,0 +1,361 @@
#include "InputManager.h"
#include <SDL3/SDL.h>
#include <algorithm>
InputManager::InputManager() {
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "InputManager initialized");
}
void InputManager::processEvents() {
// Update previous state before processing new events
updateInputState();
// Reset mouse delta
m_mouseDeltaX = 0.0f;
m_mouseDeltaY = 0.0f;
SDL_Event event;
while (SDL_PollEvent(&event)) {
// Trace every polled event type for debugging abrupt termination
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "InputManager: polled event type=%d\n", (int)event.type); fclose(f); }
}
switch (event.type) {
case SDL_EVENT_QUIT:
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
handleQuitEvent();
break;
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
handleKeyEvent(event.key);
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
handleMouseButtonEvent(event.button);
break;
case SDL_EVENT_MOUSE_MOTION:
handleMouseMotionEvent(event.motion);
break;
case SDL_EVENT_WINDOW_RESIZED:
case SDL_EVENT_WINDOW_MOVED:
case SDL_EVENT_WINDOW_MINIMIZED:
case SDL_EVENT_WINDOW_MAXIMIZED:
case SDL_EVENT_WINDOW_RESTORED:
handleWindowEvent(event.window);
break;
default:
// Unhandled event types
break;
}
}
}
void InputManager::update(float deltaTime) {
updateDAS(deltaTime);
}
bool InputManager::isKeyPressed(SDL_Scancode key) const {
auto current = m_currentKeyState.find(key);
auto previous = m_previousKeyState.find(key);
bool currentlyPressed = (current != m_currentKeyState.end() && current->second);
bool previouslyPressed = (previous != m_previousKeyState.end() && previous->second);
return currentlyPressed && !previouslyPressed;
}
bool InputManager::isKeyReleased(SDL_Scancode key) const {
auto current = m_currentKeyState.find(key);
auto previous = m_previousKeyState.find(key);
bool currentlyPressed = (current != m_currentKeyState.end() && current->second);
bool previouslyPressed = (previous != m_previousKeyState.end() && previous->second);
return !currentlyPressed && previouslyPressed;
}
bool InputManager::isKeyHeld(SDL_Scancode key) const {
auto it = m_currentKeyState.find(key);
return (it != m_currentKeyState.end() && it->second);
}
bool InputManager::isMouseButtonPressed(int button) const {
auto current = m_currentMouseState.find(button);
auto previous = m_previousMouseState.find(button);
bool currentlyPressed = (current != m_currentMouseState.end() && current->second);
bool previouslyPressed = (previous != m_previousMouseState.end() && previous->second);
return currentlyPressed && !previouslyPressed;
}
bool InputManager::isMouseButtonReleased(int button) const {
auto current = m_currentMouseState.find(button);
auto previous = m_previousMouseState.find(button);
bool currentlyPressed = (current != m_currentMouseState.end() && current->second);
bool previouslyPressed = (previous != m_previousMouseState.end() && previous->second);
return !currentlyPressed && previouslyPressed;
}
bool InputManager::isMouseButtonHeld(int button) const {
auto it = m_currentMouseState.find(button);
return (it != m_currentMouseState.end() && it->second);
}
void InputManager::getMousePosition(float& x, float& y) const {
x = m_mouseX;
y = m_mouseY;
}
void InputManager::getMouseDelta(float& deltaX, float& deltaY) const {
deltaX = m_mouseDeltaX;
deltaY = m_mouseDeltaY;
}
void InputManager::registerKeyHandler(KeyHandler handler) {
m_keyHandlers.push_back(std::move(handler));
}
void InputManager::registerMouseButtonHandler(MouseButtonHandler handler) {
m_mouseButtonHandlers.push_back(std::move(handler));
}
void InputManager::registerMouseMotionHandler(MouseMotionHandler handler) {
m_mouseMotionHandlers.push_back(std::move(handler));
}
void InputManager::registerWindowEventHandler(WindowEventHandler handler) {
m_windowEventHandlers.push_back(std::move(handler));
}
void InputManager::registerQuitHandler(QuitHandler handler) {
m_quitHandlers.push_back(std::move(handler));
}
void InputManager::configureDAS(float delayMs, float repeatMs) {
m_dasDelay = delayMs;
m_dasRepeat = repeatMs;
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "DAS configured: delay=%.1fms, repeat=%.1fms", delayMs, repeatMs);
}
bool InputManager::checkDASMovement(SDL_Scancode leftKey, SDL_Scancode rightKey, int& direction) {
bool leftHeld = isKeyHeld(leftKey);
bool rightHeld = isKeyHeld(rightKey);
// Determine current direction
int currentDirection = 0;
if (leftHeld && !rightHeld) {
currentDirection = -1;
} else if (rightHeld && !leftHeld) {
currentDirection = 1;
}
// If no direction or direction changed, reset DAS
if (currentDirection == 0 ||
(m_dasState.isActive &&
((currentDirection == -1 && m_dasState.activeKey != leftKey) ||
(currentDirection == 1 && m_dasState.activeKey != rightKey)))) {
resetDAS();
direction = 0;
return false;
}
// Check for initial key press (immediate movement)
if ((currentDirection == -1 && isKeyPressed(leftKey)) ||
(currentDirection == 1 && isKeyPressed(rightKey))) {
m_dasState.isActive = true;
m_dasState.delayTimer = m_dasDelay;
m_dasState.repeatTimer = 0.0f;
m_dasState.activeKey = (currentDirection == -1) ? leftKey : rightKey;
direction = currentDirection;
return true;
}
// If DAS is active and delay/repeat timers expired, trigger movement
if (m_dasState.isActive && m_dasState.delayTimer <= 0.0f && m_dasState.repeatTimer <= 0.0f) {
m_dasState.repeatTimer = m_dasRepeat;
direction = currentDirection;
return true;
}
direction = 0;
return false;
}
void InputManager::handleKeyEvent(const SDL_KeyboardEvent& event) {
SDL_Scancode scancode = event.scancode;
bool pressed = (event.type == SDL_EVENT_KEY_DOWN) && !event.repeat;
bool released = (event.type == SDL_EVENT_KEY_UP);
if (pressed) {
m_currentKeyState[scancode] = true;
} else if (released) {
m_currentKeyState[scancode] = false;
}
// Notify handlers
for (auto& handler : m_keyHandlers) {
try {
handler(scancode, pressed);
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_INPUT, "Exception in key handler: %s", e.what());
}
}
}
void InputManager::handleMouseButtonEvent(const SDL_MouseButtonEvent& event) {
int button = event.button;
bool pressed = (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN);
m_currentMouseState[button] = pressed;
// Notify handlers
for (auto& handler : m_mouseButtonHandlers) {
try {
handler(button, pressed, event.x, event.y);
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_INPUT, "Exception in mouse button handler: %s", e.what());
}
}
}
void InputManager::handleMouseMotionEvent(const SDL_MouseMotionEvent& event) {
float newX = event.x;
float newY = event.y;
m_mouseDeltaX = newX - m_mouseX;
m_mouseDeltaY = newY - m_mouseY;
m_mouseX = newX;
m_mouseY = newY;
// Notify handlers
for (auto& handler : m_mouseMotionHandlers) {
try {
handler(m_mouseX, m_mouseY, m_mouseDeltaX, m_mouseDeltaY);
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_INPUT, "Exception in mouse motion handler: %s", e.what());
}
}
}
void InputManager::handleWindowEvent(const SDL_WindowEvent& event) {
if (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
handleQuitEvent();
}
// Notify handlers
for (auto& handler : m_windowEventHandlers) {
try {
handler(event);
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_INPUT, "Exception in window event handler: %s", e.what());
}
}
}
void InputManager::updateInputState() {
// Copy current state to previous state
m_previousKeyState = m_currentKeyState;
m_previousMouseState = m_currentMouseState;
}
void InputManager::updateDAS(float deltaTime) {
if (!m_dasState.isActive) {
return;
}
float deltaMs = deltaTime * 1000.0f; // Convert to milliseconds
// Update delay timer
if (m_dasState.delayTimer > 0.0f) {
m_dasState.delayTimer -= deltaMs;
}
// Update repeat timer (only if delay has expired)
if (m_dasState.delayTimer <= 0.0f && m_dasState.repeatTimer > 0.0f) {
m_dasState.repeatTimer -= deltaMs;
}
}
void InputManager::resetDAS() {
m_dasState.isActive = false;
m_dasState.delayTimer = 0.0f;
m_dasState.repeatTimer = 0.0f;
m_dasState.activeKey = SDL_SCANCODE_UNKNOWN;
}
// IInputHandler interface implementation
bool InputManager::processEvent(const SDL_Event& event) {
// Process individual event and return if it was handled
switch (event.type) {
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
handleKeyEvent(event.key);
return true;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
handleMouseButtonEvent(event.button);
return true;
case SDL_EVENT_MOUSE_MOTION:
handleMouseMotionEvent(event.motion);
return true;
case SDL_EVENT_WINDOW_RESIZED:
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
case SDL_EVENT_WINDOW_MINIMIZED:
case SDL_EVENT_WINDOW_RESTORED:
handleWindowEvent(event.window);
return true;
case SDL_EVENT_QUIT:
handleQuitEvent();
return true;
default:
return false;
}
}
void InputManager::update(double deltaTime) {
update(static_cast<float>(deltaTime));
}
bool InputManager::isKeyCurrentlyPressed(SDL_Scancode scancode) const {
return isKeyHeld(scancode);
}
bool InputManager::isKeyJustPressed(SDL_Scancode scancode) const {
return isKeyPressed(scancode);
}
bool InputManager::isKeyJustReleased(SDL_Scancode scancode) const {
return isKeyReleased(scancode);
}
bool InputManager::isQuitRequested() const {
return shouldQuit();
}
void InputManager::reset() {
// Clear pressed/released states, keep held states
// In the current InputManager implementation, we use previous/current state
// so we just copy current to previous to reset the "just pressed/released" states
m_previousKeyState = m_currentKeyState;
m_previousMouseState = m_currentMouseState;
}
void InputManager::handleQuitEvent() {
FILE* f = fopen("tetris_trace.log", "a");
if (f) {
fprintf(f, "InputManager::handleQuitEvent invoked\n");
fclose(f);
}
m_shouldQuit = true;
for (auto& handler : m_quitHandlers) {
handler();
}
}

View File

@ -0,0 +1,115 @@
#pragma once
#include <SDL3/SDL.h>
#include <unordered_map>
#include <functional>
#include <vector>
#include "../interfaces/IInputHandler.h"
/**
* InputManager - Centralized input handling system
*
* Responsibilities:
* - Process SDL events and maintain input state
* - Provide clean interface for input queries
* - Handle keyboard and mouse input
* - Support event handler registration
* - Implement game-specific input logic (DAS/ARR)
*/
class InputManager : public IInputHandler {
public:
// Event handler types
using KeyHandler = std::function<void(SDL_Scancode key, bool pressed)>;
using MouseButtonHandler = std::function<void(int button, bool pressed, float x, float y)>;
using MouseMotionHandler = std::function<void(float x, float y, float deltaX, float deltaY)>;
using WindowEventHandler = std::function<void(const SDL_WindowEvent& event)>;
using QuitHandler = std::function<void()>;
InputManager();
~InputManager() = default;
// IInputHandler interface implementation
bool processEvent(const SDL_Event& event) override;
void update(double deltaTime) override;
bool isKeyCurrentlyPressed(SDL_Scancode scancode) const override;
bool isKeyJustPressed(SDL_Scancode scancode) const override;
bool isKeyJustReleased(SDL_Scancode scancode) const override;
bool isQuitRequested() const override;
void reset() override;
// Existing InputManager methods
void processEvents();
void update(float deltaTime);
// Keyboard state queries
bool isKeyPressed(SDL_Scancode key) const; // True only on the frame key was pressed
bool isKeyReleased(SDL_Scancode key) const; // True only on the frame key was released
bool isKeyHeld(SDL_Scancode key) const; // True while key is down
// Mouse state queries
bool isMouseButtonPressed(int button) const;
bool isMouseButtonReleased(int button) const;
bool isMouseButtonHeld(int button) const;
void getMousePosition(float& x, float& y) const;
void getMouseDelta(float& deltaX, float& deltaY) const;
// Event handler registration
void registerKeyHandler(KeyHandler handler);
void registerMouseButtonHandler(MouseButtonHandler handler);
void registerMouseMotionHandler(MouseMotionHandler handler);
void registerWindowEventHandler(WindowEventHandler handler);
void registerQuitHandler(QuitHandler handler);
// Game-specific input utilities (DAS/ARR system for Tetris)
struct DASState {
bool isActive = false;
float delayTimer = 0.0f;
float repeatTimer = 0.0f;
SDL_Scancode activeKey = SDL_SCANCODE_UNKNOWN;
};
void configureDAS(float delayMs, float repeatMs);
bool checkDASMovement(SDL_Scancode leftKey, SDL_Scancode rightKey, int& direction);
// Application control
bool shouldQuit() const { return m_shouldQuit; }
void requestQuit() { m_shouldQuit = true; }
private:
// Input state tracking
std::unordered_map<SDL_Scancode, bool> m_currentKeyState;
std::unordered_map<SDL_Scancode, bool> m_previousKeyState;
std::unordered_map<int, bool> m_currentMouseState;
std::unordered_map<int, bool> m_previousMouseState;
// Mouse position tracking
float m_mouseX = 0.0f;
float m_mouseY = 0.0f;
float m_mouseDeltaX = 0.0f;
float m_mouseDeltaY = 0.0f;
// Event handlers
std::vector<KeyHandler> m_keyHandlers;
std::vector<MouseButtonHandler> m_mouseButtonHandlers;
std::vector<MouseMotionHandler> m_mouseMotionHandlers;
std::vector<WindowEventHandler> m_windowEventHandlers;
std::vector<QuitHandler> m_quitHandlers;
// Application state
bool m_shouldQuit = false;
// DAS/ARR system for Tetris-style movement
DASState m_dasState;
float m_dasDelay = 170.0f; // Default DAS delay in ms
float m_dasRepeat = 40.0f; // Default ARR (Auto Repeat Rate) in ms
// Helper methods
void handleKeyEvent(const SDL_KeyboardEvent& event);
void handleMouseButtonEvent(const SDL_MouseButtonEvent& event);
void handleMouseMotionEvent(const SDL_MouseMotionEvent& event);
void handleWindowEvent(const SDL_WindowEvent& event);
void handleQuitEvent();
void updateInputState();
void updateDAS(float deltaTime);
void resetDAS();
};

View File

@ -0,0 +1,64 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
/**
* @brief Abstract interface for asset loading operations
*
* Provides a common interface for loading different types of assets,
* enabling dependency injection and easier testing.
*/
class IAssetLoader {
public:
virtual ~IAssetLoader() = default;
/**
* @brief Load a texture from file (interface method)
* @param path Path to the texture file
* @return Pointer to loaded SDL_Texture, nullptr on failure
*/
virtual SDL_Texture* loadTextureFromPath(const std::string& path) = 0;
/**
* @brief Load a font from file (interface method)
* @param name Font identifier/name
* @param path Path to the font file
* @param size Font size in pixels
* @return true if font was loaded successfully, false otherwise
*/
virtual bool loadFontAsset(const std::string& name, const std::string& path, int size) = 0;
/**
* @brief Load an audio file (interface method)
* @param name Audio identifier/name
* @param path Path to the audio file
* @return true if audio was loaded successfully, false otherwise
*/
virtual bool loadAudioAsset(const std::string& name, const std::string& path) = 0;
/**
* @brief Get a previously loaded texture (interface method)
* @param name Texture identifier/name
* @return Pointer to SDL_Texture, nullptr if not found
*/
virtual SDL_Texture* getTextureAsset(const std::string& name) = 0;
/**
* @brief Check if an asset exists
* @param name Asset identifier/name
* @return true if asset exists, false otherwise
*/
virtual bool hasAsset(const std::string& name) const = 0;
/**
* @brief Unload a specific asset
* @param name Asset identifier/name
*/
virtual void unloadAsset(const std::string& name) = 0;
/**
* @brief Unload all assets
*/
virtual void unloadAll() = 0;
};

View File

@ -0,0 +1,55 @@
#pragma once
#include <string>
/**
* @brief Abstract interface for audio system operations
*
* Provides a common interface for audio playback, enabling
* dependency injection and easier testing.
*/
class IAudioSystem {
public:
virtual ~IAudioSystem() = default;
/**
* @brief Play a sound effect
* @param name Sound effect name/identifier
*/
virtual void playSound(const std::string& name) = 0;
/**
* @brief Play background music
* @param name Music track name/identifier
*/
virtual void playMusic(const std::string& name) = 0;
/**
* @brief Stop currently playing music
*/
virtual void stopMusic() = 0;
/**
* @brief Set master volume for all audio
* @param volume Volume level (0.0 to 1.0)
*/
virtual void setMasterVolume(float volume) = 0;
/**
* @brief Set music volume
* @param volume Volume level (0.0 to 1.0)
*/
virtual void setMusicVolume(float volume) = 0;
/**
* @brief Set sound effects volume
* @param volume Volume level (0.0 to 1.0)
*/
virtual void setSoundVolume(float volume) = 0;
/**
* @brief Check if music is currently playing
* @return true if music is playing, false otherwise
*/
virtual bool isMusicPlaying() const = 0;
};

View File

@ -0,0 +1,73 @@
#pragma once
/**
* @brief Abstract interface for game rules
*
* Provides a common interface for different Tetris rule implementations,
* enabling different game modes and rule variations.
*/
class IGameRules {
public:
virtual ~IGameRules() = default;
/**
* @brief Calculate score for cleared lines
* @param linesCleared Number of lines cleared simultaneously
* @param level Current game level
* @return Score points to award
*/
virtual int calculateScore(int linesCleared, int level) const = 0;
/**
* @brief Get gravity speed for a given level
* @param level Game level
* @return Time in milliseconds for one gravity drop
*/
virtual double getGravitySpeed(int level) const = 0;
/**
* @brief Check if level should increase
* @param totalLines Total lines cleared so far
* @param currentLevel Current game level
* @return true if level should increase, false otherwise
*/
virtual bool shouldLevelUp(int totalLines, int currentLevel) const = 0;
/**
* @brief Calculate next level based on lines cleared
* @param totalLines Total lines cleared so far
* @param startLevel Starting level
* @return New level
*/
virtual int calculateLevel(int totalLines, int startLevel) const = 0;
/**
* @brief Get soft drop speed multiplier
* @return Multiplier for gravity when soft dropping
*/
virtual double getSoftDropMultiplier() const = 0;
/**
* @brief Get hard drop score per cell
* @return Points awarded per cell for hard drop
*/
virtual int getHardDropScore() const = 0;
/**
* @brief Get soft drop score per cell
* @return Points awarded per cell for soft drop
*/
virtual int getSoftDropScore() const = 0;
/**
* @brief Check if T-spins are enabled in this rule set
* @return true if T-spins are supported, false otherwise
*/
virtual bool supportsTSpins() const = 0;
/**
* @brief Check if hold feature is enabled in this rule set
* @return true if hold is supported, false otherwise
*/
virtual bool supportsHold() const = 0;
};

View File

@ -0,0 +1,59 @@
#pragma once
#include <SDL3/SDL.h>
/**
* @brief Abstract interface for input handling operations
*
* Provides a common interface for input processing,
* enabling dependency injection and easier testing.
*/
class IInputHandler {
public:
virtual ~IInputHandler() = default;
/**
* @brief Process SDL events
* @param event SDL event to process
* @return true if event was handled, false otherwise
*/
virtual bool processEvent(const SDL_Event& event) = 0;
/**
* @brief Update input state (called per frame)
* @param deltaTime Time elapsed since last frame in milliseconds
*/
virtual void update(double deltaTime) = 0;
/**
* @brief Check if a key is currently pressed (interface method)
* @param scancode SDL scancode of the key
* @return true if key is pressed, false otherwise
*/
virtual bool isKeyCurrentlyPressed(SDL_Scancode scancode) const = 0;
/**
* @brief Check if a key was just pressed this frame
* @param scancode SDL scancode of the key
* @return true if key was just pressed, false otherwise
*/
virtual bool isKeyJustPressed(SDL_Scancode scancode) const = 0;
/**
* @brief Check if a key was just released this frame
* @param scancode SDL scancode of the key
* @return true if key was just released, false otherwise
*/
virtual bool isKeyJustReleased(SDL_Scancode scancode) const = 0;
/**
* @brief Check if quit was requested (e.g., Alt+F4, window close)
* @return true if quit was requested, false otherwise
*/
virtual bool isQuitRequested() const = 0;
/**
* @brief Reset input state (typically called at frame start)
*/
virtual void reset() = 0;
};

View File

@ -0,0 +1,56 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
/**
* @brief Abstract interface for rendering operations
*
* Provides a common interface for different rendering implementations,
* enabling dependency injection and easier testing.
*/
class IRenderer {
public:
virtual ~IRenderer() = default;
/**
* @brief Clear the render target with specified color (interface method)
* @param r Red component (0-255)
* @param g Green component (0-255)
* @param b Blue component (0-255)
* @param a Alpha component (0-255)
*/
virtual void clearScreen(uint8_t r, uint8_t g, uint8_t b, uint8_t a) = 0;
/**
* @brief Present the rendered frame to the display
*/
virtual void present() = 0;
/**
* @brief Get the underlying SDL renderer for direct operations
* @return Pointer to SDL_Renderer
* @note This is provided for compatibility with existing code
*/
virtual SDL_Renderer* getSDLRenderer() = 0;
/**
* @brief Get the current window size (interface method)
* @param width Output parameter for window width
* @param height Output parameter for window height
*/
virtual void getWindowDimensions(int& width, int& height) const = 0;
/**
* @brief Set the render viewport
* @param viewport Viewport rectangle
*/
virtual void setViewport(const SDL_Rect* viewport) = 0;
/**
* @brief Set the render scale
* @param scaleX Horizontal scale factor
* @param scaleY Vertical scale factor
*/
virtual void setScale(float scaleX, float scaleY) = 0;
};

View File

@ -0,0 +1,225 @@
#include "StateManager.h"
#include "../../graphics/renderers/RenderManager.h"
#include <SDL3/SDL.h>
StateManager::StateManager(AppState initial)
: m_currentState(initial)
, m_previousState(initial)
{
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "StateManager initialized with state: %s", getStateName(initial));
}
void StateManager::registerEventHandler(AppState state, EventHandler handler) {
if (!isValidState(state)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid state for event handler registration");
return;
}
m_eventHandlers[stateToInt(state)].push_back(std::move(handler));
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Event handler registered for state: %s", getStateName(state));
}
void StateManager::registerUpdateHandler(AppState state, UpdateHandler handler) {
if (!isValidState(state)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid state for update handler registration");
return;
}
m_updateHandlers[stateToInt(state)].push_back(std::move(handler));
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Update handler registered for state: %s", getStateName(state));
}
void StateManager::registerRenderHandler(AppState state, RenderHandler handler) {
if (!isValidState(state)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid state for render handler registration");
return;
}
m_renderHandlers[stateToInt(state)].push_back(std::move(handler));
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Render handler registered for state: %s", getStateName(state));
}
void StateManager::registerOnEnter(AppState state, Hook hook) {
if (!isValidState(state)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid state for enter hook registration");
return;
}
m_onEnterHooks[stateToInt(state)].push_back(std::move(hook));
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Enter hook registered for state: %s", getStateName(state));
}
void StateManager::registerOnExit(AppState state, Hook hook) {
if (!isValidState(state)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid state for exit hook registration");
return;
}
m_onExitHooks[stateToInt(state)].push_back(std::move(hook));
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Exit hook registered for state: %s", getStateName(state));
}
// Legacy compatibility: overload accepting a no-arg function as handler
void StateManager::registerHandler(AppState s, std::function<void()> h) {
EventHandler wrapper = [h = std::move(h)](const SDL_Event&) { h(); };
registerEventHandler(s, std::move(wrapper));
}
bool StateManager::setState(AppState newState) {
if (!isValidState(newState)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Attempted to set invalid state");
return false;
}
if (newState == m_currentState) {
SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Attempted to set same state: %s", getStateName(newState));
return true;
}
if (!canTransitionTo(newState)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Invalid state transition from %s to %s",
getStateName(m_currentState), getStateName(newState));
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "State transition: %s -> %s",
getStateName(m_currentState), getStateName(newState));
// Persistent trace for debugging abrupt exits
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "setState start %s -> %s\n", getStateName(m_currentState), getStateName(newState)); fclose(f); }
}
// Execute exit hooks for current state
executeExitHooks(m_currentState);
// Update state
m_previousState = m_currentState;
m_currentState = newState;
// Execute enter hooks for new state
executeEnterHooks(m_currentState);
// Trace completion
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "setState end %s\n", getStateName(m_currentState)); fclose(f); }
}
return true;
}
void StateManager::handleEvent(const SDL_Event& event) {
auto it = m_eventHandlers.find(stateToInt(m_currentState));
if (it == m_eventHandlers.end()) {
return;
}
for (auto& handler : it->second) {
try {
handler(event);
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Exception in event handler for state %s: %s",
getStateName(m_currentState), e.what());
}
}
}
void StateManager::update(float deltaTime) {
auto it = m_updateHandlers.find(stateToInt(m_currentState));
if (it == m_updateHandlers.end()) {
return;
}
for (auto& handler : it->second) {
try {
handler(deltaTime);
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Exception in update handler for state %s: %s",
getStateName(m_currentState), e.what());
}
}
}
void StateManager::render(RenderManager& renderer) {
auto it = m_renderHandlers.find(stateToInt(m_currentState));
if (it == m_renderHandlers.end()) {
return;
}
for (auto& handler : it->second) {
try {
handler(renderer);
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Exception in render handler for state %s: %s",
getStateName(m_currentState), e.what());
}
}
}
bool StateManager::isValidState(AppState state) const {
// All enum values are currently valid
return static_cast<int>(state) >= static_cast<int>(AppState::Loading) &&
static_cast<int>(state) <= static_cast<int>(AppState::GameOver);
}
bool StateManager::canTransitionTo(AppState newState) const {
// For now, allow all transitions. Can be enhanced later with state machine validation
return isValidState(newState);
}
const char* StateManager::getStateName(AppState state) const {
switch (state) {
case AppState::Loading: return "Loading";
case AppState::Menu: return "Menu";
case AppState::Options: return "Options";
case AppState::LevelSelector: return "LevelSelector";
case AppState::Playing: return "Playing";
case AppState::LevelSelect: return "LevelSelect";
case AppState::GameOver: return "GameOver";
default: return "Unknown";
}
}
void StateManager::executeEnterHooks(AppState state) {
auto it = m_onEnterHooks.find(stateToInt(state));
if (it == m_onEnterHooks.end()) {
return;
}
int idx = 0;
for (auto& hook : it->second) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Executing enter hook %d for state %s", idx, getStateName(state));
// Also write to trace file for persistent record
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "executeEnterHook %d %s\n", idx, getStateName(state)); fclose(f); }
}
try {
hook();
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Exception in enter hook for state %s: %s",
getStateName(state), e.what());
}
++idx;
}
}
void StateManager::executeExitHooks(AppState state) {
auto it = m_onExitHooks.find(stateToInt(state));
if (it == m_onExitHooks.end()) {
return;
}
int idx = 0;
for (auto& hook : it->second) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Executing exit hook %d for state %s", idx, getStateName(state));
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "executeExitHook %d %s\n", idx, getStateName(state)); fclose(f); }
}
try {
hook();
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Exception in exit hook for state %s: %s",
getStateName(state), e.what());
}
++idx;
}
}

View File

@ -0,0 +1,87 @@
#pragma once
#include <functional>
#include <unordered_map>
#include <vector>
#include <string>
#include <SDL3/SDL.h>
// Forward declarations
class RenderManager;
// Application states used across the app
enum class AppState {
Loading,
Menu,
Options,
LevelSelector,
Playing,
LevelSelect,
GameOver
};
/**
* Enhanced StateManager - Manages application state transitions and lifecycle
*
* Improvements over original:
* - Better error handling and validation
* - Support for update and render cycles
* - State transition validation
* - Logging for debugging
* - Cleaner API design
*/
class StateManager {
public:
using EventHandler = std::function<void(const SDL_Event&)>;
using UpdateHandler = std::function<void(float deltaTime)>;
using RenderHandler = std::function<void(RenderManager& renderer)>;
using Hook = std::function<void()>;
StateManager(AppState initial);
~StateManager() = default;
// State registration
void registerEventHandler(AppState state, EventHandler handler);
void registerUpdateHandler(AppState state, UpdateHandler handler);
void registerRenderHandler(AppState state, RenderHandler handler);
void registerOnEnter(AppState state, Hook hook);
void registerOnExit(AppState state, Hook hook);
// Legacy compatibility
void registerHandler(AppState s, EventHandler h) { registerEventHandler(s, std::move(h)); }
void registerHandler(AppState s, std::function<void()> h); // overload used in some places
// State management
bool setState(AppState newState);
AppState getState() const { return m_currentState; }
AppState getPreviousState() const { return m_previousState; }
// State operations
void handleEvent(const SDL_Event& event);
void update(float deltaTime);
void render(RenderManager& renderer);
// Validation
bool isValidState(AppState state) const;
bool canTransitionTo(AppState newState) const;
// Utility
const char* getStateName(AppState state) const;
private:
// State data
AppState m_currentState;
AppState m_previousState;
// Handler storage
std::unordered_map<int, std::vector<EventHandler>> m_eventHandlers;
std::unordered_map<int, std::vector<UpdateHandler>> m_updateHandlers;
std::unordered_map<int, std::vector<RenderHandler>> m_renderHandlers;
std::unordered_map<int, std::vector<Hook>> m_onEnterHooks;
std::unordered_map<int, std::vector<Hook>> m_onExitHooks;
// Helper methods
void executeEnterHooks(AppState state);
void executeExitHooks(AppState state);
int stateToInt(AppState state) const { return static_cast<int>(state); }
};

View File

@ -2,6 +2,7 @@
#include "LineEffect.h"
#include <algorithm>
#include <cmath>
#include "audio/Audio.h"
#ifndef M_PI
#define M_PI 3.14159265358979323846
@ -80,15 +81,11 @@ bool LineEffect::init(SDL_Renderer* r) {
}
void LineEffect::shutdown() {
if (audioStream) {
SDL_DestroyAudioStream(audioStream);
audioStream = nullptr;
}
// No separate audio stream anymore; SFX go through shared Audio mixer
}
void LineEffect::initAudio() {
// For now, we'll generate simple beep sounds procedurally
// In a full implementation, you'd load WAV files
// Generate simple beep sounds procedurally (fallback when voice SFX not provided)
// Generate a simple line clear beep (440Hz for 0.2 seconds)
int sampleRate = 44100;
@ -265,33 +262,10 @@ void LineEffect::renderExplosion() {
}
void LineEffect::playLineClearSound(int lineCount) {
if (!audioStream) {
// Create audio stream for sound effects
SDL_AudioSpec spec = {};
spec.format = SDL_AUDIO_S16;
spec.channels = 2;
spec.freq = 44100;
audioStream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr);
if (!audioStream) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: Could not create audio stream for line clear effects");
return;
}
}
// Choose appropriate sound based on line count
const std::vector<int16_t>* sample = nullptr;
if (lineCount == 4) {
sample = &tetrisSample; // Special sound for Tetris
//printf("TETRIS! 4 lines cleared!\n");
} else {
sample = &lineClearSample; // Regular line clear sound
//printf("Line clear: %d lines\n", lineCount);
}
const std::vector<int16_t>* sample = (lineCount == 4) ? &tetrisSample : &lineClearSample;
if (sample && !sample->empty()) {
SDL_PutAudioStreamData(audioStream, sample->data(),
static_cast<int>(sample->size() * sizeof(int16_t)));
// Mix via shared Audio device so it layers with music
Audio::instance().playSfx(*sample, 2, 44100, (lineCount == 4) ? 0.9f : 0.7f);
}
}

View File

@ -1,7 +1,8 @@
// Game.cpp - Implementation of core Tetris game logic
#include "gameplay/Game.h"
#include "Game.h"
#include <algorithm>
#include <cmath>
#include <SDL3/SDL.h>
// Piece rotation bitmasks (row-major 4x4). Bit 0 = (0,0).
static const std::array<Shape, PIECE_COUNT> SHAPES = {{
@ -55,11 +56,53 @@ void Game::reset(int startLevel_) {
_score = 0; _lines = 0; _level = startLevel_; startLevel = startLevel_;
// Initialize gravity using NES timing table (ms per cell by level)
gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
fallAcc = 0; _elapsedSec = 0; gameOver=false; paused=false;
fallAcc = 0; gameOver=false; paused=false;
_startTime = SDL_GetPerformanceCounter();
_pausedTime = 0;
_lastPauseStart = 0;
hold = Piece{}; hold.type = PIECE_COUNT; canHold=true;
refillBag(); spawn();
}
double Game::elapsed() const {
if (!_startTime) return 0.0;
Uint64 currentTime = SDL_GetPerformanceCounter();
Uint64 totalPausedTime = _pausedTime;
// If currently paused, add time since pause started
if (paused && _lastPauseStart > 0) {
totalPausedTime += (currentTime - _lastPauseStart);
}
Uint64 activeTime = currentTime - _startTime - totalPausedTime;
double seconds = (double)activeTime / (double)SDL_GetPerformanceFrequency();
return seconds;
}
void Game::updateElapsedTime() {
// This method is now just for API compatibility
// Actual elapsed time is calculated on-demand in elapsed()
}
void Game::setPaused(bool p) {
if (p == paused) return; // No change
if (p) {
// Pausing - record when pause started
_lastPauseStart = SDL_GetPerformanceCounter();
} else {
// Unpausing - add elapsed pause time to total
if (_lastPauseStart > 0) {
Uint64 currentTime = SDL_GetPerformanceCounter();
_pausedTime += (currentTime - _lastPauseStart);
_lastPauseStart = 0;
}
}
paused = p;
}
void Game::refillBag() {
bag.clear();
for (int i=0;i<PIECE_COUNT;++i) bag.push_back(static_cast<PieceType>(i));
@ -152,17 +195,18 @@ void Game::lockPiece() {
// JS level progression (NES-like) using starting level rules
// Both startLevel and _level are 0-based now.
const int threshold = (startLevel + 1) * 10; // first promotion after this many lines
int oldLevel = _level;
// First level up happens when total lines equal threshold
// After that, every 10 lines (when (lines - threshold) % 10 == 0)
if (_lines == threshold) {
_level += 1;
} else if (_lines > threshold && ((_lines - threshold) % 10 == 0)) {
_level += 1;
int targetLevel = startLevel;
int firstThreshold = (startLevel + 1) * 10;
if (_lines >= firstThreshold) {
targetLevel = startLevel + 1 + (_lines - firstThreshold) / 10;
}
// If we haven't reached the first threshold yet, we are still at startLevel.
// The above logic handles this (targetLevel initialized to startLevel).
if (_level > oldLevel) {
if (targetLevel > _level) {
_level = targetLevel;
// Update gravity to exact NES speed for the new level
gravityMs = gravityMsForLevel(_level, gravityGlobalMultiplier);
if (levelUpCallback) levelUpCallback(_level);
@ -238,12 +282,16 @@ bool Game::tryMoveDown() {
void Game::tickGravity(double frameMs) {
if (paused) return; // Don't tick gravity when paused
// Soft drop: 20x faster for rapid continuous dropping
double effectiveGravityMs = softDropping ? (gravityMs / 5.0) : gravityMs;
fallAcc += frameMs;
while (fallAcc >= gravityMs) {
while (fallAcc >= effectiveGravityMs) {
// Attempt to move down by one row
if (tryMoveDown()) {
// Award soft drop points only if player is actively holding Down
// JS: POINTS.SOFT_DROP = 1 per cell for soft drop
if (softDropping) {
_score += 1;
}
@ -252,13 +300,14 @@ void Game::tickGravity(double frameMs) {
lockPiece();
if (gameOver) break;
}
fallAcc -= gravityMs;
fallAcc -= effectiveGravityMs;
}
}
void Game::softDropBoost(double frameMs) {
// Reduce soft drop speed multiplier from 10.0 to 3.0 to make it less aggressive
if (!paused) fallAcc += frameMs * 3.0;
// This method is now deprecated - soft drop is handled in tickGravity
// Kept for API compatibility but does nothing
(void)frameMs;
}
void Game::hardDrop() {
@ -285,15 +334,54 @@ void Game::rotate(int dir) {
return;
}
// JavaScript-style wall kicks: try horizontal, up, then larger horizontal offsets
const std::pair<int,int> kicks[] = {
{1, 0}, // right
{-1, 0}, // left
{0, -1}, // up (key difference from our previous approach)
{2, 0}, // 2 right (for I piece)
{-2, 0}, // 2 left (for I piece)
};
// Standard SRS Wall Kicks
// See: https://tetris.wiki/Super_Rotation_System#Wall_kicks
// JLSTZ Wall Kicks (0->R, R->2, 2->L, L->0)
// We only implement the clockwise (0->1, 1->2, 2->3, 3->0) and counter-clockwise (0->3, 3->2, 2->1, 1->0)
// For simplicity in this codebase, we'll use a unified set of tests that covers most cases
// or we can implement the full table.
// Let's use a robust set of kicks that covers most standard situations
std::vector<std::pair<int,int>> kicks;
if (p.type == I) {
// I-piece kicks
kicks = {
{0, 0}, // Basic rotation
{-2, 0}, {1, 0}, {-2, -1}, {1, 2}, // 0->1 (R)
{2, 0}, {-1, 0}, {2, 1}, {-1, -2}, // 1->0 (L)
{-1, 0}, {2, 0}, {-1, 2}, {2, -1}, // 1->2 (R)
{1, 0}, {-2, 0}, {1, -2}, {-2, 1}, // 2->1 (L)
{2, 0}, {-1, 0}, {2, 1}, {-1, -2}, // 2->3 (R)
{-2, 0}, {1, 0}, {-2, -1}, {1, 2}, // 3->2 (L)
{1, 0}, {-2, 0}, {1, -2}, {-2, 1}, // 3->0 (R)
{-1, 0}, {2, 0}, {-1, 2}, {2, -1} // 0->3 (L)
};
// The above is a superset; for a specific rotation state transition we should pick the right row.
// However, since we don't track "last rotation state" easily here (we just have p.rot),
// we'll try a generally permissive set of kicks that works for I-piece.
// A simplified "try everything" approach for I-piece:
kicks = {
{0, 0},
{-2, 0}, { 2, 0},
{-1, 0}, { 1, 0},
{ 0,-1}, { 0, 1}, // Up/Down
{-2,-1}, { 2,-1}, // Diagonal up
{ 1, 2}, {-1, 2}, // Diagonal down
{-2, 1}, { 2, 1}
};
} else {
// JLSTZ kicks
kicks = {
{0, 0},
{-1, 0}, { 1, 0}, // Left/Right
{ 0,-1}, // Up (floor kick)
{-1,-1}, { 1,-1}, // Diagonal up
{ 0, 1} // Down (rare but possible)
};
}
for (auto kick : kicks) {
Piece test = p;
test.x = cur.x + kick.first;

View File

@ -7,7 +7,8 @@
#include <cstdint>
#include <functional>
#include <memory>
#include "core/GravityManager.h"
#include <SDL3/SDL.h>
#include "../../core/GravityManager.h"
enum PieceType { I, O, T, S, Z, J, L, PIECE_COUNT };
using Shape = std::array<uint16_t, 4>; // four rotation bitmasks
@ -40,13 +41,14 @@ public:
bool canHoldPiece() const { return canHold; }
bool isGameOver() const { return gameOver; }
bool isPaused() const { return paused; }
void setPaused(bool p) { paused = p; }
void setPaused(bool p);
int score() const { return _score; }
int lines() const { return _lines; }
int level() const { return _level; }
int startLevelBase() const { return startLevel; }
double elapsed() const { return _elapsedSec; }
void addElapsed(double frameMs) { if (!paused) _elapsedSec += frameMs/1000.0; }
double elapsed() const; // Now calculated from start time
void updateElapsedTime(); // Update elapsed time from system clock
bool isSoftDropping() const { return softDropping; }
// Block statistics
const std::array<int, PIECE_COUNT>& getBlockCounts() const { return blockCounts; }
@ -69,6 +71,7 @@ public:
void setGravityGlobalMultiplier(double m) { gravityGlobalMultiplier = m; }
double getGravityGlobalMultiplier() const;
double getGravityMs() const;
double getFallAccumulator() const { return fallAcc; } // Debug: time accumulated toward next drop
void setLevelGravityMultiplier(int level, double m);
private:
@ -85,7 +88,9 @@ private:
int _level{1};
double gravityMs{800.0};
double fallAcc{0.0};
double _elapsedSec{0.0};
Uint64 _startTime{0}; // Performance counter at game start
Uint64 _pausedTime{0}; // Time spent paused (in performance counter ticks)
Uint64 _lastPauseStart{0}; // When the current pause started
bool gameOver{false};
int startLevel{0};
bool softDropping{false}; // true while player holds Down key
@ -98,7 +103,7 @@ private:
LevelUpCallback levelUpCallback;
// Gravity tuning -----------------------------------------------------
// Global multiplier applied to all level timings (use to slow/speed whole-game gravity)
double gravityGlobalMultiplier{2.8};
double gravityGlobalMultiplier{1.0};
// Gravity manager encapsulates frames table, multipliers and conversions
GravityManager gravityMgr;
// Backwards-compatible accessors (delegate to gravityMgr)

View File

@ -0,0 +1,280 @@
// LineEffect.cpp - Implementation of line clearing visual and audio effects
#include "LineEffect.h"
#include <algorithm>
#include <cmath>
#include "audio/Audio.h"
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
LineEffect::Particle::Particle(float px, float py)
: x(px), y(py), size(6.0f + static_cast<float>(rand()) / RAND_MAX * 12.0f), alpha(1.0f) {
// Random velocity for explosive effect
float angle = static_cast<float>(rand()) / RAND_MAX * 2.0f * M_PI;
float speed = 80.0f + static_cast<float>(rand()) / RAND_MAX * 150.0f;
vx = std::cos(angle) * speed;
vy = std::sin(angle) * speed - 30.0f;
// Random block type for texture
blockType = rand() % 7;
// Fallback colors if texture not available
switch (blockType % 4) {
case 0: color = {255, 140, 30, 255}; break;
case 1: color = {255, 255, 100, 255}; break;
case 2: color = {255, 255, 255, 255}; break;
case 3: color = {255, 100, 100, 255}; break;
}
}
void LineEffect::Particle::update() {
x += vx * 0.016f;
y += vy * 0.016f;
vy += 250.0f * 0.016f;
vx *= 0.98f;
alpha -= 0.025f; // Slower fade for blocks (longer visibility)
if (alpha < 0.0f) alpha = 0.0f;
if (size > 2.0f) size -= 0.05f;
}
void LineEffect::Particle::render(SDL_Renderer* renderer, SDL_Texture* blocksTex) {
if (alpha <= 0.0f) return;
if (blocksTex) {
// Render textured block fragment
Uint8 prevA = 255;
SDL_GetTextureAlphaMod(blocksTex, &prevA);
SDL_SetTextureAlphaMod(blocksTex, static_cast<Uint8>(alpha * 255.0f));
const int SPRITE_SIZE = 90;
float srcX = blockType * SPRITE_SIZE + 2;
float srcY = 2;
float srcW = SPRITE_SIZE - 4;
float srcH = SPRITE_SIZE - 4;
SDL_FRect srcRect = {srcX, srcY, srcW, srcH};
SDL_FRect dstRect = {x - size/2, y - size/2, size, size};
SDL_RenderTexture(renderer, blocksTex, &srcRect, &dstRect);
SDL_SetTextureAlphaMod(blocksTex, prevA);
} else {
// Fallback to circle rendering
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
Uint8 adjustedAlpha = static_cast<Uint8>(alpha * 255.0f);
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, adjustedAlpha);
for (int i = 0; i < static_cast<int>(size); ++i) {
for (int j = 0; j < static_cast<int>(size); ++j) {
float dx = i - size/2.0f;
float dy = j - size/2.0f;
if (dx*dx + dy*dy <= (size/2.0f)*(size/2.0f)) {
SDL_RenderPoint(renderer, x + dx, y + dy);
}
}
}
}
}
LineEffect::LineEffect() : renderer(nullptr), state(AnimationState::IDLE), timer(0.0f),
rng(std::random_device{}()), audioStream(nullptr) {
}
LineEffect::~LineEffect() {
shutdown();
}
bool LineEffect::init(SDL_Renderer* r) {
renderer = r;
initAudio();
return true;
}
void LineEffect::shutdown() {
// No separate audio stream anymore; SFX go through shared Audio mixer
}
void LineEffect::initAudio() {
// Generate simple beep sounds procedurally (fallback when voice SFX not provided)
// Generate a simple line clear beep (440Hz for 0.2 seconds)
int sampleRate = 44100;
int duration = static_cast<int>(0.2f * sampleRate);
lineClearSample.resize(duration * 2); // Stereo
for (int i = 0; i < duration; ++i) {
float t = static_cast<float>(i) / sampleRate;
float wave = std::sin(2.0f * M_PI * 440.0f * t) * 0.3f; // 440Hz sine wave
int16_t sample = static_cast<int16_t>(wave * 32767.0f);
lineClearSample[i * 2] = sample; // Left channel
lineClearSample[i * 2 + 1] = sample; // Right channel
}
// Generate a higher pitched tetris sound (880Hz for 0.4 seconds)
duration = static_cast<int>(0.4f * sampleRate);
tetrisSample.resize(duration * 2);
for (int i = 0; i < duration; ++i) {
float t = static_cast<float>(i) / sampleRate;
float wave = std::sin(2.0f * M_PI * 880.0f * t) * 0.4f; // 880Hz sine wave
int16_t sample = static_cast<int16_t>(wave * 32767.0f);
tetrisSample[i * 2] = sample; // Left channel
tetrisSample[i * 2 + 1] = sample; // Right channel
}
}
void LineEffect::startLineClear(const std::vector<int>& rows, int gridX, int gridY, int blockSize) {
if (rows.empty()) return;
clearingRows = rows;
state = AnimationState::FLASH_WHITE;
timer = 0.0f;
particles.clear();
// Create particles for each clearing row
for (int row : rows) {
createParticles(row, gridX, gridY, blockSize);
}
// Play appropriate sound
playLineClearSound(static_cast<int>(rows.size()));
}
void LineEffect::createParticles(int row, int gridX, int gridY, int blockSize) {
// Create particles spread across the row with explosive pattern
int particlesPerRow = 60; // More particles for dramatic explosion effect
for (int i = 0; i < particlesPerRow; ++i) {
// Create particles along the entire row width
float x = gridX + (static_cast<float>(i) / (particlesPerRow - 1)) * (10 * blockSize);
float y = gridY + row * blockSize + blockSize / 2.0f;
// Add some randomness to position
x += (static_cast<float>(rand()) / RAND_MAX - 0.5f) * blockSize * 0.8f;
y += (static_cast<float>(rand()) / RAND_MAX - 0.5f) * blockSize * 0.6f;
particles.emplace_back(x, y);
}
}
bool LineEffect::update(float deltaTime) {
if (state == AnimationState::IDLE) return true;
timer += deltaTime;
switch (state) {
case AnimationState::FLASH_WHITE:
if (timer >= FLASH_DURATION) {
state = AnimationState::EXPLODE_BLOCKS;
timer = 0.0f;
}
break;
case AnimationState::EXPLODE_BLOCKS:
updateParticles();
if (timer >= EXPLODE_DURATION) {
state = AnimationState::BLOCKS_DROP;
timer = 0.0f;
}
break;
case AnimationState::BLOCKS_DROP:
updateParticles();
if (timer >= DROP_DURATION) {
state = AnimationState::IDLE;
clearingRows.clear();
particles.clear();
return true; // Effect complete
}
break;
case AnimationState::IDLE:
return true;
}
return false; // Effect still running
}
void LineEffect::updateParticles() {
// Update all particles
for (auto& particle : particles) {
particle.update();
}
// Remove dead particles
particles.erase(
std::remove_if(particles.begin(), particles.end(),
[](const Particle& p) { return !p.isAlive(); }),
particles.end()
);
}
void LineEffect::render(SDL_Renderer* renderer, SDL_Texture* blocksTex, int gridX, int gridY, int blockSize) {
if (state == AnimationState::IDLE) return;
switch (state) {
case AnimationState::FLASH_WHITE:
renderFlash(gridX, gridY, blockSize);
break;
case AnimationState::EXPLODE_BLOCKS:
renderExplosion(blocksTex);
break;
case AnimationState::BLOCKS_DROP:
renderExplosion(blocksTex);
break;
case AnimationState::IDLE:
break;
}
}
void LineEffect::renderFlash(int gridX, int gridY, int blockSize) {
// Create a flashing white effect with varying opacity
float progress = timer / FLASH_DURATION;
float flashIntensity = std::sin(progress * M_PI * 6.0f) * 0.5f + 0.5f;
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
Uint8 alpha = static_cast<Uint8>(flashIntensity * 180.0f);
for (int row : clearingRows) {
SDL_SetRenderDrawColor(renderer, 255, 255, 255, alpha);
SDL_FRect flashRect = {
static_cast<float>(gridX - 4),
static_cast<float>(gridY + row * blockSize - 4),
static_cast<float>(10 * blockSize + 8),
static_cast<float>(blockSize + 8)
};
SDL_RenderFillRect(renderer, &flashRect);
SDL_SetRenderDrawColor(renderer, 100, 150, 255, alpha / 2);
for (int i = 1; i <= 3; ++i) {
SDL_FRect glowRect = {
flashRect.x - i,
flashRect.y - i,
flashRect.w + 2*i,
flashRect.h + 2*i
};
SDL_RenderRect(renderer, &glowRect);
}
}
}
void LineEffect::renderExplosion(SDL_Texture* blocksTex) {
for (auto& particle : particles) {
particle.render(renderer, blocksTex);
}
}
void LineEffect::playLineClearSound(int lineCount) {
// Choose appropriate sound based on line count
const std::vector<int16_t>* sample = (lineCount == 4) ? &tetrisSample : &lineClearSample;
if (sample && !sample->empty()) {
// Mix via shared Audio device so it layers with music
Audio::instance().playSfx(*sample, 2, 44100, (lineCount == 4) ? 0.9f : 0.7f);
}
}

View File

@ -0,0 +1,72 @@
// LineEffect.h - Line clearing visual and audio effects
#pragma once
#include <SDL3/SDL.h>
#include <vector>
#include <random>
class LineEffect {
public:
struct Particle {
float x, y;
float vx, vy;
float size;
float alpha;
int blockType; // Added for textured particles
SDL_Color color;
Particle(float px, float py);
void update();
void render(SDL_Renderer* renderer, SDL_Texture* blocksTex);
bool isAlive() const { return alpha > 0.0f; }
};
enum class AnimationState {
IDLE,
FLASH_WHITE,
EXPLODE_BLOCKS,
BLOCKS_DROP
};
LineEffect();
~LineEffect();
bool init(SDL_Renderer* renderer);
void shutdown();
// Start line clear effect for the specified rows
void startLineClear(const std::vector<int>& rows, int gridX, int gridY, int blockSize);
// Update and render the effect
bool update(float deltaTime); // Returns true if effect is complete
void render(SDL_Renderer* renderer, SDL_Texture* blocksTex, int gridX, int gridY, int blockSize);
// Audio
void playLineClearSound(int lineCount);
bool isActive() const { return state != AnimationState::IDLE; }
private:
SDL_Renderer* renderer{nullptr};
AnimationState state{AnimationState::IDLE};
float timer{0.0f};
std::vector<int> clearingRows;
std::vector<Particle> particles;
std::mt19937 rng{std::random_device{}()};
// Audio resources
SDL_AudioStream* audioStream{nullptr};
std::vector<int16_t> lineClearSample;
std::vector<int16_t> tetrisSample;
// Animation timing - Flash then immediate explosion effect
static constexpr float FLASH_DURATION = 0.12f; // Very brief white flash
static constexpr float EXPLODE_DURATION = 0.6f; // Longer explosive effect
static constexpr float DROP_DURATION = 0.05f; // Almost instant block drop
void createParticles(int row, int gridX, int gridY, int blockSize);
void updateParticles();
void renderFlash(int gridX, int gridY, int blockSize);
void renderExplosion(SDL_Texture* blocksTex);
bool loadAudioSample(const std::string& path, std::vector<int16_t>& sample);
void initAudio();
};

View File

@ -0,0 +1,544 @@
#include "GameRenderer.h"
#include "../gameplay/Game.h"
#include "../graphics/Font.h"
#include "../gameplay/LineEffect.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdio>
// Color constants (copied from main.cpp)
static const SDL_Color COLORS[] = {
{0, 0, 0, 255}, // 0: BLACK (empty)
{0, 255, 255, 255}, // 1: I-piece - cyan
{255, 255, 0, 255}, // 2: O-piece - yellow
{128, 0, 128, 255}, // 3: T-piece - purple
{0, 255, 0, 255}, // 4: S-piece - green
{255, 0, 0, 255}, // 5: Z-piece - red
{0, 0, 255, 255}, // 6: J-piece - blue
{255, 165, 0, 255} // 7: L-piece - orange
};
void GameRenderer::drawRect(SDL_Renderer* renderer, float x, float y, float w, float h, SDL_Color c) {
SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a);
SDL_FRect fr{x, y, w, h};
SDL_RenderFillRect(renderer, &fr);
}
void GameRenderer::drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType) {
if (!blocksTex || blockType < 0 || blockType >= PIECE_COUNT) {
// Fallback to colored rectangle if texture isn't available
SDL_Color color = (blockType >= 0 && blockType < PIECE_COUNT) ? COLORS[blockType + 1] : SDL_Color{128, 128, 128, 255};
drawRect(renderer, x, y, size-1, size-1, color);
return;
}
// JavaScript uses: sx = type * spriteSize, sy = 0, with 2px padding
// Each sprite is 90px wide in the horizontal sprite sheet
const int SPRITE_SIZE = 90;
float srcX = blockType * SPRITE_SIZE + 2; // Add 2px padding like JS
float srcY = 2; // Add 2px padding from top like JS
float srcW = SPRITE_SIZE - 4; // Subtract 4px total padding like JS
float srcH = SPRITE_SIZE - 4; // Subtract 4px total padding like JS
SDL_FRect srcRect = {srcX, srcY, srcW, srcH};
SDL_FRect dstRect = {x, y, size, size};
SDL_RenderTexture(renderer, blocksTex, &srcRect, &dstRect);
}
void GameRenderer::drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost) {
if (piece.type >= PIECE_COUNT) return;
for (int cy = 0; cy < 4; ++cy) {
for (int cx = 0; cx < 4; ++cx) {
if (Game::cellFilled(piece, cx, cy)) {
float px = ox + (piece.x + cx) * tileSize;
float py = oy + (piece.y + cy) * tileSize;
if (isGhost) {
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
// Draw ghost piece as barely visible gray outline
SDL_SetRenderDrawColor(renderer, 180, 180, 180, 20); // Very faint gray
SDL_FRect rect = {px + 2, py + 2, tileSize - 4, tileSize - 4};
SDL_RenderFillRect(renderer, &rect);
// Draw thin gray border
SDL_SetRenderDrawColor(renderer, 180, 180, 180, 30);
SDL_FRect border = {px + 1, py + 1, tileSize - 2, tileSize - 2};
SDL_RenderRect(renderer, &border);
} else {
drawBlockTexture(renderer, blocksTex, px, py, tileSize, piece.type);
}
}
}
}
}
void GameRenderer::drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize) {
if (pieceType >= PIECE_COUNT) return;
// Use the first rotation (index 0) for preview
Game::Piece previewPiece;
previewPiece.type = pieceType;
previewPiece.rot = 0;
previewPiece.x = 0;
previewPiece.y = 0;
// Center the piece in the preview area
float offsetX = 0, offsetY = 0;
if (pieceType == 0) { offsetX = tileSize * 0.5f; } // I-piece centering (assuming I = 0)
else if (pieceType == 1) { offsetX = tileSize * 0.5f; } // O-piece centering (assuming O = 1)
// Use semi-transparent alpha for preview blocks
Uint8 previewAlpha = 180;
if (blocksTex) {
SDL_SetTextureAlphaMod(blocksTex, previewAlpha);
}
for (int cy = 0; cy < 4; ++cy) {
for (int cx = 0; cx < 4; ++cx) {
if (Game::cellFilled(previewPiece, cx, cy)) {
float px = x + offsetX + cx * tileSize;
float py = y + offsetY + cy * tileSize;
drawBlockTexture(renderer, blocksTex, px, py, tileSize, pieceType);
}
}
}
// Reset alpha
if (blocksTex) {
SDL_SetTextureAlphaMod(blocksTex, 255);
}
}
void GameRenderer::renderPlayingState(
SDL_Renderer* renderer,
Game* game,
FontAtlas* pixelFont,
LineEffect* lineEffect,
SDL_Texture* blocksTex,
float logicalW,
float logicalH,
float logicalScale,
float winW,
float winH,
bool showExitConfirmPopup,
int exitPopupSelectedButton,
bool suppressPauseVisuals
) {
(void)exitPopupSelectedButton;
if (!game || !pixelFont) return;
// Calculate actual content area (centered within the window)
float contentScale = logicalScale;
float contentW = logicalW * contentScale;
float contentH = logicalH * contentScale;
float contentOffsetX = (winW - contentW) * 0.5f / contentScale;
float contentOffsetY = (winH - contentH) * 0.5f / contentScale;
// Helper lambda for drawing rectangles with content offset
auto drawRectWithOffset = [&](float x, float y, float w, float h, SDL_Color c) {
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
const float MIN_MARGIN = 40.0f;
const float TOP_MARGIN = 60.0f;
const float PANEL_WIDTH = 180.0f;
const float PANEL_SPACING = 30.0f;
const float NEXT_PIECE_HEIGHT = 120.0f;
const float BOTTOM_MARGIN = 60.0f;
float yCursor = statsY + 44.0f;
const float availableWidth = logicalW - (MIN_MARGIN * 2) - (PANEL_WIDTH * 2) - (PANEL_SPACING * 2);
const float availableHeight = logicalH - TOP_MARGIN - BOTTOM_MARGIN - NEXT_PIECE_HEIGHT;
const float maxBlockSizeW = availableWidth / Game::COLS;
const float maxBlockSizeH = availableHeight / Game::ROWS;
float previewY = rowTop - 4.0f;
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 positions
const float totalContentHeight = NEXT_PIECE_HEIGHT + GRID_H;
const float availableVerticalSpace = logicalH - TOP_MARGIN - BOTTOM_MARGIN;
const float verticalCenterOffset = (availableVerticalSpace - totalContentHeight) * 0.5f;
const float contentStartY = TOP_MARGIN + verticalCenterOffset;
const float totalLayoutWidth = PANEL_WIDTH + PANEL_SPACING + GRID_W + PANEL_SPACING + PANEL_WIDTH;
const float layoutStartX = (logicalW - totalLayoutWidth) * 0.5f;
float barY = previewY + previewSize + 10.0f;
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;
float rowBottom = percY + 14.0f;
SDL_FRect rowBg{
previewX - 10.0f,
rowTop - 8.0f,
rowWidth + 20.0f,
rowBottom - rowTop
};
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
if (game->hasCompletedLines() && lineEffect && !lineEffect->isActive()) {
auto completedLines = game->getCompletedLines();
lineEffect->startLineClear(completedLines, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
}
// Draw game grid border
drawRectWithOffset(gridX - 3 - contentOffsetX, gridY - 3 - contentOffsetY, GRID_W + 6, GRID_H + 6, {100, 120, 200, 255});
drawRectWithOffset(gridX - 1 - contentOffsetX, gridY - 1 - contentOffsetY, GRID_W + 2, GRID_H + 2, {60, 80, 160, 255});
drawRectWithOffset(gridX - contentOffsetX, gridY - contentOffsetY, GRID_W, GRID_H, {20, 25, 35, 255});
// Draw panel backgrounds
SDL_SetRenderDrawColor(renderer, 10, 15, 25, 160);
SDL_FRect lbg{statsX - 16, gridY - 10, statsW + 32, GRID_H + 20};
SDL_RenderFillRect(renderer, &lbg);
SDL_FRect rbg{scoreX - 16, gridY - 16, statsW + 32, GRID_H + 32};
SDL_RenderFillRect(renderer, &rbg);
// Draw grid lines
SDL_SetRenderDrawColor(renderer, 40, 45, 60, 255);
for (int x = 1; x < Game::COLS; ++x) {
float lineX = gridX + x * finalBlockSize;
SDL_RenderLine(renderer, lineX, gridY, lineX, gridY + GRID_H);
}
for (int y = 1; y < Game::ROWS; ++y) {
float lineY = gridY + y * finalBlockSize;
SDL_RenderLine(renderer, gridX, lineY, gridX + GRID_W, lineY);
}
// Draw block statistics panel border
drawRectWithOffset(statsX - 3 - contentOffsetX, statsY - 3 - contentOffsetY, statsW + 6, statsH + 6, {100, 120, 200, 255});
drawRectWithOffset(statsX - contentOffsetX, statsY - contentOffsetY, statsW, statsH, {30, 35, 50, 255});
// Draw next piece preview panel border
drawRectWithOffset(nextX - 3 - contentOffsetX, nextY - 3 - contentOffsetY, nextW + 6, nextH + 6, {100, 120, 200, 255});
drawRectWithOffset(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);
}
}
}
bool allowActivePieceRender = !game->isPaused() || suppressPauseVisuals;
// Draw ghost piece (where current piece will land)
if (allowActivePieceRender) {
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 (allowActivePieceRender) {
drawPiece(renderer, blocksTex, game->current(), gridX, gridY, finalBlockSize, false);
}
// Draw line clearing effects
if (lineEffect && lineEffect->isActive()) {
lineEffect->render(renderer, static_cast<int>(gridX), static_cast<int>(gridY), static_cast<int>(finalBlockSize));
}
float yCursor = statsY + 44.0f;
// 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, static_cast<PieceType>(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();
float previewY = rowTop - 4.0f;
for (int i = 0; i < PIECE_COUNT; ++i) totalBlocks += blockCounts[i];
const float rowPadding = 18.0f;
const float rowWidth = statsW - rowPadding * 2.0f;
const float rowSpacing = 12.0f;
float yCursor = statsY + 44.0f;
for (int i = 0; i < PIECE_COUNT; ++i) {
float rowTop = yCursor;
float previewSize = finalBlockSize * 0.52f;
float previewX = statsX + rowPadding;
float previewY = rowTop - 14.0f;
// Determine actual piece height so bars never overlap blocks
Game::Piece previewPiece{};
previewPiece.type = static_cast<PieceType>(i);
previewPiece.rot = 0;
previewPiece.x = 0;
previewPiece.y = 0;
int maxCy = -1;
for (int cy = 0; cy < 4; ++cy) {
for (int cx = 0; cx < 4; ++cx) {
if (Game::cellFilled(previewPiece, cx, cy)) {
maxCy = std::max(maxCy, cy);
}
}
}
float pieceHeight = (maxCy >= 0 ? maxCy + 1.0f : 1.0f) * previewSize;
int count = blockCounts[i];
char countStr[16];
snprintf(countStr, sizeof(countStr), "%d", count);
int countW = 0, countH = 0;
pixelFont->measure(countStr, 1.0f, countW, countH);
float countX = previewX + rowWidth - static_cast<float>(countW);
float countY = previewY + 9.0f;
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 = previewX;
float barY = previewY + pieceHeight + 12.0f;
float barH = 6.0f;
float barW = rowWidth;
float percY = barY + barH + 8.0f;
float fillW = barW * (perc / 100.0f);
fillW = std::clamp(fillW, 0.0f, barW);
float cardTop = rowTop - 14.0f;
float rowBottom = percY + 16.0f;
SDL_FRect rowBg{
previewX - 12.0f,
cardTop,
rowWidth + 24.0f,
rowBottom - cardTop
};
SDL_SetRenderDrawColor(renderer, 18, 26, 40, 200);
SDL_RenderFillRect(renderer, &rowBg);
SDL_SetRenderDrawColor(renderer, 70, 100, 150, 210);
SDL_RenderRect(renderer, &rowBg);
drawSmallPiece(renderer, blocksTex, static_cast<PieceType>(i), previewX, previewY, previewSize);
pixelFont->draw(renderer, countX, countY, countStr, 1.0f, {245, 245, 255, 255});
pixelFont->draw(renderer, previewX, percY, percStr, 0.8f, {215, 225, 240, 255});
SDL_SetRenderDrawColor(renderer, 110, 120, 140, 200);
SDL_FRect track{barX, barY, barW, barH};
SDL_RenderFillRect(renderer, &track);
SDL_Color pc = COLORS[i + 1];
SDL_SetRenderDrawColor(renderer, pc.r, pc.g, pc.b, 255);
SDL_FRect fill{barX, barY, fillW, barH};
SDL_RenderFillRect(renderer, &fill);
yCursor = rowBottom + rowSpacing;
}
// Draw score panel (right side)
const float contentTopOffset = 0.0f;
const float contentBottomOffset = 290.0f;
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
int startLv = game->startLevelBase();
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 display
pixelFont->draw(renderer, scoreX, baseY + 265, "TIME", 1.0f, {255, 220, 0, 255});
int totalSecs = static_cast<int>(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
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, logicalW - 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, static_cast<PieceType>(game->held().type), statsX + 60, statsY + statsH - 80, finalBlockSize * 0.6f);
}
// Pause overlay (suppressed when requested, e.g., countdown)
if (!suppressPauseVisuals && game->isPaused() && !showExitConfirmPopup) {
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
for (int i = -4; i <= 4; ++i) {
float spread = static_cast<float>(std::abs(i));
Uint8 alpha = Uint8(std::max(8.f, 32.f - spread * 4.f));
SDL_SetRenderDrawColor(renderer, 24, 32, 48, alpha);
SDL_FRect blurRect{
gridX - spread * 2.0f,
gridY - spread * 1.5f,
GRID_W + spread * 4.0f,
GRID_H + spread * 3.0f
};
SDL_RenderFillRect(renderer, &blurRect);
}
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 180);
SDL_FRect pauseOverlay{0, 0, logicalW, logicalH};
SDL_RenderFillRect(renderer, &pauseOverlay);
pixelFont->draw(renderer, logicalW * 0.5f - 80, logicalH * 0.5f - 20, "PAUSED", 2.0f, {255, 255, 255, 255});
pixelFont->draw(renderer, logicalW * 0.5f - 120, logicalH * 0.5f + 30, "Press P to resume", 0.8f, {200, 200, 220, 255});
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
}
if (showExitConfirmPopup) {
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 200);
SDL_FRect fullWin{0.f, 0.f, winW, winH};
SDL_RenderFillRect(renderer, &fullWin);
const float panelW = 640.0f;
const float panelH = 320.0f;
SDL_FRect panel{
(logicalW - panelW) * 0.5f + contentOffsetX,
(logicalH - panelH) * 0.5f + contentOffsetY,
panelW,
panelH
};
SDL_FRect shadow{panel.x + 6.0f, panel.y + 10.0f, panel.w, panel.h};
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 140);
SDL_RenderFillRect(renderer, &shadow);
for (int i = 0; i < 5; ++i) {
SDL_FRect glow{panel.x - float(i * 2), panel.y - float(i * 2), panel.w + float(i * 4), panel.h + float(i * 4)};
SDL_SetRenderDrawColor(renderer, 0, 180, 255, Uint8(44 - i * 7));
SDL_RenderRect(renderer, &glow);
}
SDL_SetRenderDrawColor(renderer, 18, 30, 52, 255);
SDL_RenderFillRect(renderer, &panel);
SDL_SetRenderDrawColor(renderer, 70, 120, 210, 255);
SDL_RenderRect(renderer, &panel);
SDL_FRect inner{panel.x + 24.0f, panel.y + 98.0f, panel.w - 48.0f, panel.h - 146.0f};
SDL_SetRenderDrawColor(renderer, 16, 24, 40, 235);
SDL_RenderFillRect(renderer, &inner);
SDL_SetRenderDrawColor(renderer, 40, 80, 140, 235);
SDL_RenderRect(renderer, &inner);
const std::string title = "EXIT GAME?";
int titleW = 0, titleH = 0;
const float titleScale = 1.8f;
pixelFont->measure(title, titleScale, titleW, titleH);
pixelFont->draw(renderer, panel.x + (panel.w - titleW) * 0.5f, panel.y + 30.0f, title, titleScale, {255, 230, 140, 255});
std::array<std::string, 2> lines = {
"Are you sure you want to quit?",
"Current progress will be lost."
};
float lineY = inner.y + 22.0f;
const float lineScale = 1.05f;
for (const auto& line : lines) {
int lineW = 0, lineH = 0;
pixelFont->measure(line, lineScale, lineW, lineH);
float textX = panel.x + (panel.w - lineW) * 0.5f;
pixelFont->draw(renderer, textX, lineY, line, lineScale, SDL_Color{210, 220, 240, 255});
lineY += lineH + 10.0f;
}
const float horizontalPad = 28.0f;
const float buttonGap = 32.0f;
const float buttonH = 66.0f;
float buttonW = (inner.w - horizontalPad * 2.0f - buttonGap) * 0.5f;
float buttonY = inner.y + inner.h - buttonH - 24.0f;
auto drawButton = [&](float x, const char* label, SDL_Color base) {
SDL_FRect btn{x, buttonY, buttonW, buttonH};
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 120);
SDL_FRect btnShadow{btn.x + 4.0f, btn.y + 6.0f, btn.w, btn.h};
SDL_RenderFillRect(renderer, &btnShadow);
SDL_SetRenderDrawColor(renderer, base.r, base.g, base.b, base.a);
SDL_RenderFillRect(renderer, &btn);
SDL_SetRenderDrawColor(renderer, 90, 130, 200, 255);
SDL_RenderRect(renderer, &btn);
int textW = 0, textH = 0;
const float labelScale = 1.4f;
pixelFont->measure(label, labelScale, textW, textH);
float textX = btn.x + (btn.w - textW) * 0.5f;
float textY = btn.y + (btn.h - textH) * 0.5f;
pixelFont->draw(renderer, textX, textY, label, labelScale, SDL_Color{255, 255, 255, 255});
};
float yesX = inner.x + horizontalPad;
float noX = yesX + buttonW + buttonGap;
drawButton(yesX, "YES", SDL_Color{185, 70, 70, 255});
drawButton(noX, "NO", SDL_Color{60, 95, 150, 255});
}
}

View File

@ -0,0 +1,42 @@
#pragma once
#include <SDL3/SDL.h>
#include "../gameplay/Game.h"
// Forward declarations
class FontAtlas;
class LineEffect;
/**
* GameRenderer - Utility class for rendering the Tetris game board and HUD.
*
* This class encapsulates all the game-specific rendering logic that was
* previously in main.cpp, making it reusable across different contexts.
*/
class GameRenderer {
public:
// Render the complete playing state including game board, HUD, and effects
static void renderPlayingState(
SDL_Renderer* renderer,
Game* game,
FontAtlas* pixelFont,
LineEffect* lineEffect,
SDL_Texture* blocksTex,
float logicalW,
float logicalH,
float logicalScale,
float winW,
float winH,
bool showExitConfirmPopup,
int exitPopupSelectedButton = 1,
bool suppressPauseVisuals = false
);
private:
// Helper functions for drawing game elements
static void drawBlockTexture(SDL_Renderer* renderer, SDL_Texture* blocksTex, float x, float y, float size, int blockType);
static void drawPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, const Game::Piece& piece, float ox, float oy, float tileSize, bool isGhost = false);
static void drawSmallPiece(SDL_Renderer* renderer, SDL_Texture* blocksTex, PieceType pieceType, float x, float y, float tileSize);
// Helper function for drawing rectangles
static void drawRect(SDL_Renderer* renderer, float x, float y, float w, float h, SDL_Color c);
};

View File

@ -0,0 +1,298 @@
#include "RenderManager.h"
#include <SDL3/SDL.h>
#include <algorithm>
RenderManager::RenderManager() = default;
RenderManager::~RenderManager() {
if (m_initialized) {
shutdown();
}
}
bool RenderManager::initialize(int width, int height, const std::string& title) {
if (m_initialized) {
SDL_LogWarn(SDL_LOG_CATEGORY_RENDER, "RenderManager already initialized");
return true;
}
SDL_LogInfo(SDL_LOG_CATEGORY_RENDER, "Initializing RenderManager (%dx%d)", width, height);
// Create window
m_window = SDL_CreateWindow(
title.c_str(),
width, height,
SDL_WINDOW_RESIZABLE
);
if (!m_window) {
SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Failed to create window: %s", SDL_GetError());
return false;
}
// Create renderer
m_renderer = SDL_CreateRenderer(m_window, nullptr);
if (!m_renderer) {
SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Failed to create renderer: %s", SDL_GetError());
SDL_DestroyWindow(m_window);
m_window = nullptr;
return false;
}
// Enable VSync
SDL_SetRenderVSync(m_renderer, 1);
// Store window dimensions
m_windowWidth = width;
m_windowHeight = height;
m_logicalWidth = width;
m_logicalHeight = height;
// Set initial logical size
setLogicalSize(width, height);
m_initialized = true;
SDL_LogInfo(SDL_LOG_CATEGORY_RENDER, "RenderManager initialized successfully");
return true;
}
void RenderManager::shutdown() {
if (!m_initialized) {
return;
}
SDL_LogInfo(SDL_LOG_CATEGORY_RENDER, "Shutting down RenderManager");
if (m_renderer) {
SDL_DestroyRenderer(m_renderer);
m_renderer = nullptr;
}
if (m_window) {
SDL_DestroyWindow(m_window);
m_window = nullptr;
}
m_initialized = false;
SDL_LogInfo(SDL_LOG_CATEGORY_RENDER, "RenderManager shutdown complete");
}
void RenderManager::beginFrame() {
if (!m_initialized || !m_renderer) {
return;
}
// Trace beginFrame entry
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::beginFrame entry\n"); fclose(f); }
}
// Clear the screen (wrapped with trace)
clear(12, 12, 16, 255); // Dark background similar to original
// Trace after clear
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::beginFrame after clear\n"); fclose(f); }
}
}
void RenderManager::endFrame() {
if (!m_initialized || !m_renderer) {
return;
}
// Trace before present
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::endFrame before present\n"); fclose(f); }
}
SDL_RenderPresent(m_renderer);
// Trace after present
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::endFrame after present\n"); fclose(f); }
}
}
void RenderManager::setLogicalSize(int width, int height) {
if (!m_initialized || !m_renderer) {
return;
}
m_logicalWidth = width;
m_logicalHeight = height;
updateLogicalScale();
}
void RenderManager::setViewport(int x, int y, int width, int height) {
if (!m_initialized || !m_renderer) {
return;
}
SDL_Rect viewport = { x, y, width, height };
SDL_SetRenderViewport(m_renderer, &viewport);
// Keep cached logical viewport in sync if this matches our computed logical scale
m_logicalVP = viewport;
}
void RenderManager::setScale(float scaleX, float scaleY) {
if (!m_initialized || !m_renderer) {
return;
}
m_scaleX = scaleX;
m_scaleY = scaleY;
SDL_SetRenderScale(m_renderer, scaleX, scaleY);
}
void RenderManager::resetViewport() {
if (!m_initialized || !m_renderer) {
return;
}
// Reset to full window viewport and recompute logical scale/viewport
SDL_SetRenderViewport(m_renderer, nullptr);
updateLogicalScale();
}
void RenderManager::clear(Uint8 r, Uint8 g, Uint8 b, Uint8 a) {
if (!m_initialized || !m_renderer) {
return;
}
SDL_SetRenderDrawColor(m_renderer, r, g, b, a);
SDL_RenderClear(m_renderer);
}
void RenderManager::renderTexture(SDL_Texture* texture, const SDL_FRect* src, const SDL_FRect* dst) {
if (!m_initialized || !m_renderer || !texture) {
return;
}
// Trace renderTexture usage
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::renderTexture entry tex=%llu src=%p dst=%p\n", (unsigned long long)(uintptr_t)texture, (void*)src, (void*)dst); fclose(f); }
}
SDL_RenderTexture(m_renderer, texture, src, dst);
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "RenderManager::renderTexture after SDL_RenderTexture tex=%llu\n", (unsigned long long)(uintptr_t)texture); fclose(f); }
}
}
void RenderManager::renderTexture(SDL_Texture* texture, float x, float y) {
if (!texture) {
return;
}
float w, h;
SDL_GetTextureSize(texture, &w, &h);
SDL_FRect dst = { x, y, w, h };
renderTexture(texture, nullptr, &dst);
}
void RenderManager::renderTexture(SDL_Texture* texture, float x, float y, float w, float h) {
if (!texture) {
return;
}
SDL_FRect dst = { x, y, w, h };
renderTexture(texture, nullptr, &dst);
}
void RenderManager::renderRect(const SDL_FRect& rect, Uint8 r, Uint8 g, Uint8 b, Uint8 a) {
if (!m_initialized || !m_renderer) {
return;
}
SDL_SetRenderDrawColor(m_renderer, r, g, b, a);
SDL_RenderFillRect(m_renderer, &rect);
}
void RenderManager::renderLine(float x1, float y1, float x2, float y2, Uint8 r, Uint8 g, Uint8 b, Uint8 a) {
if (!m_initialized || !m_renderer) {
return;
}
SDL_SetRenderDrawColor(m_renderer, r, g, b, a);
SDL_RenderLine(m_renderer, x1, y1, x2, y2);
}
void RenderManager::renderPoint(float x, float y, Uint8 r, Uint8 g, Uint8 b, Uint8 a) {
if (!m_initialized || !m_renderer) {
return;
}
SDL_SetRenderDrawColor(m_renderer, r, g, b, a);
SDL_RenderPoint(m_renderer, x, y);
}
void RenderManager::handleWindowResize(int newWidth, int newHeight) {
if (!m_initialized) {
return;
}
SDL_LogInfo(SDL_LOG_CATEGORY_RENDER, "Window resized to %dx%d", newWidth, newHeight);
m_windowWidth = newWidth;
m_windowHeight = newHeight;
updateLogicalScale();
}
void RenderManager::setFullscreen(bool fullscreen) {
if (!m_initialized || !m_window) {
return;
}
if (m_isFullscreen == fullscreen) {
return;
}
SDL_SetWindowFullscreen(m_window, fullscreen ? SDL_WINDOW_FULLSCREEN : 0);
m_isFullscreen = fullscreen;
// Update window size after fullscreen change
SDL_GetWindowSize(m_window, &m_windowWidth, &m_windowHeight);
updateLogicalScale();
}
void RenderManager::getWindowSize(int& width, int& height) const {
width = m_windowWidth;
height = m_windowHeight;
}
void RenderManager::getTextureSize(SDL_Texture* tex, int& w, int& h) const {
if (!tex) { w = 0; h = 0; return; }
// SDL3 provides SDL_GetTextureSize which accepts float or int pointers depending on overloads
float fw = 0.0f, fh = 0.0f;
SDL_GetTextureSize(tex, &fw, &fh);
w = int(fw + 0.5f);
h = int(fh + 0.5f);
}
void RenderManager::updateLogicalScale() {
if (!m_initialized || !m_renderer) {
return;
}
// Calculate scale to fit logical size into window
float scaleX = static_cast<float>(m_windowWidth) / static_cast<float>(m_logicalWidth);
float scaleY = static_cast<float>(m_windowHeight) / static_cast<float>(m_logicalHeight);
// Use uniform scaling to maintain aspect ratio
float scale = std::min(scaleX, scaleY);
if (scale <= 0.0f) {
scale = 1.0f;
}
setScale(scale, scale);
// Compute centered logical viewport that preserves aspect ratio and is centered in window
int vpW = static_cast<int>(m_logicalWidth * scale);
int vpH = static_cast<int>(m_logicalHeight * scale);
int vpX = (m_windowWidth - vpW) / 2;
int vpY = (m_windowHeight - vpH) / 2;
SDL_Rect vp{ vpX, vpY, vpW, vpH };
SDL_SetRenderViewport(m_renderer, &vp);
// Cache logical viewport and scale for callers
m_logicalVP = vp;
m_logicalScale = scale;
}

View File

@ -0,0 +1,92 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
/**
* RenderManager - Abstracts SDL rendering functionality
*
* Responsibilities:
* - Manage SDL_Window and SDL_Renderer lifecycle
* - Provide high-level rendering interface
* - Handle viewport and scaling logic
* - Abstract SDL-specific details from game code
*/
class RenderManager {
public:
RenderManager();
~RenderManager();
// Initialization and cleanup
bool initialize(int width, int height, const std::string& title);
void shutdown();
// Frame management
void beginFrame();
void endFrame();
// Viewport and scaling
void setLogicalSize(int width, int height);
void setViewport(int x, int y, int width, int height);
void setScale(float scaleX, float scaleY);
void resetViewport();
// Query the computed logical viewport and scale (useful for consistent input mapping)
SDL_Rect getLogicalViewport() const { return m_logicalVP; }
float getLogicalScale() const { return m_logicalScale; }
// Basic rendering operations
void clear(Uint8 r = 0, Uint8 g = 0, Uint8 b = 0, Uint8 a = 255);
// Texture rendering
void renderTexture(SDL_Texture* texture, const SDL_FRect* src, const SDL_FRect* dst);
void renderTexture(SDL_Texture* texture, float x, float y);
void renderTexture(SDL_Texture* texture, float x, float y, float w, float h);
// Primitive rendering
void renderRect(const SDL_FRect& rect, Uint8 r, Uint8 g, Uint8 b, Uint8 a = 255);
void renderLine(float x1, float y1, float x2, float y2, Uint8 r, Uint8 g, Uint8 b, Uint8 a = 255);
void renderPoint(float x, float y, Uint8 r, Uint8 g, Uint8 b, Uint8 a = 255);
// Window management
void handleWindowResize(int newWidth, int newHeight);
void setFullscreen(bool fullscreen);
void getWindowSize(int& width, int& height) const;
// Direct access to SDL objects (temporary, will be removed later)
SDL_Renderer* getSDLRenderer() const { return m_renderer; }
SDL_Window* getSDLWindow() const { return m_window; }
// Texture queries
void getTextureSize(SDL_Texture* tex, int& w, int& h) const;
// State queries
bool isInitialized() const { return m_initialized; }
bool isFullscreen() const { return m_isFullscreen; }
private:
// SDL objects
SDL_Window* m_window = nullptr;
SDL_Renderer* m_renderer = nullptr;
// Window properties
int m_windowWidth = 0;
int m_windowHeight = 0;
int m_logicalWidth = 0;
int m_logicalHeight = 0;
float m_scaleX = 1.0f;
float m_scaleY = 1.0f;
// State
bool m_initialized = false;
bool m_isFullscreen = false;
// Cached logical viewport and scale (centered within window)
SDL_Rect m_logicalVP{0,0,0,0};
float m_logicalScale = 1.0f;
// Helper methods
void updateLogicalScale();
// Query the computed logical viewport and scale (useful for consistent input mapping)
SDL_Rect getLogicalViewport() const { return m_logicalVP; }
float getLogicalScale() const { return m_logicalScale; }
};

View File

@ -0,0 +1,41 @@
// Starfield.cpp - implementation (copied into src/graphics)
#include "graphics/Starfield.h"
#include <SDL3/SDL.h>
#include <random>
void Starfield::init(int count, int w, int h)
{
stars.clear();
stars.reserve(count);
std::mt19937 rng{std::random_device{}()};
std::uniform_real_distribution<float> dx(0.f, (float)w), dy(0.f, (float)h), dz(0.2f, 1.f);
for (int i = 0; i < count; ++i)
stars.push_back({dx(rng), dy(rng), dz(rng), 15.f + 35.f * dz(rng)});
lastW = w;
lastH = h;
}
void Starfield::update(float dt, int w, int h)
{
if (w != lastW || h != lastH || stars.empty())
init((w * h) / 8000 + 120, w, h);
for (auto &s : stars)
{
s.y += s.speed * dt;
if (s.y > h)
{
s.y -= h;
s.x = (float)(rand() % w);
}
}
}
void Starfield::draw(SDL_Renderer *r) const
{
SDL_SetRenderDrawColor(r, 255, 255, 255, 255);
for (auto &s : stars)
{
SDL_FRect fr{s.x, s.y, 1.f * s.z, 1.f * s.z};
SDL_RenderFillRect(r, &fr);
}
}

View File

@ -0,0 +1,15 @@
// Starfield.h - Procedural starfield background effect
#pragma once
#include <vector>
struct SDL_Renderer; // fwd
class Starfield {
public:
void init(int count, int w, int h);
void update(float dt, int w, int h);
void draw(SDL_Renderer* r) const;
private:
struct Star { float x,y,z,speed; };
std::vector<Star> stars;
int lastW{0}, lastH{0};
};

Some files were not shown because too many files have changed in this diff Show More