9.2 KiB
Spacetris — 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)
- 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 inGravityManager.
- Note: FRAMES_TABLE is now immutable inside
- Extract GravityManager (SRP)
- Note:
src/core/GravityManager.{h,cpp}implemented andGamedelegates gravity computation to it.
- Note:
- Replace globals /
externtexture usage withStateContext-passed resources- Note: popup/background and menu wrappers now accept textures via
StateContext; the temporaryfile_blocksTexbridge was removed. Rungrep -R "extern .*SDL_Texture" src/to confirm no remaining externs.
- Note: popup/background and menu wrappers now accept textures via
- [~] 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.
- 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.
- Note: majority of printf/fprintf debug prints were replaced with
- Add unit tests (gravity conversion, level progression, line clear behavior)
- Note: a small test runner (
tests/GravityTests.cpp) and thetetris_testsCMake target were added and run locally; gravity tests pass (see build-msvc test run). Converting to broader Catch2 suites is optional.
- Note: a small test runner (
- 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
Gameresponsibilities 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
constexprand immutable.- File:
src/Game.cpp(reverse any mutable anonymous namespace table changes) - Why: avoids accidental global mutation; makes compute deterministic.
- File:
- Move per-level multiplier array from global static to
Gameinstance:- Add
std::array<double, 30> levelMultipliersinsideGame(default 1.0). - Change
setLevelGravityMultiplierto update instance array and recomputegravityMs.
- Add
- 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
printfdebug prints withSDL_Logor an injectedLoggerinterface. - Prefer a build-time
#ifdef DEBUGor 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* backgroundTexor similar; make call-sites acceptSDL_Texture*or read fromctx.
Files: src/main.cpp, src/states/MenuState.cpp, other states.
4) Extract GravityManager (Medium risk — 2–6 hours)
- Create
src/core/GravityManager.h|cppthat encapsulates- the
constexprNES frames table (read-only) - per-instance multipliers
- conversion helpers: frames->ms and ms->fps
- the
Gamewill holdGravityManager gravity;and callgravity.getMs(level).
Benefits: easier testing and isolated behavior change for future gravity models.
5) Extract Board/Scoring responsibilities (Medium — 4–8 hours)
- Split
Gameinto smaller collaborators:Board— raw grid, collision, line detection/clear operationsScorer— scoring rules, combo handling, level progression thresholdsPieceController— piece spawn, rotation, kicks
- Keep
Gameas 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.jsonor asFetchContentin 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<SDL_FRect> 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
gravityGlobalMultiplierand 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_ptrwith custom deleter where appropriate, or centralize lifetime in aResourceManager. - Add
constqualifiers to getters where possible. - Remove magic numbers; define named
constexprconstants 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.ps1copies all required DLLs for SDL3 and SDL3_ttf fromvcpkg_installedpaths. - 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
- Small safety PR: make frames table
constexpr, move multipliers intoGameinstance, clamp values, and add SDL_Log usage. (1–3 hours) - Global cleanup PR: remove
externtextures and ensure all resource access goes throughStateContext. (1–2 hours) - Add debug knobs & HUD display improvements. (1 hour)
- Add tests and CMake test target. (3–6 hours)
- Extract GravityManager and write unit tests for it. (2–4 hours)
- Extract Board and Scorer (bigger refactor, add tests). (4–8 hours)
- CI + packaging + formatting. (2–4 hours)
Recommended quick wins (apply immediately)
- Convert NES frames table to
constexprand clamp gravity to >= 1ms. - Replace
printfwithSDL_Logfor debug output. - Pass
SDL_Texture*viaStateContextinstead ofexternglobals (you already did this; check for leftovers withgrepfor "extern .*backgroundTex"). - Add simple unit test that asserts
gravityMs(level0) == FRAME_MS * 48 * multiplier.
Example: gravity computation (reference)
// 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<double,30> 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
Gameand add tests/CI.
Next steps I can take for you now
- Create a PR that converts the frames table to
constexprand moves multipliers intoGameinstance (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.