trianglengin 1.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tests/__init__.py +0 -0
- tests/conftest.py +108 -0
- tests/core/__init__.py +2 -0
- tests/core/environment/README.md +47 -0
- tests/core/environment/__init__.py +2 -0
- tests/core/environment/test_action_codec.py +50 -0
- tests/core/environment/test_game_state.py +483 -0
- tests/core/environment/test_grid_data.py +205 -0
- tests/core/environment/test_grid_logic.py +362 -0
- tests/core/environment/test_shape_logic.py +171 -0
- tests/core/environment/test_step.py +372 -0
- tests/core/structs/__init__.py +0 -0
- tests/core/structs/test_shape.py +83 -0
- tests/core/structs/test_triangle.py +97 -0
- tests/utils/__init__.py +0 -0
- tests/utils/test_geometry.py +93 -0
- trianglengin/__init__.py +18 -0
- trianglengin/app.py +110 -0
- trianglengin/cli.py +134 -0
- trianglengin/config/__init__.py +9 -0
- trianglengin/config/display_config.py +47 -0
- trianglengin/config/env_config.py +103 -0
- trianglengin/core/__init__.py +8 -0
- trianglengin/core/environment/__init__.py +31 -0
- trianglengin/core/environment/action_codec.py +37 -0
- trianglengin/core/environment/game_state.py +217 -0
- trianglengin/core/environment/grid/README.md +46 -0
- trianglengin/core/environment/grid/__init__.py +18 -0
- trianglengin/core/environment/grid/grid_data.py +140 -0
- trianglengin/core/environment/grid/line_cache.py +189 -0
- trianglengin/core/environment/grid/logic.py +131 -0
- trianglengin/core/environment/logic/__init__.py +3 -0
- trianglengin/core/environment/logic/actions.py +38 -0
- trianglengin/core/environment/logic/step.py +134 -0
- trianglengin/core/environment/shapes/__init__.py +19 -0
- trianglengin/core/environment/shapes/logic.py +84 -0
- trianglengin/core/environment/shapes/templates.py +587 -0
- trianglengin/core/structs/__init__.py +27 -0
- trianglengin/core/structs/constants.py +28 -0
- trianglengin/core/structs/shape.py +61 -0
- trianglengin/core/structs/triangle.py +48 -0
- trianglengin/interaction/README.md +45 -0
- trianglengin/interaction/__init__.py +17 -0
- trianglengin/interaction/debug_mode_handler.py +96 -0
- trianglengin/interaction/event_processor.py +43 -0
- trianglengin/interaction/input_handler.py +82 -0
- trianglengin/interaction/play_mode_handler.py +141 -0
- trianglengin/utils/__init__.py +9 -0
- trianglengin/utils/geometry.py +73 -0
- trianglengin/utils/types.py +10 -0
- trianglengin/visualization/README.md +44 -0
- trianglengin/visualization/__init__.py +61 -0
- trianglengin/visualization/core/README.md +52 -0
- trianglengin/visualization/core/__init__.py +12 -0
- trianglengin/visualization/core/colors.py +117 -0
- trianglengin/visualization/core/coord_mapper.py +73 -0
- trianglengin/visualization/core/fonts.py +55 -0
- trianglengin/visualization/core/layout.py +101 -0
- trianglengin/visualization/core/visualizer.py +232 -0
- trianglengin/visualization/drawing/README.md +45 -0
- trianglengin/visualization/drawing/__init__.py +30 -0
- trianglengin/visualization/drawing/grid.py +156 -0
- trianglengin/visualization/drawing/highlight.py +30 -0
- trianglengin/visualization/drawing/hud.py +39 -0
- trianglengin/visualization/drawing/previews.py +172 -0
- trianglengin/visualization/drawing/shapes.py +36 -0
- trianglengin-1.0.6.dist-info/METADATA +367 -0
- trianglengin-1.0.6.dist-info/RECORD +72 -0
- trianglengin-1.0.6.dist-info/WHEEL +5 -0
- trianglengin-1.0.6.dist-info/entry_points.txt +2 -0
- trianglengin-1.0.6.dist-info/licenses/LICENSE +22 -0
- trianglengin-1.0.6.dist-info/top_level.txt +2 -0
@@ -0,0 +1,362 @@
|
|
1
|
+
# File: tests/core/environment/test_grid_logic.py
|
2
|
+
import logging
|
3
|
+
|
4
|
+
import pytest
|
5
|
+
|
6
|
+
from trianglengin.config import EnvConfig
|
7
|
+
from trianglengin.core.environment.grid import GridData
|
8
|
+
from trianglengin.core.environment.grid import logic as GridLogic
|
9
|
+
from trianglengin.core.structs import Shape
|
10
|
+
|
11
|
+
# Default color for fixture shapes
|
12
|
+
DEFAULT_TEST_COLOR = (100, 100, 100)
|
13
|
+
log = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
@pytest.fixture
|
17
|
+
def default_config() -> EnvConfig:
|
18
|
+
"""Fixture for default environment configuration."""
|
19
|
+
return EnvConfig()
|
20
|
+
|
21
|
+
|
22
|
+
@pytest.fixture
|
23
|
+
def default_grid(default_config: EnvConfig) -> GridData:
|
24
|
+
"""Fixture for a default GridData instance."""
|
25
|
+
# Ensure the cache is populated for the default config before tests run
|
26
|
+
# This will now use the fixed _compute_lines_and_map_v4
|
27
|
+
GridData(default_config)
|
28
|
+
# Return a fresh instance for the test, which will reuse the cache
|
29
|
+
return GridData(default_config)
|
30
|
+
|
31
|
+
|
32
|
+
def occupy_coords(grid_data: GridData, coords: set[tuple[int, int]]):
|
33
|
+
"""Helper to occupy specific coordinates."""
|
34
|
+
for r, c in coords:
|
35
|
+
if grid_data.valid(r, c) and not grid_data.is_death(r, c):
|
36
|
+
grid_data._occupied_np[r, c] = True
|
37
|
+
# Assign a dummy color ID
|
38
|
+
grid_data._color_id_np[r, c] = 0
|
39
|
+
|
40
|
+
|
41
|
+
# --- Basic Placement Tests ---
|
42
|
+
# (Keep existing basic placement tests - they should still pass)
|
43
|
+
|
44
|
+
|
45
|
+
def test_can_place_basic(default_grid: GridData):
|
46
|
+
"""Test basic placement in empty grid."""
|
47
|
+
shape = Shape([(0, 0, False), (0, 1, True)], DEFAULT_TEST_COLOR) # D, U
|
48
|
+
start_r, start_c = -1, -1
|
49
|
+
for r in range(default_grid.rows):
|
50
|
+
for c in range(default_grid.cols):
|
51
|
+
if not default_grid.is_death(r, c) and (r + c) % 2 == 0: # Find Down cell
|
52
|
+
start_r, start_c = r, c
|
53
|
+
break
|
54
|
+
if start_r != -1:
|
55
|
+
break
|
56
|
+
if start_r == -1:
|
57
|
+
pytest.skip("Could not find a valid starting Down cell.")
|
58
|
+
assert GridLogic.can_place(default_grid, shape, start_r, start_c)
|
59
|
+
|
60
|
+
|
61
|
+
def test_can_place_occupied(default_grid: GridData):
|
62
|
+
"""Test placement fails if target is occupied."""
|
63
|
+
shape = Shape([(0, 0, False)], DEFAULT_TEST_COLOR) # Single Down
|
64
|
+
start_r, start_c = -1, -1
|
65
|
+
for r in range(default_grid.rows):
|
66
|
+
for c in range(default_grid.cols):
|
67
|
+
if not default_grid.is_death(r, c) and (r + c) % 2 == 0: # Find Down cell
|
68
|
+
start_r, start_c = r, c
|
69
|
+
break
|
70
|
+
if start_r != -1:
|
71
|
+
break
|
72
|
+
if start_r == -1:
|
73
|
+
pytest.skip("Could not find a valid starting Down cell.")
|
74
|
+
default_grid._occupied_np[start_r, start_c] = True
|
75
|
+
assert not GridLogic.can_place(default_grid, shape, start_r, start_c)
|
76
|
+
|
77
|
+
|
78
|
+
def test_can_place_death_zone(default_grid: GridData):
|
79
|
+
"""Test placement fails if target is in death zone."""
|
80
|
+
Shape([(0, 0, False)], DEFAULT_TEST_COLOR) # Single Down
|
81
|
+
death_r, death_c = -1, -1
|
82
|
+
for r in range(default_grid.rows):
|
83
|
+
for c in range(default_grid.cols):
|
84
|
+
if default_grid.is_death(r, c):
|
85
|
+
death_r, death_c = r, c
|
86
|
+
break
|
87
|
+
if death_r != -1:
|
88
|
+
break
|
89
|
+
if death_r == -1:
|
90
|
+
pytest.skip("Could not find a death zone cell.")
|
91
|
+
shape_to_use = Shape([(0, 0, (death_r + death_c) % 2 != 0)], DEFAULT_TEST_COLOR)
|
92
|
+
assert not GridLogic.can_place(default_grid, shape_to_use, death_r, death_c)
|
93
|
+
|
94
|
+
|
95
|
+
def test_can_place_orientation_mismatch(default_grid: GridData):
|
96
|
+
"""Test placement fails if shape orientation doesn't match grid."""
|
97
|
+
shape = Shape([(0, 0, True)], DEFAULT_TEST_COLOR) # Needs Up
|
98
|
+
start_r, start_c = -1, -1
|
99
|
+
for r in range(default_grid.rows):
|
100
|
+
for c in range(default_grid.cols):
|
101
|
+
if not default_grid.is_death(r, c) and (r + c) % 2 == 0: # Find Down cell
|
102
|
+
start_r, start_c = r, c
|
103
|
+
break
|
104
|
+
if start_r != -1:
|
105
|
+
break
|
106
|
+
if start_r == -1:
|
107
|
+
pytest.skip("Could not find a valid starting Down cell.")
|
108
|
+
assert not GridLogic.can_place(default_grid, shape, start_r, start_c)
|
109
|
+
|
110
|
+
shape_down = Shape([(0, 0, False)], DEFAULT_TEST_COLOR) # Needs Down
|
111
|
+
up_r, up_c = -1, -1
|
112
|
+
for r in range(default_grid.rows):
|
113
|
+
for c in range(default_grid.cols):
|
114
|
+
if not default_grid.is_death(r, c) and (r + c) % 2 != 0: # Find Up cell
|
115
|
+
up_r, up_c = r, c
|
116
|
+
break
|
117
|
+
if up_r != -1:
|
118
|
+
break
|
119
|
+
if up_r == -1:
|
120
|
+
pytest.skip("Could not find a valid starting Up cell.")
|
121
|
+
assert not GridLogic.can_place(default_grid, shape_down, up_r, up_c)
|
122
|
+
|
123
|
+
|
124
|
+
def test_can_place_out_of_bounds(default_grid: GridData):
|
125
|
+
"""Test placement fails if shape goes out of bounds."""
|
126
|
+
shape = Shape([(0, 0, False), (0, 1, True)], DEFAULT_TEST_COLOR)
|
127
|
+
assert not GridLogic.can_place(default_grid, shape, 0, default_grid.cols - 1)
|
128
|
+
shape_tall = Shape([(0, 0, False), (1, 0, False)], DEFAULT_TEST_COLOR)
|
129
|
+
assert not GridLogic.can_place(default_grid, shape_tall, default_grid.rows - 1, 0)
|
130
|
+
|
131
|
+
|
132
|
+
# --- Line Clearing Tests ---
|
133
|
+
|
134
|
+
|
135
|
+
def test_check_and_clear_lines_simple(default_grid: GridData):
|
136
|
+
"""Test clearing a single horizontal line."""
|
137
|
+
target_line = None
|
138
|
+
for line in default_grid._lines:
|
139
|
+
# Find a reasonably long horizontal line for a good test
|
140
|
+
if len(line) > 4 and all(c[0] == line[0][0] for c in line):
|
141
|
+
target_line = line
|
142
|
+
break
|
143
|
+
if not target_line:
|
144
|
+
pytest.skip("Could not find a suitable horizontal line.")
|
145
|
+
target_line_set = set(target_line)
|
146
|
+
occupy_coords(default_grid, target_line_set)
|
147
|
+
last_coord = target_line[-1]
|
148
|
+
lines_cleared, coords_cleared, cleared_line_sets = GridLogic.check_and_clear_lines(
|
149
|
+
default_grid, {last_coord}
|
150
|
+
)
|
151
|
+
assert lines_cleared == 1, f"Expected 1 line clear, got {lines_cleared}"
|
152
|
+
assert coords_cleared == target_line_set, "Cleared coordinates mismatch"
|
153
|
+
assert cleared_line_sets == {frozenset(target_line)}, "Cleared line set mismatch"
|
154
|
+
for r, c in target_line_set:
|
155
|
+
assert not default_grid._occupied_np[r, c], f"Cell ({r},{c}) was not cleared"
|
156
|
+
assert default_grid._color_id_np[r, c] == -1, (
|
157
|
+
f"Color ID for ({r},{c}) not reset"
|
158
|
+
)
|
159
|
+
|
160
|
+
|
161
|
+
def test_check_and_clear_lines_no_clear(default_grid: GridData):
|
162
|
+
"""Test that no lines are cleared if none are complete."""
|
163
|
+
target_line = None
|
164
|
+
for line in default_grid._lines:
|
165
|
+
# Find a reasonably long line
|
166
|
+
if len(line) > 4:
|
167
|
+
target_line = line
|
168
|
+
break
|
169
|
+
if not target_line:
|
170
|
+
pytest.skip("Could not find a suitable line.")
|
171
|
+
# Occupy all but the last coordinate
|
172
|
+
coords_to_occupy = set(target_line[:-1])
|
173
|
+
occupy_coords(default_grid, coords_to_occupy)
|
174
|
+
# Check clear using the last occupied coordinate
|
175
|
+
last_occupied_coord = target_line[-2] # The one before the empty one
|
176
|
+
lines_cleared, coords_cleared, cleared_line_sets = GridLogic.check_and_clear_lines(
|
177
|
+
default_grid, {last_occupied_coord}
|
178
|
+
)
|
179
|
+
assert lines_cleared == 0, f"Expected 0 lines cleared, got {lines_cleared}"
|
180
|
+
assert not coords_cleared, "Coords should not be cleared"
|
181
|
+
assert not cleared_line_sets, "Cleared line sets should be empty"
|
182
|
+
# Verify grid cells remain occupied
|
183
|
+
for r, c in coords_to_occupy:
|
184
|
+
assert default_grid._occupied_np[r, c], (
|
185
|
+
f"Cell ({r},{c}) should still be occupied"
|
186
|
+
)
|
187
|
+
|
188
|
+
|
189
|
+
# --- Specific Boundary Line Clearing Scenarios ---
|
190
|
+
|
191
|
+
# Define the boundary lines explicitly based on default EnvConfig
|
192
|
+
# IMPORTANT: Use the natural traversal order expected from line_cache v4
|
193
|
+
BOUNDARY_LINES = {
|
194
|
+
"top_left_diag": (
|
195
|
+
(4, 0),
|
196
|
+
(3, 0),
|
197
|
+
(3, 1),
|
198
|
+
(2, 1),
|
199
|
+
(2, 2),
|
200
|
+
(1, 2),
|
201
|
+
(1, 3),
|
202
|
+
(0, 3),
|
203
|
+
(0, 4),
|
204
|
+
),
|
205
|
+
"top_horiz": tuple((0, c) for c in range(3, 12)),
|
206
|
+
"top_right_diag": (
|
207
|
+
(0, 10),
|
208
|
+
(0, 11),
|
209
|
+
(1, 11),
|
210
|
+
(1, 12),
|
211
|
+
(2, 12),
|
212
|
+
(2, 13),
|
213
|
+
(3, 13),
|
214
|
+
(3, 14),
|
215
|
+
(4, 14),
|
216
|
+
),
|
217
|
+
"bottom_right_diag": (
|
218
|
+
(3, 14),
|
219
|
+
(4, 14),
|
220
|
+
(4, 13),
|
221
|
+
(5, 13),
|
222
|
+
(5, 12),
|
223
|
+
(6, 12),
|
224
|
+
(6, 11),
|
225
|
+
(7, 11),
|
226
|
+
(7, 10),
|
227
|
+
),
|
228
|
+
"bottom_horiz": tuple((7, c) for c in range(3, 12)),
|
229
|
+
"bottom_left_diag": (
|
230
|
+
(3, 0),
|
231
|
+
(4, 0),
|
232
|
+
(4, 1),
|
233
|
+
(5, 1),
|
234
|
+
(5, 2),
|
235
|
+
(6, 2),
|
236
|
+
(6, 3),
|
237
|
+
(7, 3),
|
238
|
+
(7, 4),
|
239
|
+
),
|
240
|
+
}
|
241
|
+
|
242
|
+
|
243
|
+
@pytest.mark.parametrize("line_name, line_coords", BOUNDARY_LINES.items())
|
244
|
+
def test_boundary_line_clears_only_when_full(
|
245
|
+
line_name: str, line_coords: tuple[tuple[int, int], ...], default_grid: GridData
|
246
|
+
):
|
247
|
+
"""
|
248
|
+
Tests that boundary lines clear only when the final middle piece(s) are placed,
|
249
|
+
simulating placing pieces from the outside inwards.
|
250
|
+
"""
|
251
|
+
grid_data = default_grid
|
252
|
+
grid_data.reset() # Start with a clean grid for each line test
|
253
|
+
n = len(line_coords)
|
254
|
+
line_set = set(line_coords)
|
255
|
+
line_fs = frozenset(line_coords)
|
256
|
+
log.info(f"Testing line '{line_name}' (len={n}): {line_coords}")
|
257
|
+
|
258
|
+
# Verify line exists in cache first (essential prerequisite)
|
259
|
+
first_coord = line_coords[0]
|
260
|
+
assert first_coord in grid_data._coord_to_lines_map, (
|
261
|
+
f"Coord {first_coord} not in map for line '{line_name}'"
|
262
|
+
)
|
263
|
+
found_mapping = False
|
264
|
+
if first_coord in grid_data._coord_to_lines_map:
|
265
|
+
for mapped_fs in grid_data._coord_to_lines_map[first_coord]:
|
266
|
+
if mapped_fs == line_fs:
|
267
|
+
found_mapping = True
|
268
|
+
break
|
269
|
+
assert found_mapping, (
|
270
|
+
f"Line {line_fs} not mapped to {first_coord} for line '{line_name}'"
|
271
|
+
)
|
272
|
+
|
273
|
+
# --- Simulate Inward Placement ---
|
274
|
+
placed_coords = set()
|
275
|
+
final_clear_occurred = False
|
276
|
+
for i in range(n // 2 + (n % 2)): # Iterate inwards (e.g., 0, 1, 2, 3, 4 for n=9)
|
277
|
+
coord1 = line_coords[i]
|
278
|
+
coord2 = line_coords[n - 1 - i]
|
279
|
+
is_last_pair_or_middle = i == (n // 2 + (n % 2) - 1)
|
280
|
+
log.debug(
|
281
|
+
f" Step {i}: Considering pair {coord1} / {coord2}. Is last: {is_last_pair_or_middle}"
|
282
|
+
)
|
283
|
+
|
284
|
+
# --- Place first cell of the pair ---
|
285
|
+
if coord1 not in placed_coords:
|
286
|
+
log.debug(f" Placing {coord1}...")
|
287
|
+
grid_data._occupied_np[coord1[0], coord1[1]] = True
|
288
|
+
grid_data._color_id_np[coord1[0], coord1[1]] = 0
|
289
|
+
placed_coords.add(coord1)
|
290
|
+
lines_cleared_1, coords_cleared_1, cleared_sets_1 = (
|
291
|
+
GridLogic.check_and_clear_lines(grid_data, {coord1})
|
292
|
+
)
|
293
|
+
|
294
|
+
should_clear_now = len(placed_coords) == n
|
295
|
+
if should_clear_now:
|
296
|
+
log.debug(f" Checking FINAL clear after placing {coord1}")
|
297
|
+
assert lines_cleared_1 == 1, (
|
298
|
+
f"Line '{line_name}' should clear after placing final piece {coord1}, but did not."
|
299
|
+
)
|
300
|
+
assert coords_cleared_1 == line_set, (
|
301
|
+
f"Cleared coords mismatch for '{line_name}' after final piece {coord1}."
|
302
|
+
)
|
303
|
+
assert cleared_sets_1 == {line_fs}, (
|
304
|
+
f"Cleared line set mismatch for '{line_name}' after final piece {coord1}."
|
305
|
+
)
|
306
|
+
final_clear_occurred = True
|
307
|
+
else:
|
308
|
+
log.debug(f" Checking NO clear after placing {coord1}")
|
309
|
+
assert lines_cleared_1 == 0, (
|
310
|
+
f"Line '{line_name}' cleared prematurely after placing {coord1} (step {i}, total placed: {len(placed_coords)}/{n})"
|
311
|
+
)
|
312
|
+
assert not coords_cleared_1, (
|
313
|
+
f"Coords cleared prematurely for '{line_name}' after {coord1}"
|
314
|
+
)
|
315
|
+
assert grid_data._occupied_np[coord1[0], coord1[1]], (
|
316
|
+
f"{coord1} should still be occupied after non-clearing step"
|
317
|
+
)
|
318
|
+
|
319
|
+
# --- Place second cell of the pair (if different from first) ---
|
320
|
+
if coord1 != coord2 and coord2 not in placed_coords:
|
321
|
+
log.debug(f" Placing {coord2}...")
|
322
|
+
grid_data._occupied_np[coord2[0], coord2[1]] = True
|
323
|
+
grid_data._color_id_np[coord2[0], coord2[1]] = 0
|
324
|
+
placed_coords.add(coord2)
|
325
|
+
lines_cleared_2, coords_cleared_2, cleared_sets_2 = (
|
326
|
+
GridLogic.check_and_clear_lines(grid_data, {coord2})
|
327
|
+
)
|
328
|
+
|
329
|
+
should_clear_now = len(placed_coords) == n
|
330
|
+
if should_clear_now:
|
331
|
+
log.debug(f" Checking FINAL clear after placing {coord2}")
|
332
|
+
assert lines_cleared_2 == 1, (
|
333
|
+
f"Line '{line_name}' should clear after placing final piece {coord2}, but did not."
|
334
|
+
)
|
335
|
+
assert coords_cleared_2 == line_set, (
|
336
|
+
f"Cleared coords mismatch for '{line_name}' after final piece {coord2}."
|
337
|
+
)
|
338
|
+
assert cleared_sets_2 == {line_fs}, (
|
339
|
+
f"Cleared line set mismatch for '{line_name}' after final piece {coord2}."
|
340
|
+
)
|
341
|
+
final_clear_occurred = True
|
342
|
+
else:
|
343
|
+
log.debug(f" Checking NO clear after placing {coord2}")
|
344
|
+
assert lines_cleared_2 == 0, (
|
345
|
+
f"Line '{line_name}' cleared prematurely after placing {coord2} (step {i}, total placed: {len(placed_coords)}/{n})"
|
346
|
+
)
|
347
|
+
assert not coords_cleared_2, (
|
348
|
+
f"Coords cleared prematurely for '{line_name}' after {coord2}"
|
349
|
+
)
|
350
|
+
assert grid_data._occupied_np[coord2[0], coord2[1]], (
|
351
|
+
f"{coord2} should still be occupied after non-clearing step"
|
352
|
+
)
|
353
|
+
|
354
|
+
# --- Final Verification ---
|
355
|
+
assert final_clear_occurred, (
|
356
|
+
f"The final clear event did not happen for line '{line_name}' after placing all {n} pieces."
|
357
|
+
)
|
358
|
+
# Check that all cells in the line are indeed clear *after* the loop finishes
|
359
|
+
for r_clr, c_clr in line_set:
|
360
|
+
assert not grid_data._occupied_np[r_clr, c_clr], (
|
361
|
+
f"Cell ({r_clr},{c_clr}) was not cleared for line '{line_name}' after test completion"
|
362
|
+
)
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# File: trianglengin/tests/core/environment/test_shape_logic.py
|
2
|
+
import random
|
3
|
+
|
4
|
+
import pytest
|
5
|
+
|
6
|
+
# Import directly from the library being tested
|
7
|
+
from trianglengin.core.environment import GameState
|
8
|
+
from trianglengin.core.environment.shapes import logic as ShapeLogic
|
9
|
+
from trianglengin.core.structs import Shape
|
10
|
+
|
11
|
+
# Use fixtures from the local conftest.py
|
12
|
+
# Fixtures are implicitly injected by pytest
|
13
|
+
|
14
|
+
|
15
|
+
def test_generate_random_shape(fixed_rng: random.Random):
|
16
|
+
"""Test generating a single random shape."""
|
17
|
+
shape = ShapeLogic.generate_random_shape(fixed_rng)
|
18
|
+
assert isinstance(shape, Shape)
|
19
|
+
assert shape.triangles is not None
|
20
|
+
assert shape.color is not None
|
21
|
+
assert len(shape.triangles) > 0
|
22
|
+
# Check connectivity (optional but good)
|
23
|
+
assert ShapeLogic.is_shape_connected(shape)
|
24
|
+
|
25
|
+
|
26
|
+
def test_generate_multiple_shapes(fixed_rng: random.Random):
|
27
|
+
"""Test generating multiple shapes to ensure variety (or lack thereof with fixed seed)."""
|
28
|
+
shape1 = ShapeLogic.generate_random_shape(fixed_rng)
|
29
|
+
# Re-seed or use different rng instance if true randomness is needed per call
|
30
|
+
# For this test, using the same fixed_rng will likely produce the same shape again
|
31
|
+
shape2 = ShapeLogic.generate_random_shape(fixed_rng)
|
32
|
+
# --- REMOVED INCORRECT ASSERTION ---
|
33
|
+
# assert shape1 == shape2 # Expect same shape due to fixed seed - THIS IS INCORRECT
|
34
|
+
# --- END REMOVED ---
|
35
|
+
# Check that subsequent calls produce different results with the same RNG instance
|
36
|
+
assert shape1 != shape2, (
|
37
|
+
"Two consecutive calls with the same RNG produced the exact same shape (template and color), which is highly unlikely."
|
38
|
+
)
|
39
|
+
|
40
|
+
# Use a different seed for variation
|
41
|
+
rng2 = random.Random(54321)
|
42
|
+
shape3 = ShapeLogic.generate_random_shape(rng2)
|
43
|
+
# Check that different RNGs produce different results (highly likely)
|
44
|
+
assert shape1 != shape3 or shape1.color != shape3.color
|
45
|
+
|
46
|
+
|
47
|
+
def test_refill_shape_slots_empty(game_state: GameState, fixed_rng: random.Random):
|
48
|
+
"""Test refilling when all slots are initially empty."""
|
49
|
+
game_state.shapes = [None] * game_state.env_config.NUM_SHAPE_SLOTS
|
50
|
+
ShapeLogic.refill_shape_slots(game_state, fixed_rng)
|
51
|
+
assert all(s is not None for s in game_state.shapes)
|
52
|
+
assert len(game_state.shapes) == game_state.env_config.NUM_SHAPE_SLOTS
|
53
|
+
|
54
|
+
|
55
|
+
def test_refill_shape_slots_partial(game_state: GameState, fixed_rng: random.Random):
|
56
|
+
"""Test refilling when some slots are empty - SHOULD NOT REFILL."""
|
57
|
+
num_slots = game_state.env_config.NUM_SHAPE_SLOTS
|
58
|
+
if num_slots < 2:
|
59
|
+
pytest.skip("Test requires at least 2 shape slots")
|
60
|
+
|
61
|
+
# Start with full slots
|
62
|
+
ShapeLogic.refill_shape_slots(game_state, fixed_rng)
|
63
|
+
assert all(s is not None for s in game_state.shapes)
|
64
|
+
|
65
|
+
# Empty one slot
|
66
|
+
game_state.shapes[0] = None
|
67
|
+
# Store original state (important: copy shapes if they are mutable)
|
68
|
+
original_shapes = [s.copy() if s else None for s in game_state.shapes]
|
69
|
+
|
70
|
+
# Attempt refill - it should do nothing
|
71
|
+
ShapeLogic.refill_shape_slots(game_state, fixed_rng)
|
72
|
+
|
73
|
+
# Check that shapes remain unchanged
|
74
|
+
assert game_state.shapes == original_shapes, "Refill happened unexpectedly"
|
75
|
+
|
76
|
+
|
77
|
+
def test_refill_shape_slots_full(game_state: GameState, fixed_rng: random.Random):
|
78
|
+
"""Test refilling when all slots are already full - SHOULD NOT REFILL."""
|
79
|
+
# Start with full slots
|
80
|
+
ShapeLogic.refill_shape_slots(game_state, fixed_rng)
|
81
|
+
assert all(s is not None for s in game_state.shapes)
|
82
|
+
original_shapes = [s.copy() if s else None for s in game_state.shapes]
|
83
|
+
|
84
|
+
# Attempt refill - should do nothing
|
85
|
+
ShapeLogic.refill_shape_slots(game_state, fixed_rng)
|
86
|
+
|
87
|
+
# Check shapes are unchanged
|
88
|
+
assert game_state.shapes == original_shapes, "Refill happened when slots were full"
|
89
|
+
|
90
|
+
|
91
|
+
def test_refill_shape_slots_batch_trigger(game_state: GameState) -> None:
|
92
|
+
"""Test that refill only happens when ALL slots are empty."""
|
93
|
+
num_slots = game_state.env_config.NUM_SHAPE_SLOTS
|
94
|
+
if num_slots < 2:
|
95
|
+
pytest.skip("Test requires at least 2 shape slots")
|
96
|
+
|
97
|
+
# Fill all slots initially
|
98
|
+
ShapeLogic.refill_shape_slots(game_state, game_state._rng)
|
99
|
+
initial_shapes = [s.copy() if s else None for s in game_state.shapes]
|
100
|
+
assert all(s is not None for s in initial_shapes)
|
101
|
+
|
102
|
+
# Empty one slot - refill should NOT happen
|
103
|
+
game_state.shapes[0] = None
|
104
|
+
shapes_after_one_empty = [s.copy() if s else None for s in game_state.shapes]
|
105
|
+
ShapeLogic.refill_shape_slots(game_state, game_state._rng)
|
106
|
+
assert game_state.shapes == shapes_after_one_empty, (
|
107
|
+
"Refill happened when only one slot was empty"
|
108
|
+
)
|
109
|
+
|
110
|
+
# Empty all slots - refill SHOULD happen
|
111
|
+
game_state.shapes = [None] * num_slots
|
112
|
+
ShapeLogic.refill_shape_slots(game_state, game_state._rng)
|
113
|
+
assert all(s is not None for s in game_state.shapes), (
|
114
|
+
"Refill did not happen when all slots were empty"
|
115
|
+
)
|
116
|
+
# Check that the shapes are different from the initial ones (probabilistically)
|
117
|
+
assert game_state.shapes != initial_shapes, (
|
118
|
+
"Shapes after refill are identical to initial shapes (unlikely)"
|
119
|
+
)
|
120
|
+
|
121
|
+
|
122
|
+
# --- ADDED TESTS ---
|
123
|
+
def test_get_neighbors():
|
124
|
+
"""Test neighbor calculation for up and down triangles."""
|
125
|
+
# Up triangle at (1, 1)
|
126
|
+
up_neighbors = ShapeLogic.get_neighbors(r=1, c=1, is_up=True)
|
127
|
+
# Expected: Left (1,0), Right (1,2), Vertical (Down) (2,1)
|
128
|
+
assert set(up_neighbors) == {(1, 0), (1, 2), (2, 1)}
|
129
|
+
|
130
|
+
# Down triangle at (1, 2)
|
131
|
+
down_neighbors = ShapeLogic.get_neighbors(r=1, c=2, is_up=False)
|
132
|
+
# Expected: Left (1,1), Right (1,3), Vertical (Up) (0,2)
|
133
|
+
assert set(down_neighbors) == {(1, 1), (1, 3), (0, 2)}
|
134
|
+
|
135
|
+
|
136
|
+
def test_is_shape_connected_true(simple_shape: Shape): # Use fixture
|
137
|
+
"""Test connectivity for various connected shapes."""
|
138
|
+
# Single triangle
|
139
|
+
shape1 = Shape([(0, 0, True)], (1, 1, 1))
|
140
|
+
assert ShapeLogic.is_shape_connected(shape1)
|
141
|
+
|
142
|
+
# Domino (horizontal) - Down(0,0) connects to Up(0,1)
|
143
|
+
shape2 = Shape([(0, 0, False), (0, 1, True)], (1, 1, 1))
|
144
|
+
assert ShapeLogic.is_shape_connected(shape2)
|
145
|
+
|
146
|
+
# L-shape (from simple_shape fixture) - Down(0,0) connects Up(1,0), Up(1,0) connects Down(1,1)
|
147
|
+
assert ShapeLogic.is_shape_connected(simple_shape) # Test the fixture directly
|
148
|
+
|
149
|
+
# Empty shape
|
150
|
+
shape4 = Shape([], (1, 1, 1))
|
151
|
+
assert ShapeLogic.is_shape_connected(shape4)
|
152
|
+
|
153
|
+
# More complex connected shape
|
154
|
+
shape5 = Shape(
|
155
|
+
[(0, 0, False), (0, 1, True), (1, 1, False), (1, 0, True)], (1, 1, 1)
|
156
|
+
)
|
157
|
+
assert ShapeLogic.is_shape_connected(shape5)
|
158
|
+
|
159
|
+
|
160
|
+
def test_is_shape_connected_false():
|
161
|
+
"""Test connectivity for disconnected shapes."""
|
162
|
+
# Two separate triangles
|
163
|
+
shape1 = Shape([(0, 0, True), (2, 2, False)], (1, 1, 1))
|
164
|
+
assert not ShapeLogic.is_shape_connected(shape1)
|
165
|
+
|
166
|
+
# Three triangles, two connected, one separate
|
167
|
+
shape2 = Shape([(0, 0, False), (0, 1, True), (3, 3, True)], (1, 1, 1))
|
168
|
+
assert not ShapeLogic.is_shape_connected(shape2)
|
169
|
+
|
170
|
+
|
171
|
+
# --- END ADDED TESTS ---
|