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.
Files changed (58) hide show
  1. trianglengin/__init__.py +35 -0
  2. trianglengin/config/README.md +38 -0
  3. trianglengin/config/__init__.py +8 -0
  4. trianglengin/config/display_config.py +47 -0
  5. trianglengin/config/env_config.py +62 -0
  6. trianglengin/core/__init__.py +10 -0
  7. trianglengin/cpp/CMakeLists.txt +42 -0
  8. trianglengin/cpp/bindings.cpp +211 -0
  9. trianglengin/cpp/config.h +28 -0
  10. trianglengin/cpp/game_state.cpp +327 -0
  11. trianglengin/cpp/game_state.h +73 -0
  12. trianglengin/cpp/grid_data.cpp +239 -0
  13. trianglengin/cpp/grid_data.h +78 -0
  14. trianglengin/cpp/grid_logic.cpp +125 -0
  15. trianglengin/cpp/grid_logic.h +30 -0
  16. trianglengin/cpp/shape_logic.cpp +100 -0
  17. trianglengin/cpp/shape_logic.h +28 -0
  18. trianglengin/cpp/structs.h +40 -0
  19. trianglengin/game_interface.py +222 -0
  20. trianglengin/py.typed +0 -0
  21. trianglengin/trianglengin_cpp.cpython-312-darwin.so +0 -0
  22. trianglengin/ui/README.md +35 -0
  23. trianglengin/ui/__init__.py +21 -0
  24. trianglengin/ui/app.py +107 -0
  25. trianglengin/ui/cli.py +123 -0
  26. trianglengin/ui/config.py +44 -0
  27. trianglengin/ui/interaction/README.md +44 -0
  28. trianglengin/ui/interaction/__init__.py +19 -0
  29. trianglengin/ui/interaction/debug_mode_handler.py +72 -0
  30. trianglengin/ui/interaction/event_processor.py +49 -0
  31. trianglengin/ui/interaction/input_handler.py +89 -0
  32. trianglengin/ui/interaction/play_mode_handler.py +156 -0
  33. trianglengin/ui/visualization/README.md +42 -0
  34. trianglengin/ui/visualization/__init__.py +58 -0
  35. trianglengin/ui/visualization/core/README.md +51 -0
  36. trianglengin/ui/visualization/core/__init__.py +16 -0
  37. trianglengin/ui/visualization/core/colors.py +115 -0
  38. trianglengin/ui/visualization/core/coord_mapper.py +85 -0
  39. trianglengin/ui/visualization/core/fonts.py +65 -0
  40. trianglengin/ui/visualization/core/layout.py +77 -0
  41. trianglengin/ui/visualization/core/visualizer.py +248 -0
  42. trianglengin/ui/visualization/drawing/README.md +49 -0
  43. trianglengin/ui/visualization/drawing/__init__.py +43 -0
  44. trianglengin/ui/visualization/drawing/grid.py +213 -0
  45. trianglengin/ui/visualization/drawing/highlight.py +31 -0
  46. trianglengin/ui/visualization/drawing/hud.py +43 -0
  47. trianglengin/ui/visualization/drawing/previews.py +181 -0
  48. trianglengin/ui/visualization/drawing/shapes.py +46 -0
  49. trianglengin/ui/visualization/drawing/utils.py +23 -0
  50. trianglengin/utils/__init__.py +9 -0
  51. trianglengin/utils/geometry.py +62 -0
  52. trianglengin/utils/types.py +10 -0
  53. trianglengin-2.0.1.dist-info/METADATA +250 -0
  54. trianglengin-2.0.1.dist-info/RECORD +58 -0
  55. trianglengin-2.0.1.dist-info/WHEEL +5 -0
  56. trianglengin-2.0.1.dist-info/entry_points.txt +2 -0
  57. trianglengin-2.0.1.dist-info/licenses/LICENSE +22 -0
  58. 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)