Skip to content

Milestone 1: Python 3.12 Modernization with Type Safety and Testing#3

Merged
mintzer merged 14 commits into
morph-mainfrom
python-tetris-game-pygame-milestone_1-fb2e2d
Jun 29, 2026
Merged

Milestone 1: Python 3.12 Modernization with Type Safety and Testing#3
mintzer merged 14 commits into
morph-mainfrom
python-tetris-game-pygame-milestone_1-fb2e2d

Conversation

@mcode-app-dev

@mcode-app-dev mcode-app-dev Bot commented Jun 29, 2026

Copy link
Copy Markdown

View Milestone

Table of Contents


Status

Successfully completed. All deliverables specified in the Milestone Plan were implemented:

  1. pyproject.toml introduced with requires-python = ">=3.12", pygame-ce>=2.5.0, and dev extras (mypy, ruff, pytest).
  2. Full PEP 484/526 type annotations applied to all seven source modules; mypy --strict enabled in pyproject.toml.
  3. ruff configured with rule sets E, W, F, UP, B, I; formatting normalized across the codebase.
  4. Targeted code-idiom fixes applied: LBlock tab indentation normalized, wildcard import replaced with explicit imports, explicit boolean comparisons removed, if __name__ == "__main__": guard added, comma-joined imports split.
  5. tests/ directory created with conftest.py, test_grid.py, test_block.py, and test_game.py (473 lines total).
  6. Game verified to launch and play correctly after all changes.

A [build-system] table was initially missing from pyproject.toml and the build backend was incorrect (hatchling instead of setuptools); both were corrected in follow-up commits. Post-review commits also improved test quality: random.seed(42) was added to conftest.py, a zeroed_lblock fixture was extracted, a _all_blocks() factory was introduced in game.py, and additional tests for Game.move_left, Game.move_right, Game.move_down, Game.reset, and all block subclass cell layouts were added.

No deviations from the Milestone Plan were made.


Feature Overview

This milestone modernizes the Python Tetris Game codebase from an unversioned, untyped, untested Python 3 project to a clean Python 3.12 application. No end-user-visible gameplay changes are introduced; the game plays identically to before. The developer-facing changes are:

  • Reproducible dependency management: pip install -e ".[dev]" installs all runtime and development dependencies from pyproject.toml.
  • Type safety: All seven modules are annotated; mypy --strict passes with no errors.
  • Automated style enforcement: ruff check . and ruff format . enforce PEP 8 compliance, idiomatic Python 3.12 syntax, and consistent import ordering.
  • Unit test coverage: pytest tests/ validates grid boundary checks, row clearing, block transforms, scoring logic, block-bag refill, and core game actions.

Testing

Automated Testing

Run the full test suite from the repository root:

pytest tests/

Test files and coverage:

  • tests/test_grid.pyGrid.is_inside, Grid.is_empty, Grid.is_row_full, Grid.clear_row, Grid.move_row_down, Grid.clear_full_rows, Grid.reset
  • tests/test_block.pyBlock.move, Block.get_cell_positions, Block.rotate, Block.undo_rotation; cell layout assertions for all 7 block subclasses
  • tests/test_game.pyGame.update_score (all scoring branches), Game.get_random_block (7-bag refill), Game.move_left, Game.move_right, Game.move_down, Game.rotate, Game.reset

Type checking:

mypy --strict .

Linting:

ruff check .

Manual Testing

  1. Create a virtual environment and install dependencies:
    python -m venv .venv
    source .venv/bin/activate   # Windows: .venv\Scripts\activate
    pip install -e ".[dev]"
  2. Launch the game:
    python main.py
  3. Verify:
    • The game window opens with the title "Python Tetris".
    • Background music plays.
    • Arrow keys move and rotate the active block.
    • Completed rows clear with a sound effect.
    • The score increments (100 / 300 / 500 points for 1 / 2 / 3 lines).
    • Pressing any key after game over resets the board.

Architecture

Overview

graph TD
    subgraph Legend[" "]
        direction LR
        LY["Modified"]:::modified
        LG["New"]:::new
    end

    pyproject["pyproject.toml"]:::new
    main["main.py\n(entry point)"]:::modified
    game["game.py\n(game state)"]:::modified
    grid["grid.py\n(board state)"]:::modified
    block["block.py\n(block base class)"]:::modified
    blocks["blocks.py\n(7 Tetromino types)"]:::modified
    position["position.py\n(cell position)"]:::modified
    colors["colors.py\n(color palette)"]:::modified

    main --> game
    game --> grid
    game --> block
    blocks --> block
    block --> position
    grid --> position
    game --> colors
    grid --> colors

    classDef modified fill:#FFD700,stroke:#333,color:#000
    classDef new fill:#90EE90,stroke:#333,color:#000
Loading

Changes

pyproject.toml

New file at the repository root. Declares name = "python-tetris-game-pygame", requires-python = ">=3.12", runtime dependency pygame-ce>=2.5.0,<3.0, and dev extras mypy>=1.10, ruff>=0.4, pytest>=8.0. Includes [tool.ruff], [tool.ruff.lint], [tool.mypy], and [tool.pytest.ini_options] configuration sections. A [build-system] table (setuptools.build_meta) was added in a follow-up fix commit.

position.py

The Position class was converted from a hand-written __init__-only class to @dataclass(frozen=True) with typed fields row: int and column: int. This eliminates boilerplate, adds __eq__, __repr__, and __hash__ automatically, enforces immutability, and enables reliable equality comparisons in tests and game logic.

colors.py

All class-level color constants annotated as tuple[int, int, int]. The get_cell_colors() return type annotated as list[tuple[int, int, int]].

block.py

All instance attributes annotated (id: int, cells: dict[int, list[Position]], cell_size: int, row_offset: int, column_offset: int, rotation_state: int, colors: list[tuple[int, int, int]]). All method signatures annotated with parameter types and -> None / -> list[Position] / -> bool return types.

blocks.py

Tab indentation in LBlock.__init__ normalized to 4-space (matching all other block subclasses). All __init__ methods annotated with -> None return types.

grid.py

The grid attribute annotated as list[list[int]]. All method signatures annotated, including draw(screen: pygame.Surface) -> None, is_inside(row: int, column: int) -> bool, is_row_full(row: int) -> bool, clear_row(row: int) -> None, and move_row_down(row: int, num_rows: int) -> None.

game.py

Wildcard import from blocks import * replaced with an explicit import listing all seven block classes (IBlock, JBlock, LBlock, OBlock, SBlock, TBlock, ZBlock). A Block base class import was added. All instance attributes annotated (grid: Grid, blocks: list[Block], current_block: Block, next_block: Block, game_over: bool, score: int, rotate_sound: pygame.mixer.Sound, clear_sound: pygame.mixer.Sound). All method return types annotated. A _all_blocks() module-level factory function was extracted (review fix) to eliminate repeated inline [IBlock(), JBlock(), ...] list literals in __init__, get_random_block, and reset. Explicit boolean comparisons (== False, == True) removed.

main.py

import pygame,sys split into two separate import statements. All top-level variables (screen: pygame.Surface, clock: pygame.time.Clock, game: Game, GAME_UPDATE: int, font/surface/rect variables) annotated. The entire game loop wrapped in if __name__ == "__main__":. All == True / == False comparisons replaced with if game.game_over: / if not game.game_over:.

Design Decisions

1. @dataclass(frozen=True) for Position
The Position class was converted to a frozen dataclass rather than keeping it as a plain class with manual __eq__/__repr__. The dataclass decorator generates __eq__, __repr__, and __hash__ with no added boilerplate, and frozen=True enforces immutability consistent with how Position is used (blocks create new Position instances on every move; they are never mutated in-place). The generated __hash__ also allows Position to be used as a dict key or set member if needed in the future.

2. Explicit block imports in game.py
The wildcard import from blocks import * was replaced with an explicit list of all seven block classes. This makes the module's dependencies visible to mypy and ruff (which flag F403/F405 on star imports), prevents namespace pollution, and satisfies mypy --strict. The block class set is fixed (the canonical Tetris piece set is well-defined), so maintaining the explicit list is not a maintenance burden.

3. Pure-logic test scope
Tests were scoped to the three modules containing no pygame display dependencies (grid.py, block.py, game.py). conftest.py sets SDL_VIDEODRIVER=dummy and SDL_AUDIODRIVER=dummy and patches pygame.mixer.Sound / pygame.mixer.music with MagicMock instances, allowing Game.__init__ to run in headless CI without audio hardware. Rendering methods (draw) and the event loop (main.py) are excluded from automated tests; they are validated by running the game manually.

4. mypy --strict from the start
Rather than an incremental annotation strategy, all seven modules were fully annotated and strict = true was set in [tool.mypy] in the initial commit. The codebase is only ~315 lines; the annotations are straightforward (no complex generics, no recursive types). pygame-ce 2.5.x ships with py.typed stubs, so stub completeness was not a blocker.

5. _all_blocks() factory function
The repeated inline block list [IBlock(), JBlock(), LBlock(), OBlock(), SBlock(), TBlock(), ZBlock()] appeared three times in game.py (in __init__, get_random_block, and reset). A module-level _all_blocks() -> list[Block] factory was extracted to eliminate the repetition and reduce the risk of the lists diverging.


Suggested Order of Review

  1. pyproject.toml — Establishes the toolchain baseline; review dependency pins, ruff rule selection, and mypy settings before reading any source changes.
  2. position.py — Smallest change; illustrates the dataclass conversion pattern used as the foundation for typed position objects throughout the codebase.
  3. colors.py — Simple constant annotations; no structural changes.
  4. block.py — Annotated base class; establishes attribute and method signature patterns repeated in blocks.py, grid.py, and game.py.
  5. blocks.py — Block subclasses; review the indentation fix in LBlock and the -> None return type additions.
  6. grid.py — Annotated board state; list[list[int]] grid type and annotated mutation methods.
  7. game.py — Core game state; review the explicit import list, _all_blocks() factory, attribute annotations, and boolean idiom fixes.
  8. main.py — Entry point; review the if __name__ == "__main__": guard, split imports, boolean idiom fixes, and top-level variable annotations.
  9. tests/conftest.py — Test harness setup; SDL dummy drivers, RNG seed, mixer mocking.
  10. tests/test_grid.py, tests/test_block.py, tests/test_game.py — Test coverage for grid state, block transforms, and game logic respectively.

mcode-bot added 14 commits June 29, 2026 14:59
- position.py: convert to @DataClass(frozen=True) with typed row/column fields
- colors.py: annotate all color class attributes as tuple[int, int, int]; annotate get_cell_colors() return type
- block.py: annotate all instance attributes and method signatures
- blocks.py: fix LBlock tab indentation to 4-space; add -> None return types to all __init__ methods
- grid.py: annotate all instance attributes and method signatures
- game.py: replace wildcard 'from blocks import *' with explicit imports; add Block import; annotate all attributes and methods; replace == False/== True with not/direct bool checks
- main.py: split 'import pygame,sys' into two lines; add if __name__ == "__main__": guard wrapping game loop; replace all == True/== False comparisons with idiomatic boolean checks; annotate all top-level variables
…inside test_get_random_block_removes_from_bag
…locks() factory in game.py and _fresh_bag() helper in test_game.py
…ing block subclasses (JBlock, IBlock, OBlock, SBlock, TBlock, ZBlock)
@mintzer mintzer merged commit f0e6724 into morph-main Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants