Skip to content

Commit da27e37

Browse files
committed
Add pytest test suite with conftest, test_grid, test_block, test_game
1 parent 637fd64 commit da27e37

4 files changed

Lines changed: 289 additions & 0 deletions

File tree

tests/conftest.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import sys
2+
import os
3+
from unittest.mock import MagicMock
4+
5+
# Set dummy SDL drivers so pygame.init() doesn't error on headless/audio-less systems
6+
os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
7+
os.environ.setdefault("SDL_AUDIODRIVER", "dummy")
8+
9+
import pygame
10+
pygame.init()
11+
12+
# Patch mixer before game is imported so Game.__init__ doesn't need real audio hardware
13+
pygame.mixer.Sound = MagicMock(return_value=MagicMock())
14+
pygame.mixer.music = MagicMock()
15+
16+
# Add repo root to sys.path so all source modules are importable
17+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

tests/test_block.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import sys
2+
import os
3+
4+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
5+
6+
from blocks import LBlock
7+
from position import Position
8+
9+
10+
def test_move_updates_offsets():
11+
block = LBlock()
12+
# LBlock.__init__ calls move(0, 3) so reset offsets first
13+
block.row_offset = 0
14+
block.column_offset = 0
15+
block.move(2, 3)
16+
assert block.row_offset == 2
17+
assert block.column_offset == 3
18+
19+
20+
def test_move_accumulates():
21+
block = LBlock()
22+
block.row_offset = 0
23+
block.column_offset = 0
24+
block.move(1, 0)
25+
block.move(2, 0)
26+
assert block.row_offset == 3
27+
28+
29+
def test_get_cell_positions_applies_offset():
30+
block = LBlock()
31+
# Reset to known zero offsets, then apply a single offset
32+
block.row_offset = 0
33+
block.column_offset = 0
34+
block.rotation_state = 0
35+
36+
# Get base positions (no offset)
37+
base_positions = [
38+
Position(p.row, p.column) for p in block.cells[0]
39+
]
40+
41+
block.move(1, 0)
42+
moved_positions = block.get_cell_positions()
43+
44+
for base, moved in zip(base_positions, moved_positions):
45+
assert moved.row == base.row + 1
46+
assert moved.column == base.column
47+
48+
49+
def test_rotate_increments_state():
50+
block = LBlock()
51+
initial_state = block.rotation_state
52+
block.rotate()
53+
assert block.rotation_state == initial_state + 1
54+
55+
56+
def test_rotate_wraps_around():
57+
block = LBlock()
58+
num_states = len(block.cells)
59+
# Rotate through all states; next rotate should wrap back to 0
60+
for _ in range(num_states):
61+
block.rotate()
62+
assert block.rotation_state == 0
63+
64+
65+
def test_undo_rotation_decrements_state():
66+
block = LBlock()
67+
block.rotation_state = 2
68+
block.undo_rotation()
69+
assert block.rotation_state == 1
70+
71+
72+
def test_undo_rotation_wraps_around():
73+
block = LBlock()
74+
block.rotation_state = 0
75+
block.undo_rotation()
76+
# Should wrap to the last valid state
77+
assert block.rotation_state == len(block.cells) - 1

tests/test_game.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import sys
2+
import os
3+
4+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
5+
6+
# conftest.py has already patched pygame.mixer before this import
7+
from game import Game
8+
from blocks import IBlock, JBlock, LBlock, OBlock, SBlock, TBlock, ZBlock
9+
10+
ALL_BLOCK_TYPES = {IBlock, JBlock, LBlock, OBlock, SBlock, TBlock, ZBlock}
11+
12+
13+
def test_initial_score_is_zero():
14+
game = Game()
15+
assert game.score == 0
16+
17+
18+
def test_initial_game_over_is_false():
19+
game = Game()
20+
assert game.game_over is False
21+
22+
23+
def test_update_score_one_line():
24+
game = Game()
25+
game.update_score(1, 0)
26+
assert game.score == 100
27+
28+
29+
def test_update_score_two_lines():
30+
game = Game()
31+
game.update_score(2, 0)
32+
assert game.score == 300
33+
34+
35+
def test_update_score_three_lines():
36+
game = Game()
37+
game.update_score(3, 0)
38+
assert game.score == 500
39+
40+
41+
def test_update_score_move_down():
42+
game = Game()
43+
game.update_score(0, 5)
44+
assert game.score == 5
45+
46+
47+
def test_update_score_no_lines():
48+
game = Game()
49+
game.update_score(0, 0)
50+
assert game.score == 0
51+
52+
53+
def test_get_random_block_removes_from_bag():
54+
game = Game()
55+
# After __init__, two blocks have already been drawn (current_block and next_block).
56+
# Reset the bag to a full set of 7 to get a predictable starting count.
57+
from blocks import IBlock, JBlock, LBlock, OBlock, SBlock, TBlock, ZBlock
58+
game.blocks = [IBlock(), JBlock(), LBlock(), OBlock(), SBlock(), TBlock(), ZBlock()]
59+
game.get_random_block()
60+
assert len(game.blocks) == 6
61+
62+
63+
def test_get_random_block_refills_bag():
64+
game = Game()
65+
# Drain all 7 blocks from a fresh bag
66+
game.blocks = [IBlock(), JBlock(), LBlock(), OBlock(), SBlock(), TBlock(), ZBlock()]
67+
for _ in range(7):
68+
game.get_random_block()
69+
# Bag is now empty; next call must refill and return a block
70+
assert len(game.blocks) == 0
71+
block = game.get_random_block()
72+
# After refill and one removal, bag has 6 items
73+
assert block is not None
74+
assert len(game.blocks) == 6
75+
76+
77+
def test_get_random_block_all_7_types():
78+
game = Game()
79+
# Start with a fresh, full bag
80+
game.blocks = [IBlock(), JBlock(), LBlock(), OBlock(), SBlock(), TBlock(), ZBlock()]
81+
drawn_types = set()
82+
for _ in range(7):
83+
block = game.get_random_block()
84+
drawn_types.add(type(block))
85+
assert drawn_types == ALL_BLOCK_TYPES

tests/test_grid.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import sys
2+
import os
3+
4+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
5+
6+
from grid import Grid
7+
8+
9+
def test_is_inside_valid():
10+
grid = Grid()
11+
# All corners and a middle cell should be inside
12+
assert grid.is_inside(0, 0) is True
13+
assert grid.is_inside(19, 9) is True
14+
assert grid.is_inside(0, 9) is True
15+
assert grid.is_inside(19, 0) is True
16+
assert grid.is_inside(10, 5) is True
17+
18+
19+
def test_is_inside_invalid():
20+
grid = Grid()
21+
assert grid.is_inside(-1, 0) is False
22+
assert grid.is_inside(20, 0) is False
23+
assert grid.is_inside(0, -1) is False
24+
assert grid.is_inside(0, 10) is False
25+
26+
27+
def test_is_empty_returns_true_for_zero():
28+
grid = Grid()
29+
# Fresh grid cells are all 0, so they should be empty
30+
assert grid.is_empty(0, 0) is True
31+
assert grid.is_empty(10, 5) is True
32+
assert grid.is_empty(19, 9) is True
33+
34+
35+
def test_is_empty_returns_false_for_nonzero():
36+
grid = Grid()
37+
grid.grid[5][3] = 1
38+
assert grid.is_empty(5, 3) is False
39+
40+
41+
def test_is_row_full_false_when_empty():
42+
grid = Grid()
43+
assert grid.is_row_full(0) is False
44+
assert grid.is_row_full(10) is False
45+
assert grid.is_row_full(19) is False
46+
47+
48+
def test_is_row_full_true_when_all_filled():
49+
grid = Grid()
50+
for col in range(grid.num_cols):
51+
grid.grid[7][col] = 1
52+
assert grid.is_row_full(7) is True
53+
54+
55+
def test_clear_row():
56+
grid = Grid()
57+
# Fill row 5 with non-zero values
58+
for col in range(grid.num_cols):
59+
grid.grid[5][col] = 3
60+
grid.clear_row(5)
61+
for col in range(grid.num_cols):
62+
assert grid.grid[5][col] == 0
63+
64+
65+
def test_move_row_down():
66+
grid = Grid()
67+
# Place a distinct pattern in row 3
68+
for col in range(grid.num_cols):
69+
grid.grid[3][col] = col + 1 # values 1..10
70+
grid.move_row_down(3, 2)
71+
# Row 3+2=5 should now contain the old row 3 values
72+
for col in range(grid.num_cols):
73+
assert grid.grid[5][col] == col + 1
74+
# Row 3 should be cleared
75+
for col in range(grid.num_cols):
76+
assert grid.grid[3][col] == 0
77+
78+
79+
def test_clear_full_rows_single():
80+
grid = Grid()
81+
# Fill the bottom row completely
82+
for col in range(grid.num_cols):
83+
grid.grid[19][col] = 1
84+
cleared = grid.clear_full_rows()
85+
assert cleared == 1
86+
# Row 19 should now be empty
87+
for col in range(grid.num_cols):
88+
assert grid.grid[19][col] == 0
89+
90+
91+
def test_clear_full_rows_multiple():
92+
grid = Grid()
93+
# Fill bottom two rows completely
94+
for col in range(grid.num_cols):
95+
grid.grid[18][col] = 2
96+
grid.grid[19][col] = 1
97+
cleared = grid.clear_full_rows()
98+
assert cleared == 2
99+
100+
101+
def test_reset():
102+
grid = Grid()
103+
# Set some cells
104+
grid.grid[0][0] = 5
105+
grid.grid[10][5] = 3
106+
grid.grid[19][9] = 7
107+
grid.reset()
108+
for row in range(grid.num_rows):
109+
for col in range(grid.num_cols):
110+
assert grid.grid[row][col] == 0

0 commit comments

Comments
 (0)