trianglengin 2.0.1__cp312-cp312-macosx_11_0_arm64.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.
- trianglengin/__init__.py +35 -0
- trianglengin/config/README.md +38 -0
- trianglengin/config/__init__.py +8 -0
- trianglengin/config/display_config.py +47 -0
- trianglengin/config/env_config.py +62 -0
- trianglengin/core/__init__.py +10 -0
- trianglengin/cpp/CMakeLists.txt +42 -0
- trianglengin/cpp/bindings.cpp +211 -0
- trianglengin/cpp/config.h +28 -0
- trianglengin/cpp/game_state.cpp +327 -0
- trianglengin/cpp/game_state.h +73 -0
- trianglengin/cpp/grid_data.cpp +239 -0
- trianglengin/cpp/grid_data.h +78 -0
- trianglengin/cpp/grid_logic.cpp +125 -0
- trianglengin/cpp/grid_logic.h +30 -0
- trianglengin/cpp/shape_logic.cpp +100 -0
- trianglengin/cpp/shape_logic.h +28 -0
- trianglengin/cpp/structs.h +40 -0
- trianglengin/game_interface.py +222 -0
- trianglengin/py.typed +0 -0
- trianglengin/trianglengin_cpp.cpython-312-darwin.so +0 -0
- trianglengin/ui/README.md +35 -0
- trianglengin/ui/__init__.py +21 -0
- trianglengin/ui/app.py +107 -0
- trianglengin/ui/cli.py +123 -0
- trianglengin/ui/config.py +44 -0
- trianglengin/ui/interaction/README.md +44 -0
- trianglengin/ui/interaction/__init__.py +19 -0
- trianglengin/ui/interaction/debug_mode_handler.py +72 -0
- trianglengin/ui/interaction/event_processor.py +49 -0
- trianglengin/ui/interaction/input_handler.py +89 -0
- trianglengin/ui/interaction/play_mode_handler.py +156 -0
- trianglengin/ui/visualization/README.md +42 -0
- trianglengin/ui/visualization/__init__.py +58 -0
- trianglengin/ui/visualization/core/README.md +51 -0
- trianglengin/ui/visualization/core/__init__.py +16 -0
- trianglengin/ui/visualization/core/colors.py +115 -0
- trianglengin/ui/visualization/core/coord_mapper.py +85 -0
- trianglengin/ui/visualization/core/fonts.py +65 -0
- trianglengin/ui/visualization/core/layout.py +77 -0
- trianglengin/ui/visualization/core/visualizer.py +248 -0
- trianglengin/ui/visualization/drawing/README.md +49 -0
- trianglengin/ui/visualization/drawing/__init__.py +43 -0
- trianglengin/ui/visualization/drawing/grid.py +213 -0
- trianglengin/ui/visualization/drawing/highlight.py +31 -0
- trianglengin/ui/visualization/drawing/hud.py +43 -0
- trianglengin/ui/visualization/drawing/previews.py +181 -0
- trianglengin/ui/visualization/drawing/shapes.py +46 -0
- trianglengin/ui/visualization/drawing/utils.py +23 -0
- trianglengin/utils/__init__.py +9 -0
- trianglengin/utils/geometry.py +62 -0
- trianglengin/utils/types.py +10 -0
- trianglengin-2.0.1.dist-info/METADATA +250 -0
- trianglengin-2.0.1.dist-info/RECORD +58 -0
- trianglengin-2.0.1.dist-info/WHEEL +5 -0
- trianglengin-2.0.1.dist-info/entry_points.txt +2 -0
- trianglengin-2.0.1.dist-info/licenses/LICENSE +22 -0
- trianglengin-2.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,248 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
import numpy as np
|
4
|
+
import pygame # Now a required dependency
|
5
|
+
|
6
|
+
# Use absolute imports for core components
|
7
|
+
from trianglengin.config import EnvConfig
|
8
|
+
from trianglengin.game_interface import GameState, Shape
|
9
|
+
|
10
|
+
# Use absolute imports within UI package where needed
|
11
|
+
from trianglengin.ui.config import DisplayConfig
|
12
|
+
|
13
|
+
# Import specific modules directly from the core package
|
14
|
+
from trianglengin.ui.visualization.core import colors, coord_mapper
|
15
|
+
from trianglengin.ui.visualization.core import (
|
16
|
+
layout as layout_module,
|
17
|
+
) # Import layout directly
|
18
|
+
|
19
|
+
# Import drawing functions using absolute paths
|
20
|
+
from trianglengin.ui.visualization.drawing import grid as grid_drawing
|
21
|
+
from trianglengin.ui.visualization.drawing import highlight as highlight_drawing
|
22
|
+
from trianglengin.ui.visualization.drawing import hud as hud_drawing
|
23
|
+
from trianglengin.ui.visualization.drawing import previews as preview_drawing
|
24
|
+
from trianglengin.ui.visualization.drawing.previews import (
|
25
|
+
draw_floating_preview,
|
26
|
+
draw_placement_preview,
|
27
|
+
)
|
28
|
+
|
29
|
+
logger = logging.getLogger(__name__)
|
30
|
+
|
31
|
+
|
32
|
+
class Visualizer:
|
33
|
+
"""
|
34
|
+
Orchestrates rendering of a single game state for interactive modes.
|
35
|
+
Uses the GameState wrapper to access C++ state.
|
36
|
+
Receives interaction state (hover, selection) via render parameters.
|
37
|
+
"""
|
38
|
+
|
39
|
+
def __init__(
|
40
|
+
self,
|
41
|
+
screen: pygame.Surface,
|
42
|
+
display_config: DisplayConfig, # From UI config
|
43
|
+
env_config: EnvConfig, # From core config
|
44
|
+
fonts: dict[str, pygame.font.Font | None],
|
45
|
+
) -> None:
|
46
|
+
self.screen = screen
|
47
|
+
self.display_config = display_config
|
48
|
+
self.env_config = env_config
|
49
|
+
self.fonts = fonts
|
50
|
+
self.layout_rects: dict[str, pygame.Rect] | None = None
|
51
|
+
self.preview_rects: dict[int, pygame.Rect] = {}
|
52
|
+
self._layout_calculated_for_size: tuple[int, int] = (0, 0)
|
53
|
+
self.ensure_layout()
|
54
|
+
|
55
|
+
def ensure_layout(self) -> dict[str, pygame.Rect]:
|
56
|
+
"""Returns cached layout or calculates it if needed."""
|
57
|
+
current_w, current_h = self.screen.get_size()
|
58
|
+
current_size = (current_w, current_h)
|
59
|
+
|
60
|
+
if (
|
61
|
+
self.layout_rects is None
|
62
|
+
or self._layout_calculated_for_size != current_size
|
63
|
+
):
|
64
|
+
# Call the function via the imported module
|
65
|
+
self.layout_rects = layout_module.calculate_interactive_layout(
|
66
|
+
current_w,
|
67
|
+
current_h,
|
68
|
+
self.display_config,
|
69
|
+
)
|
70
|
+
self._layout_calculated_for_size = current_size
|
71
|
+
logger.info(
|
72
|
+
f"Recalculated interactive layout for size {current_size}: {self.layout_rects}"
|
73
|
+
)
|
74
|
+
self.preview_rects = {} # Clear preview rect cache on layout change
|
75
|
+
|
76
|
+
return self.layout_rects if self.layout_rects is not None else {}
|
77
|
+
|
78
|
+
def render(
|
79
|
+
self,
|
80
|
+
game_state: GameState, # Core GameState
|
81
|
+
mode: str,
|
82
|
+
selected_shape_idx: int = -1,
|
83
|
+
hover_shape: Shape | None = None, # Core Shape
|
84
|
+
hover_grid_coord: tuple[int, int] | None = None,
|
85
|
+
hover_is_valid: bool = False,
|
86
|
+
hover_screen_pos: tuple[int, int] | None = None,
|
87
|
+
debug_highlight_coord: tuple[int, int] | None = None,
|
88
|
+
) -> None:
|
89
|
+
"""Renders the entire game visualization for interactive modes."""
|
90
|
+
self.screen.fill(colors.GRID_BG_DEFAULT)
|
91
|
+
layout_rects = self.ensure_layout()
|
92
|
+
grid_rect = layout_rects.get("grid")
|
93
|
+
preview_rect = layout_rects.get("preview")
|
94
|
+
|
95
|
+
grid_data_np = game_state.get_grid_data_np()
|
96
|
+
|
97
|
+
if grid_rect and grid_rect.width > 0 and grid_rect.height > 0:
|
98
|
+
try:
|
99
|
+
grid_surf = self.screen.subsurface(grid_rect)
|
100
|
+
cw, ch, ox, oy = coord_mapper._calculate_render_params(
|
101
|
+
grid_rect.width, grid_rect.height, self.env_config
|
102
|
+
)
|
103
|
+
self._render_grid_area(
|
104
|
+
grid_surf,
|
105
|
+
game_state,
|
106
|
+
grid_data_np,
|
107
|
+
mode,
|
108
|
+
grid_rect,
|
109
|
+
hover_shape,
|
110
|
+
hover_grid_coord,
|
111
|
+
hover_is_valid,
|
112
|
+
hover_screen_pos,
|
113
|
+
debug_highlight_coord,
|
114
|
+
cw,
|
115
|
+
ch,
|
116
|
+
ox,
|
117
|
+
oy,
|
118
|
+
)
|
119
|
+
except ValueError as e:
|
120
|
+
logger.error(f"Error creating grid subsurface ({grid_rect}): {e}")
|
121
|
+
pygame.draw.rect(self.screen, colors.RED, grid_rect, 1)
|
122
|
+
|
123
|
+
if preview_rect and preview_rect.width > 0 and preview_rect.height > 0:
|
124
|
+
try:
|
125
|
+
preview_surf = self.screen.subsurface(preview_rect)
|
126
|
+
self._render_preview_area(
|
127
|
+
preview_surf, game_state, mode, preview_rect, selected_shape_idx
|
128
|
+
)
|
129
|
+
except ValueError as e:
|
130
|
+
logger.error(f"Error creating preview subsurface ({preview_rect}): {e}")
|
131
|
+
pygame.draw.rect(self.screen, colors.RED, preview_rect, 1)
|
132
|
+
|
133
|
+
hud_drawing.render_hud(
|
134
|
+
surface=self.screen,
|
135
|
+
mode=mode,
|
136
|
+
fonts=self.fonts,
|
137
|
+
)
|
138
|
+
|
139
|
+
def _render_grid_area(
|
140
|
+
self,
|
141
|
+
grid_surf: pygame.Surface,
|
142
|
+
game_state: GameState,
|
143
|
+
grid_data_np: dict[str, np.ndarray],
|
144
|
+
mode: str,
|
145
|
+
grid_rect: pygame.Rect,
|
146
|
+
hover_shape: Shape | None,
|
147
|
+
hover_grid_coord: tuple[int, int] | None,
|
148
|
+
hover_is_valid: bool,
|
149
|
+
hover_screen_pos: tuple[int, int] | None,
|
150
|
+
debug_highlight_coord: tuple[int, int] | None,
|
151
|
+
cw: float,
|
152
|
+
ch: float,
|
153
|
+
ox: float,
|
154
|
+
oy: float,
|
155
|
+
) -> None:
|
156
|
+
"""Renders the main game grid and overlays onto the provided grid_surf."""
|
157
|
+
grid_drawing.draw_grid_background(
|
158
|
+
grid_surf,
|
159
|
+
self.env_config,
|
160
|
+
self.display_config,
|
161
|
+
cw,
|
162
|
+
ch,
|
163
|
+
ox,
|
164
|
+
oy,
|
165
|
+
game_state.is_over(),
|
166
|
+
mode == "debug",
|
167
|
+
death_mask_np=grid_data_np["death"],
|
168
|
+
)
|
169
|
+
|
170
|
+
grid_drawing.draw_grid_state(
|
171
|
+
grid_surf,
|
172
|
+
occupied_np=grid_data_np["occupied"],
|
173
|
+
color_id_np=grid_data_np["color_id"],
|
174
|
+
death_np=grid_data_np["death"],
|
175
|
+
rows=self.env_config.ROWS,
|
176
|
+
cols=self.env_config.COLS,
|
177
|
+
cw=cw,
|
178
|
+
ch=ch,
|
179
|
+
ox=ox,
|
180
|
+
oy=oy,
|
181
|
+
)
|
182
|
+
|
183
|
+
if mode == "play" and hover_shape:
|
184
|
+
if hover_grid_coord:
|
185
|
+
draw_placement_preview(
|
186
|
+
grid_surf,
|
187
|
+
hover_shape,
|
188
|
+
hover_grid_coord[0],
|
189
|
+
hover_grid_coord[1],
|
190
|
+
is_valid=hover_is_valid,
|
191
|
+
cw=cw,
|
192
|
+
ch=ch,
|
193
|
+
ox=ox,
|
194
|
+
oy=oy,
|
195
|
+
)
|
196
|
+
elif hover_screen_pos:
|
197
|
+
local_hover_pos = (
|
198
|
+
hover_screen_pos[0] - grid_rect.left,
|
199
|
+
hover_screen_pos[1] - grid_rect.top,
|
200
|
+
)
|
201
|
+
if grid_surf.get_rect().collidepoint(local_hover_pos):
|
202
|
+
draw_floating_preview(
|
203
|
+
grid_surf,
|
204
|
+
hover_shape,
|
205
|
+
local_hover_pos,
|
206
|
+
)
|
207
|
+
|
208
|
+
if mode == "debug" and debug_highlight_coord:
|
209
|
+
r, c = debug_highlight_coord
|
210
|
+
highlight_drawing.draw_debug_highlight(
|
211
|
+
grid_surf,
|
212
|
+
r,
|
213
|
+
c,
|
214
|
+
cw=cw,
|
215
|
+
ch=ch,
|
216
|
+
ox=ox,
|
217
|
+
oy=oy,
|
218
|
+
)
|
219
|
+
|
220
|
+
score_font = self.fonts.get("score")
|
221
|
+
if score_font:
|
222
|
+
score_text = f"Score: {game_state.game_score():.0f}"
|
223
|
+
score_surf = score_font.render(score_text, True, colors.YELLOW)
|
224
|
+
score_rect = score_surf.get_rect(topleft=(5, 5))
|
225
|
+
grid_surf.blit(score_surf, score_rect)
|
226
|
+
|
227
|
+
def _render_preview_area(
|
228
|
+
self,
|
229
|
+
preview_surf: pygame.Surface,
|
230
|
+
game_state: GameState,
|
231
|
+
mode: str,
|
232
|
+
preview_rect: pygame.Rect,
|
233
|
+
selected_shape_idx: int,
|
234
|
+
) -> None:
|
235
|
+
"""Renders the shape preview slots onto preview_surf and caches rects."""
|
236
|
+
current_shapes = game_state.get_shapes()
|
237
|
+
current_preview_rects = preview_drawing.render_previews(
|
238
|
+
preview_surf,
|
239
|
+
current_shapes,
|
240
|
+
preview_rect.topleft,
|
241
|
+
mode,
|
242
|
+
self.env_config,
|
243
|
+
self.display_config,
|
244
|
+
selected_shape_idx=selected_shape_idx,
|
245
|
+
)
|
246
|
+
# Cache the absolute screen coordinates of the preview slots
|
247
|
+
if not self.preview_rects or self.preview_rects != current_preview_rects:
|
248
|
+
self.preview_rects = current_preview_rects
|
@@ -0,0 +1,49 @@
|
|
1
|
+
|
2
|
+
# UI Visualization Drawing Submodule (`trianglengin.ui.visualization.drawing`)
|
3
|
+
|
4
|
+
**Requires:** `pygame`
|
5
|
+
|
6
|
+
## Purpose and Architecture
|
7
|
+
|
8
|
+
This submodule contains specialized functions responsible for drawing specific visual elements of the game onto Pygame surfaces for the **interactive modes** within the `trianglengin.ui` package. These functions are typically called by the core renderer (`Visualizer`) in [`trianglengin.ui.visualization.core`](../core/README.md).
|
9
|
+
|
10
|
+
- **[`grid.py`](grid.py):** Functions for drawing the grid background (`draw_grid_background`), the individual triangles within it colored based on occupancy/emptiness (`draw_grid_state`), and optional debug overlays (`draw_debug_grid_overlay`).
|
11
|
+
- **[`shapes.py`](shapes.py):** Contains `draw_shape`, a function to render a given `Shape` object at a specific location on a surface (used primarily for previews).
|
12
|
+
- **[`previews.py`](previews.py):** Handles rendering related to shape previews:
|
13
|
+
- `render_previews`: Draws the dedicated preview area, including borders and the shapes within their slots, handling selection highlights.
|
14
|
+
- `draw_placement_preview`: Draws a semi-transparent version of a shape snapped to the grid, indicating a potential placement location (used in play mode hover).
|
15
|
+
- `draw_floating_preview`: Draws a semi-transparent shape directly under the mouse cursor when hovering over the grid but not snapped (used in play mode hover).
|
16
|
+
- **[`hud.py`](hud.py):** `render_hud` draws Heads-Up Display elements like help text onto the main screen surface (simplified for interactive modes).
|
17
|
+
- **[`highlight.py`](highlight.py):** `draw_debug_highlight` draws a distinct border around a specific triangle, used for visual feedback in debug mode.
|
18
|
+
- **[`utils.py`](utils.py):** Helper functions, like `get_triangle_points`, used by other drawing functions.
|
19
|
+
|
20
|
+
## Exposed Interfaces
|
21
|
+
|
22
|
+
- **Grid Drawing:**
|
23
|
+
- `draw_grid_background(...)`
|
24
|
+
- `draw_grid_state(...)`
|
25
|
+
- `draw_debug_grid_overlay(...)`
|
26
|
+
- **Shape Drawing:**
|
27
|
+
- `draw_shape(...)`
|
28
|
+
- **Preview Drawing:**
|
29
|
+
- `render_previews(...) -> Dict[int, pygame.Rect]`
|
30
|
+
- `draw_placement_preview(...)`
|
31
|
+
- `draw_floating_preview(...)`
|
32
|
+
- **HUD Drawing:**
|
33
|
+
- `render_hud(...)`
|
34
|
+
- **Highlight Drawing:**
|
35
|
+
- `draw_debug_highlight(...)`
|
36
|
+
- **Utilities:**
|
37
|
+
- `get_triangle_points(...) -> list[tuple[float, float]]`
|
38
|
+
|
39
|
+
## Dependencies
|
40
|
+
|
41
|
+
- **`trianglengin` (core):** `EnvConfig`, `GameState`, `Shape`.
|
42
|
+
- **`trianglengin.ui.config`**: `DisplayConfig`.
|
43
|
+
- **`trianglengin.ui.visualization.core`**: `colors`, `coord_mapper`.
|
44
|
+
- **`pygame`**: The core library used for all drawing operations.
|
45
|
+
- **Standard Libraries:** `typing`, `logging`, `math`.
|
46
|
+
|
47
|
+
---
|
48
|
+
|
49
|
+
**Note:** Please keep this README updated when adding new drawing functions, modifying existing ones, or changing their dependencies.
|
@@ -0,0 +1,43 @@
|
|
1
|
+
"""Drawing functions for specific visual elements."""
|
2
|
+
|
3
|
+
# Guard UI imports
|
4
|
+
try:
|
5
|
+
# Import the new utility function
|
6
|
+
from .grid import (
|
7
|
+
draw_debug_grid_overlay,
|
8
|
+
draw_grid_background,
|
9
|
+
draw_grid_state,
|
10
|
+
)
|
11
|
+
from .highlight import draw_debug_highlight
|
12
|
+
from .hud import render_hud
|
13
|
+
from .previews import (
|
14
|
+
draw_floating_preview,
|
15
|
+
draw_placement_preview,
|
16
|
+
render_previews,
|
17
|
+
)
|
18
|
+
from .shapes import draw_shape
|
19
|
+
from .utils import get_triangle_points
|
20
|
+
|
21
|
+
__all__ = [
|
22
|
+
"draw_grid_background",
|
23
|
+
"draw_grid_state",
|
24
|
+
"draw_debug_grid_overlay",
|
25
|
+
"draw_shape",
|
26
|
+
"render_previews",
|
27
|
+
"draw_placement_preview",
|
28
|
+
"draw_floating_preview",
|
29
|
+
"render_hud",
|
30
|
+
"draw_debug_highlight",
|
31
|
+
"get_triangle_points", # Export the helper
|
32
|
+
]
|
33
|
+
|
34
|
+
except ImportError as e:
|
35
|
+
import warnings
|
36
|
+
|
37
|
+
warnings.warn(
|
38
|
+
f"Could not import UI drawing components ({e}). "
|
39
|
+
"Ensure 'pygame' is installed (`pip install trianglengin[ui]`).",
|
40
|
+
ImportWarning,
|
41
|
+
stacklevel=2,
|
42
|
+
)
|
43
|
+
__all__ = []
|
@@ -0,0 +1,213 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
import numpy as np
|
4
|
+
|
5
|
+
# Guard UI imports
|
6
|
+
try:
|
7
|
+
import pygame
|
8
|
+
|
9
|
+
from ..core import colors # Relative import
|
10
|
+
from .utils import get_triangle_points # Relative import
|
11
|
+
except ImportError as e:
|
12
|
+
raise ImportError(
|
13
|
+
"UI components require 'pygame'. Install with 'pip install trianglengin[ui]'."
|
14
|
+
) from e
|
15
|
+
|
16
|
+
# Use absolute imports for core components
|
17
|
+
from trianglengin.config import EnvConfig
|
18
|
+
|
19
|
+
# Import DisplayConfig from the correct UI location
|
20
|
+
from trianglengin.ui.config import DisplayConfig
|
21
|
+
|
22
|
+
log = logging.getLogger(__name__)
|
23
|
+
|
24
|
+
|
25
|
+
def draw_grid_background(
|
26
|
+
surface: pygame.Surface,
|
27
|
+
env_config: EnvConfig, # Core config
|
28
|
+
display_config: DisplayConfig, # UI config
|
29
|
+
cw: float,
|
30
|
+
ch: float,
|
31
|
+
ox: float,
|
32
|
+
oy: float,
|
33
|
+
game_over: bool = False,
|
34
|
+
debug_mode: bool = False,
|
35
|
+
death_mask_np: np.ndarray | None = None,
|
36
|
+
) -> None:
|
37
|
+
"""Draws the background grid structure using pre-calculated render parameters."""
|
38
|
+
bg_color = colors.GRID_BG_GAME_OVER if game_over else colors.GRID_BG_DEFAULT
|
39
|
+
surface.fill(bg_color)
|
40
|
+
|
41
|
+
if cw <= 0 or ch <= 0:
|
42
|
+
log.warning("Cannot draw grid background with zero cell dimensions.")
|
43
|
+
return
|
44
|
+
|
45
|
+
rows, cols = env_config.ROWS, env_config.COLS
|
46
|
+
if death_mask_np is None or death_mask_np.shape != (rows, cols):
|
47
|
+
log.warning(
|
48
|
+
"Death mask not provided or shape mismatch, cannot draw death zones accurately."
|
49
|
+
)
|
50
|
+
# Attempt to reconstruct death mask from config if not provided
|
51
|
+
death_mask_np = np.full((rows, cols), True, dtype=bool)
|
52
|
+
for r_idx in range(rows):
|
53
|
+
if r_idx < len(env_config.PLAYABLE_RANGE_PER_ROW):
|
54
|
+
start_c, end_c = env_config.PLAYABLE_RANGE_PER_ROW[r_idx]
|
55
|
+
if start_c < end_c:
|
56
|
+
death_mask_np[r_idx, start_c:end_c] = False
|
57
|
+
else:
|
58
|
+
log.warning(f"Missing playable range definition for row {r_idx}")
|
59
|
+
|
60
|
+
for r in range(rows):
|
61
|
+
for c in range(cols):
|
62
|
+
is_up = (r + c) % 2 != 0
|
63
|
+
is_death = death_mask_np[r, c]
|
64
|
+
|
65
|
+
if is_death:
|
66
|
+
cell_color = colors.DEATH_ZONE_COLOR
|
67
|
+
else:
|
68
|
+
# Alternate background color based on checkerboard pattern
|
69
|
+
cell_color = (
|
70
|
+
colors.GRID_BG_LIGHT if (r % 2 == c % 2) else colors.GRID_BG_DARK
|
71
|
+
)
|
72
|
+
|
73
|
+
points = get_triangle_points(r, c, is_up, ox, oy, cw, ch)
|
74
|
+
pygame.draw.polygon(surface, cell_color, points)
|
75
|
+
pygame.draw.polygon(surface, colors.GRID_LINE_COLOR, points, 1)
|
76
|
+
|
77
|
+
# Draw debug coordinates if in debug mode and font is available
|
78
|
+
if debug_mode:
|
79
|
+
debug_font = display_config.DEBUG_FONT
|
80
|
+
if debug_font:
|
81
|
+
text = f"{r},{c}"
|
82
|
+
text_surf = debug_font.render(text, True, colors.DEBUG_TOGGLE_COLOR)
|
83
|
+
center_x = sum(p[0] for p in points) / 3
|
84
|
+
center_y = sum(p[1] for p in points) / 3
|
85
|
+
text_rect = text_surf.get_rect(center=(center_x, center_y))
|
86
|
+
surface.blit(text_surf, text_rect)
|
87
|
+
|
88
|
+
|
89
|
+
def draw_grid_state(
|
90
|
+
surface: pygame.Surface,
|
91
|
+
occupied_np: np.ndarray,
|
92
|
+
color_id_np: np.ndarray,
|
93
|
+
death_np: np.ndarray,
|
94
|
+
rows: int,
|
95
|
+
cols: int,
|
96
|
+
cw: float,
|
97
|
+
ch: float,
|
98
|
+
ox: float,
|
99
|
+
oy: float,
|
100
|
+
) -> None:
|
101
|
+
"""Draws the occupied triangles with their colors using pre-calculated parameters."""
|
102
|
+
if cw <= 0 or ch <= 0:
|
103
|
+
log.warning("Cannot draw grid state with zero cell dimensions.")
|
104
|
+
return
|
105
|
+
|
106
|
+
if (
|
107
|
+
occupied_np.shape != (rows, cols)
|
108
|
+
or color_id_np.shape != (rows, cols)
|
109
|
+
or death_np.shape != (rows, cols)
|
110
|
+
):
|
111
|
+
log.error("Grid state array shape mismatch.")
|
112
|
+
return
|
113
|
+
|
114
|
+
for r in range(rows):
|
115
|
+
for c in range(cols):
|
116
|
+
if death_np[r, c]:
|
117
|
+
continue # Skip death zones
|
118
|
+
|
119
|
+
if occupied_np[r, c]:
|
120
|
+
color_id = int(color_id_np[r, c])
|
121
|
+
color = colors.ID_TO_COLOR_MAP.get(color_id)
|
122
|
+
is_up = (r + c) % 2 != 0
|
123
|
+
points = get_triangle_points(r, c, is_up, ox, oy, cw, ch)
|
124
|
+
|
125
|
+
if color is not None:
|
126
|
+
pygame.draw.polygon(surface, color, points)
|
127
|
+
elif color_id == colors.DEBUG_COLOR_ID:
|
128
|
+
# Draw debug-toggled cells with a specific color
|
129
|
+
pygame.draw.polygon(surface, colors.DEBUG_TOGGLE_COLOR, points)
|
130
|
+
else:
|
131
|
+
# Fallback for unexpected color IDs
|
132
|
+
log.warning(
|
133
|
+
f"Occupied cell ({r},{c}) has invalid color ID: {color_id}"
|
134
|
+
)
|
135
|
+
pygame.draw.polygon(surface, colors.TRIANGLE_EMPTY_COLOR, points)
|
136
|
+
|
137
|
+
|
138
|
+
def draw_debug_grid_overlay(
|
139
|
+
surface: pygame.Surface,
|
140
|
+
display_config: DisplayConfig, # UI config
|
141
|
+
rows: int,
|
142
|
+
cols: int,
|
143
|
+
cw: float,
|
144
|
+
ch: float,
|
145
|
+
ox: float,
|
146
|
+
oy: float,
|
147
|
+
) -> None:
|
148
|
+
"""Draws debug information (coordinates) using pre-calculated render parameters."""
|
149
|
+
font = display_config.DEBUG_FONT
|
150
|
+
if not font:
|
151
|
+
log.warning("Debug font not available for overlay.")
|
152
|
+
return
|
153
|
+
if cw <= 0 or ch <= 0:
|
154
|
+
log.warning("Cannot draw debug overlay with zero cell dimensions.")
|
155
|
+
return
|
156
|
+
|
157
|
+
for r in range(rows):
|
158
|
+
for c in range(cols):
|
159
|
+
is_up = (r + c) % 2 != 0
|
160
|
+
points = get_triangle_points(r, c, is_up, ox, oy, cw, ch)
|
161
|
+
text = f"{r},{c}"
|
162
|
+
text_surf = font.render(text, True, colors.DEBUG_TOGGLE_COLOR)
|
163
|
+
center_x = sum(p[0] for p in points) / 3
|
164
|
+
center_y = sum(p[1] for p in points) / 3
|
165
|
+
text_rect = text_surf.get_rect(center=(center_x, center_y))
|
166
|
+
surface.blit(text_surf, text_rect)
|
167
|
+
|
168
|
+
|
169
|
+
def draw_grid(
|
170
|
+
surface: pygame.Surface,
|
171
|
+
grid_data_np: dict[str, np.ndarray],
|
172
|
+
env_config: EnvConfig, # Core config
|
173
|
+
display_config: DisplayConfig, # UI config
|
174
|
+
cw: float,
|
175
|
+
ch: float,
|
176
|
+
ox: float,
|
177
|
+
oy: float,
|
178
|
+
game_over: bool = False,
|
179
|
+
debug_mode: bool = False,
|
180
|
+
) -> None:
|
181
|
+
"""Main function to draw the entire grid including background and state."""
|
182
|
+
draw_grid_background(
|
183
|
+
surface,
|
184
|
+
env_config,
|
185
|
+
display_config,
|
186
|
+
cw,
|
187
|
+
ch,
|
188
|
+
ox,
|
189
|
+
oy,
|
190
|
+
game_over,
|
191
|
+
debug_mode,
|
192
|
+
grid_data_np["death"],
|
193
|
+
)
|
194
|
+
draw_grid_state(
|
195
|
+
surface,
|
196
|
+
grid_data_np["occupied"],
|
197
|
+
grid_data_np["color_id"],
|
198
|
+
grid_data_np["death"],
|
199
|
+
env_config.ROWS,
|
200
|
+
env_config.COLS,
|
201
|
+
cw,
|
202
|
+
ch,
|
203
|
+
ox,
|
204
|
+
oy,
|
205
|
+
)
|
206
|
+
# Overlay debug coordinates only if in debug mode
|
207
|
+
# Note: draw_grid_background already handles drawing coords if debug_mode=True
|
208
|
+
# This function might be redundant if the background function does it.
|
209
|
+
# Keeping it separate allows potentially different debug info later.
|
210
|
+
# if debug_mode:
|
211
|
+
# draw_debug_grid_overlay(
|
212
|
+
# surface, display_config, env_config.ROWS, env_config.COLS, cw, ch, ox, oy
|
213
|
+
# )
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Guard UI imports
|
2
|
+
try:
|
3
|
+
import pygame
|
4
|
+
|
5
|
+
from ..core import colors # Relative import
|
6
|
+
from .utils import get_triangle_points # Relative import
|
7
|
+
except ImportError as e:
|
8
|
+
raise ImportError(
|
9
|
+
"UI components require 'pygame'. Install with 'pip install trianglengin[ui]'."
|
10
|
+
) from e
|
11
|
+
|
12
|
+
|
13
|
+
def draw_debug_highlight(
|
14
|
+
surface: pygame.Surface,
|
15
|
+
r: int,
|
16
|
+
c: int,
|
17
|
+
cw: float,
|
18
|
+
ch: float,
|
19
|
+
ox: float,
|
20
|
+
oy: float,
|
21
|
+
) -> None:
|
22
|
+
"""Highlights a specific triangle border for debugging using pre-calculated parameters."""
|
23
|
+
if surface.get_width() <= 0 or surface.get_height() <= 0:
|
24
|
+
return
|
25
|
+
if cw <= 0 or ch <= 0:
|
26
|
+
return
|
27
|
+
|
28
|
+
is_up = (r + c) % 2 != 0
|
29
|
+
# Use helper
|
30
|
+
pts = get_triangle_points(r, c, is_up, ox, oy, cw, ch)
|
31
|
+
pygame.draw.polygon(surface, colors.DEBUG_TOGGLE_COLOR, pts, 3) # Draw border
|
@@ -0,0 +1,43 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
# Guard UI imports
|
4
|
+
try:
|
5
|
+
import pygame
|
6
|
+
|
7
|
+
from ..core import colors # Relative import
|
8
|
+
except ImportError as e:
|
9
|
+
raise ImportError(
|
10
|
+
"UI components require 'pygame'. Install with 'pip install trianglengin[ui]'."
|
11
|
+
) from e
|
12
|
+
|
13
|
+
|
14
|
+
def render_hud(
|
15
|
+
surface: pygame.Surface,
|
16
|
+
mode: str,
|
17
|
+
fonts: dict[str, pygame.font.Font | None],
|
18
|
+
_display_stats: dict[str, Any] | None = None, # Prefix with underscore
|
19
|
+
) -> None:
|
20
|
+
"""
|
21
|
+
Renders HUD elements for interactive modes (play/debug).
|
22
|
+
Displays only help text relevant to the mode.
|
23
|
+
Ignores _display_stats.
|
24
|
+
"""
|
25
|
+
screen_w, screen_h = surface.get_size()
|
26
|
+
help_font = fonts.get("help")
|
27
|
+
|
28
|
+
if not help_font:
|
29
|
+
return
|
30
|
+
|
31
|
+
bottom_y = screen_h - 10 # Position from bottom
|
32
|
+
|
33
|
+
# --- Render Help Text Only ---
|
34
|
+
help_text = "[ESC] Quit"
|
35
|
+
if mode == "play":
|
36
|
+
help_text += " | [Click] Select/Place Shape"
|
37
|
+
elif mode == "debug":
|
38
|
+
help_text += " | [Click] Toggle Cell"
|
39
|
+
|
40
|
+
help_surf = help_font.render(help_text, True, colors.LIGHT_GRAY)
|
41
|
+
# Position help text at the bottom right
|
42
|
+
help_rect = help_surf.get_rect(bottomright=(screen_w - 15, bottom_y))
|
43
|
+
surface.blit(help_surf, help_rect)
|