# Tetris — Upgrade Roadmap This document lists recommended code, architecture, tooling, and runtime upgrades for the native SDL3 Tetris project. Items are grouped, prioritized, and mapped to target files and effort estimates so you can plan incremental work. ## Short plan - Audit surface area and break tasks into small, testable PRs. - Start with low-risk safety and build improvements, then refactors (Gravity/Scoring/Board), then tests/CI, then UX polish and optional features. ## Checklist (high level) - [x] Make NES gravity table constant (constexpr) and move per-level multipliers out of a mutable global - Note: FRAMES_TABLE is now immutable inside `GravityManager`; per-level multipliers are instance-scoped in `GravityManager`. - [x] Extract GravityManager (SRP) - Note: `src/core/GravityManager.{h,cpp}` implemented and `Game` delegates gravity computation to it. - [x] Replace globals / `extern` texture usage with `StateContext`-passed resources - Note: popup/background and menu wrappers now accept textures via `StateContext`; the temporary `file_blocksTex` bridge was removed. Run `grep -R "extern .*SDL_Texture" src/` to confirm no remaining externs. - [~] Add runtime knobs (gravity multiplier +/-, HUD display) and clamp ranges - Note: HUD displays gravity ms/fps; global/level multipliers are exposed via API but runtime keys/persistence are still pending. - [x] Replace ad-hoc printf with SDL_Log or injected Logger service - Note: majority of printf/fprintf debug prints were replaced with `SDL_Log*` calls; a quick grep audit is recommended to find any remaining ad-hoc prints. - [x] Add unit tests (gravity conversion, level progression, line clear behavior) - Note: a small test runner (`tests/GravityTests.cpp`) and the `tetris_tests` CMake target were added and run locally; gravity tests pass (see build-msvc test run). Converting to broader Catch2 suites is optional. - [ ] Add CI (build + tests) and code style checks - [ ] Improve input hit-testing for level popup and scalable UI - [ ] Add defensive guards (clamps, null checks) and const-correctness - [ ] Improve resource management (RAII, smart pointers), and error handling for SDL API calls --- ## Rationale & Goals - Improve maintainability: split `Game` responsibilities into focused classes (SRP) so logic is testable. - Improve safety: remove mutable shared state (globals/`extern`) and minimize hidden side-effects. - Improve tuning: per-level and global gravity tuning must be instance-local and debug-friendly. - Improve developer experience: add unit tests, CI, and runtime debug knobs for quick tuning. --- ## Detailed tasks (prioritized) ### 1) Safety & small win (Low risk — 1–3 hours) - Make the NES frames-per-cell table `constexpr` and immutable. - File: `src/Game.cpp` (reverse any mutable anonymous namespace table changes) - Why: avoids accidental global mutation; makes compute deterministic. - Move per-level multiplier array from global static to `Game` instance: - Add `std::array levelMultipliers` inside `Game` (default 1.0). - Change `setLevelGravityMultiplier` to update instance array and recompute `gravityMs`. - Clamp gravity values to a minimum (e.g., 1.0 ms) and clamp multipliers to sensible range (0.1..10.0). Deliverable: small patch to `Game.h`/`Game.cpp` and a rebuild verification. ### 2) Replace ad-hoc logging (Low risk — 0.5–1 hour) - Replace `printf` debug prints with `SDL_Log` or an injected `Logger` interface. - Prefer a build-time `#ifdef DEBUG` or runtime verbosity flag so release builds are quiet. Files: `src/Game.cpp`, any file using printf for debug. ### 3) Remove fragile globals / externs (Low risk — 1–2 hours) - Ensure all textures, fonts and shared resources are passed via `StateContext& ctx`. - Remove leftover `extern SDL_Texture* backgroundTex` or similar; make call-sites accept `SDL_Texture*` or read from `ctx`. Files: `src/main.cpp`, `src/states/MenuState.cpp`, other states. ### 4) Extract GravityManager (Medium risk — 2–6 hours) - Create `src/core/GravityManager.h|cpp` that encapsulates - the `constexpr` NES frames table (read-only) - per-instance multipliers - conversion helpers: frames->ms and ms->fps - `Game` will hold `GravityManager gravity;` and call `gravity.getMs(level)`. Benefits: easier testing and isolated behavior change for future gravity models. ### 5) Extract Board/Scoring responsibilities (Medium — 4–8 hours) - Split `Game` into smaller collaborators: - `Board` — raw grid, collision, line detection/clear operations - `Scorer` — scoring rules, combo handling, level progression thresholds - `PieceController` — piece spawn, rotation, kicks - Keep `Game` as an orchestrator that composes these objects. Files: `src/Board.*`, `src/Scorer.*`, `src/PieceController.*`, adjust `Game` to compose them. ### 6) Unit tests & test infrastructure (Medium — 3–6 hours) - Add Catch2 or GoogleTest to `vcpkg.json` or as `FetchContent` in CMake. - Add tests: - Gravity conversion tests (frames→ms, per-level multipliers, global multiplier) - Board behavior: place blocks, clear lines, gravity after clear - Level progression increment logic and resulting gravity changes - Add a `tests/` CMake target and basic CI integration (see next section). ### 7) CI / Build checks (Medium — 2–4 hours) - Add GitHub Actions to build Debug/Release on Windows and run tests. - Add static analysis: clang-tidy/clang-format or cpplint configured for the project style. - Add a Pre-commit hook to run format checks. Files: `.github/workflows/build.yml`, `.clang-format`, optional `clang-tidy` config. ### 8) UX and input correctness (Low–Medium — 1–3 hours) - Update level popup hit-testing to use the same computed button rectangles used for drawing. - Expose a function that returns vector of button bounds from the popup draw logic. - Ensure popup background texture is stretched to the logical viewport and that overlay alpha is constant across window sizes. - Add keyboard navigation for popup (arrow keys + Enter) and mouse hover effects. Files: `src/main.cpp`, `src/states/MenuState.cpp`. ### 9) Runtime debug knobs (Low — 1 hour) - Add keys to increase/decrease `gravityGlobalMultiplier` (e.g., `[` and `]`) and reset to 1.0. - Show `gravityGlobalMultiplier` and per-level effective ms on HUD (already partly implemented). - Persist tuning to a small JSON file `settings/debug_tuning.json` (optional). Files: `src/Game.h/cpp`, `src/main.cpp` HUD code. ### 10) Defensive & correctness improvements (Low — 2–4 hours) - Add null checks for SDL_CreateTextureFromSurface and related APIs; log and fallback gracefully. - Convert raw SDL_Texture* ownership to `std::unique_ptr` with custom deleter where appropriate, or centralize lifetime in a `ResourceManager`. - Add `const` qualifiers to getters where possible. - Remove magic numbers; define named `constexpr` constants for UI sizes, softDrop multiplier, DAS/ARR etc. Files: various (`src/*.cpp`, `src/*.h`). ### 11) Packaging & build improvements (Low — 1–2 hours) - Verify `build-production.ps1` copies all required DLLs for SDL3 and SDL3_ttf from `vcpkg_installed` paths. - Add an automated packaging job to CI that creates a ZIP artifact on release tags. Files: `build-production.ps1`, `.github/workflows/release.yml`. --- ## Suggested incremental PR plan 1. Small safety PR: make frames table `constexpr`, move multipliers into `Game` instance, clamp values, and add SDL_Log usage. (1–3 hours) 2. Global cleanup PR: remove `extern` textures and ensure all resource access goes through `StateContext`. (1–2 hours) 3. Add debug knobs & HUD display improvements. (1 hour) 4. Add tests and CMake test target. (3–6 hours) 5. Extract GravityManager and write unit tests for it. (2–4 hours) 6. Extract Board and Scorer (bigger refactor, add tests). (4–8 hours) 7. CI + packaging + formatting. (2–4 hours) --- ## Recommended quick wins (apply immediately) - Convert NES frames table to `constexpr` and clamp gravity to >= 1ms. - Replace `printf` with `SDL_Log` for debug output. - Pass `SDL_Texture*` via `StateContext` instead of `extern` globals (you already did this; check for leftovers with `grep` for "extern .*backgroundTex"). - Add simple unit test that asserts `gravityMs(level0) == FRAME_MS * 48 * multiplier`. --- ## Example: gravity computation (reference) ```cpp // gravity constants constexpr double NES_FPS = 60.0988; constexpr double FRAME_MS = 1000.0 / NES_FPS; 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,1}; // per-instance multipliers in Game std::array levelMultipliers; // default 1.0 double GravityManager::msForLevel(int level) const { int idx = std::clamp(level, 0, 29); double frames = FRAMES_TABLE[idx] * levelMultipliers[idx]; return std::max(1.0, frames * FRAME_MS * globalMultiplier); } ``` --- ## Estimated total effort - Conservative: 16–36 hours depending on how far you split `Game` and add tests/CI. ## Next steps I can take for you now - Create a PR that converts the frames table to `constexpr` and moves multipliers into `Game` instance (small patch + build). (I can do this.) - Add a unit-test harness using Catch2 and a small GravityManager test. Tell me which of the next steps above you'd like me to implement now and I will start the code changes.