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,156 @@
1
+ # src/trianglengin/ui/interaction/play_mode_handler.py
2
+ import logging
3
+ from typing import TYPE_CHECKING, cast
4
+
5
+ # Guard UI imports
6
+ try:
7
+ import pygame
8
+
9
+ # Use absolute import
10
+ from trianglengin.ui.visualization import core as vis_core
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
+ if TYPE_CHECKING:
20
+ # Use absolute import for InputHandler as well
21
+ from trianglengin.game_interface import GameState, Shape
22
+ from trianglengin.ui.interaction.input_handler import InputHandler
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ # Add EnvConfig type hint for config
28
+ def _encode_action(shape_idx: int, r: int, c: int, config: EnvConfig) -> int:
29
+ """Helper to encode action based on config. Matches C++ internal logic."""
30
+ grid_size = config.ROWS * config.COLS
31
+ if not (
32
+ 0 <= shape_idx < config.NUM_SHAPE_SLOTS
33
+ and 0 <= r < config.ROWS
34
+ and 0 <= c < config.COLS
35
+ ):
36
+ return -1
37
+ # Cast the result to int for mypy
38
+ return cast("int", shape_idx * grid_size + r * config.COLS + c)
39
+
40
+
41
+ def handle_play_click(event: pygame.event.Event, handler: "InputHandler") -> None:
42
+ """Handles mouse clicks in play mode (select preview, place shape). Modifies handler state."""
43
+ if not (event.type == pygame.MOUSEBUTTONDOWN and event.button == 1):
44
+ return
45
+
46
+ game_state: GameState = handler.game_state
47
+ visualizer = handler.visualizer
48
+ mouse_pos = handler.mouse_pos
49
+
50
+ if game_state.is_over():
51
+ logger.info("Game is over, ignoring click.")
52
+ return
53
+
54
+ layout_rects = visualizer.ensure_layout()
55
+ grid_rect = layout_rects.get("grid")
56
+ preview_rects = visualizer.preview_rects
57
+ current_shapes = game_state.get_shapes()
58
+
59
+ preview_idx = vis_core.coord_mapper.get_preview_index_from_screen(
60
+ mouse_pos, preview_rects
61
+ )
62
+ if preview_idx is not None:
63
+ if handler.selected_shape_idx == preview_idx:
64
+ handler.selected_shape_idx = -1
65
+ handler.hover_grid_coord = None
66
+ handler.hover_shape = None
67
+ logger.info("Deselected shape.")
68
+ elif (
69
+ 0 <= preview_idx < len(current_shapes)
70
+ and current_shapes[preview_idx] is not None
71
+ ):
72
+ handler.selected_shape_idx = preview_idx
73
+ logger.info(f"Selected shape index: {preview_idx}")
74
+ update_play_hover(handler)
75
+ else:
76
+ logger.info(f"Clicked empty/invalid preview slot: {preview_idx}")
77
+ if handler.selected_shape_idx != -1:
78
+ handler.selected_shape_idx = -1
79
+ handler.hover_grid_coord = None
80
+ handler.hover_shape = None
81
+ return
82
+
83
+ selected_idx = handler.selected_shape_idx
84
+ if selected_idx != -1 and grid_rect and grid_rect.collidepoint(mouse_pos):
85
+ grid_coords = vis_core.coord_mapper.get_grid_coords_from_screen(
86
+ mouse_pos, grid_rect, game_state.env_config
87
+ )
88
+ shape_to_place: Shape | None = (
89
+ current_shapes[selected_idx]
90
+ if 0 <= selected_idx < len(current_shapes)
91
+ else None
92
+ )
93
+
94
+ if grid_coords and shape_to_place:
95
+ r, c = grid_coords
96
+ potential_action = _encode_action(selected_idx, r, c, game_state.env_config)
97
+
98
+ if (
99
+ potential_action != -1
100
+ and potential_action in game_state.valid_actions()
101
+ ):
102
+ reward, done = game_state.step(potential_action)
103
+ logger.info(
104
+ f"Placed shape {selected_idx} at {grid_coords}. R={reward:.1f}, Done={done}"
105
+ )
106
+ handler.selected_shape_idx = -1
107
+ handler.hover_grid_coord = None
108
+ handler.hover_shape = None
109
+ else:
110
+ logger.info(
111
+ f"Clicked grid at {grid_coords}, but placement invalid (action {potential_action} not found in valid set)."
112
+ )
113
+ else:
114
+ logger.info(f"Clicked grid at {grid_coords}, but shape or coords invalid.")
115
+
116
+
117
+ def update_play_hover(handler: "InputHandler") -> None:
118
+ """Updates the hover state within the InputHandler."""
119
+ handler.hover_grid_coord = None
120
+ handler.hover_is_valid = False
121
+ handler.hover_shape = None
122
+
123
+ game_state: GameState = handler.game_state
124
+ visualizer = handler.visualizer
125
+ mouse_pos = handler.mouse_pos
126
+
127
+ if game_state.is_over() or handler.selected_shape_idx == -1:
128
+ return
129
+
130
+ layout_rects = visualizer.ensure_layout()
131
+ grid_rect = layout_rects.get("grid")
132
+ selected_idx = handler.selected_shape_idx
133
+ current_shapes = game_state.get_shapes()
134
+ if not (0 <= selected_idx < len(current_shapes)):
135
+ return
136
+ shape: Shape | None = current_shapes[selected_idx]
137
+ if not shape:
138
+ return
139
+
140
+ handler.hover_shape = shape
141
+
142
+ if not grid_rect or not grid_rect.collidepoint(mouse_pos):
143
+ return
144
+
145
+ grid_coords = vis_core.coord_mapper.get_grid_coords_from_screen(
146
+ mouse_pos, grid_rect, game_state.env_config
147
+ )
148
+
149
+ if grid_coords:
150
+ r, c = grid_coords
151
+ potential_action = _encode_action(selected_idx, r, c, game_state.env_config)
152
+ is_valid = (
153
+ potential_action != -1 and potential_action in game_state.valid_actions()
154
+ )
155
+ handler.hover_grid_coord = grid_coords
156
+ handler.hover_is_valid = is_valid
@@ -0,0 +1,42 @@
1
+
2
+ # UI Visualization Module (`trianglengin.ui.visualization`)
3
+
4
+ **Requires:** `pygame`
5
+
6
+ ## Purpose and Architecture
7
+
8
+ This module is responsible for rendering the game state visually using the Pygame library, specifically for the **interactive modes** (play/debug) provided by the `trianglengin.ui` package.
9
+
10
+ - **Core Components ([`core/README.md`](core/README.md)):**
11
+ - `Visualizer`: Orchestrates the rendering process for interactive modes.
12
+ - `layout`: Calculates the screen positions and sizes for different UI areas.
13
+ - `fonts`: Loads necessary font files.
14
+ - `colors`: Defines a centralized palette of RGB color tuples.
15
+ - `coord_mapper`: Provides functions to map screen coordinates to grid coordinates and preview indices.
16
+ - **Drawing Components ([`drawing/README.md`](drawing/README.md)):**
17
+ - Contains specific functions for drawing different elements onto Pygame surfaces (grid, shapes, previews, HUD, highlights).
18
+
19
+ **Note:** This module depends on the core `trianglengin` engine for game state data (`GameState`, `EnvConfig`, `Shape`).
20
+
21
+ ## Exposed Interfaces
22
+
23
+ - **Core Classes & Functions:**
24
+ - `Visualizer`: Main renderer for interactive modes.
25
+ - `load_fonts`: Loads Pygame fonts.
26
+ - `colors`: Module containing color constants (e.g., `colors.WHITE`).
27
+ - `get_grid_coords_from_screen`: Maps screen to grid coordinates.
28
+ - `get_preview_index_from_screen`: Maps screen to preview index.
29
+ - **Drawing Functions:** (Exposed via `trianglengin.ui.visualization.drawing`)
30
+ - **Config:** (Configs are *not* re-exported from this module)
31
+
32
+ ## Dependencies
33
+
34
+ - **`trianglengin` (core):** `GameState`, `EnvConfig`, `Shape`.
35
+ - **`trianglengin.ui.config`**: `DisplayConfig`.
36
+ - **`trianglengin.utils`**: `geometry`.
37
+ - **`pygame`**: The core library used for all drawing, surface manipulation, and font rendering.
38
+ - **Standard Libraries:** `typing`, `logging`, `math`.
39
+
40
+ ---
41
+
42
+ **Note:** Please keep this README updated when changing rendering logic, adding new visual elements, modifying layout calculations, or altering the interfaces exposed.
@@ -0,0 +1,58 @@
1
+ """
2
+ Visualization module for rendering the game state using Pygame.
3
+ Provides components for interactive play/debug modes.
4
+ """
5
+
6
+ # Imports are now direct. If pygame is missing, errors will occur upon use.
7
+
8
+ # Import core components needed externally
9
+ # Import core EnvConfig for type hinting if needed
10
+ # from trianglengin.config import EnvConfig # REMOVED Re-export
11
+ # from ..config import DisplayConfig # REMOVED Re-export
12
+
13
+ from .core import colors # Import the colors module
14
+ from .core.coord_mapper import ( # Import specific functions
15
+ get_grid_coords_from_screen,
16
+ get_preview_index_from_screen,
17
+ )
18
+ from .core.fonts import load_fonts # Import specific function
19
+ from .core.visualizer import Visualizer # Import the class
20
+
21
+ # Import drawing functions directly
22
+ from .drawing.grid import (
23
+ draw_debug_grid_overlay,
24
+ draw_grid_background,
25
+ draw_grid_state,
26
+ )
27
+ from .drawing.highlight import draw_debug_highlight
28
+ from .drawing.hud import render_hud
29
+ from .drawing.previews import (
30
+ draw_floating_preview,
31
+ draw_placement_preview,
32
+ render_previews,
33
+ )
34
+ from .drawing.shapes import draw_shape
35
+ from .drawing.utils import get_triangle_points
36
+
37
+ __all__ = [
38
+ # Core Renderer & Layout related
39
+ "Visualizer",
40
+ "load_fonts",
41
+ "colors", # Export the colors module
42
+ "get_grid_coords_from_screen",
43
+ "get_preview_index_from_screen",
44
+ # Drawing Functions
45
+ "draw_grid_background",
46
+ "draw_grid_state",
47
+ "draw_debug_grid_overlay",
48
+ "draw_shape",
49
+ "render_previews",
50
+ "draw_placement_preview",
51
+ "draw_floating_preview",
52
+ "render_hud",
53
+ "draw_debug_highlight",
54
+ "get_triangle_points",
55
+ # Configs are NOT re-exported here
56
+ # "DisplayConfig",
57
+ # "EnvConfig",
58
+ ]
@@ -0,0 +1,51 @@
1
+
2
+
3
+ # UI Visualization Core Submodule (`trianglengin.ui.visualization.core`)
4
+
5
+ **Requires:** `pygame`
6
+
7
+ ## Purpose and Architecture
8
+
9
+ This submodule contains the central classes and foundational elements for the **interactive** visualization system within the `trianglengin.ui` package. It orchestrates rendering for play/debug modes, manages layout and coordinate systems, and defines core visual properties like colors and fonts.
10
+
11
+ - **Render Orchestration:**
12
+ - [`Visualizer`](visualizer.py): The main class for rendering in **interactive modes** ("play", "debug"). It maintains the Pygame screen, calculates layout using `layout.py`, manages cached preview area rectangles, and calls appropriate drawing functions from [`trianglengin.ui.visualization.drawing`](../drawing/README.md). **It receives interaction state (hover position, selected index) via its `render` method to display visual feedback.**
13
+ - **Layout Management:**
14
+ - [`layout.py`](layout.py): Contains functions (`calculate_interactive_layout`, `calculate_training_layout`) to determine the size and position of the main UI areas based on the screen dimensions, mode, and `DisplayConfig`.
15
+ - **Coordinate System:**
16
+ - [`coord_mapper.py`](coord_mapper.py): Provides essential mapping functions:
17
+ - `_calculate_render_params`: Internal helper to get scaling and offset for grid rendering.
18
+ - `get_grid_coords_from_screen`: Converts mouse/screen coordinates into logical grid (row, column) coordinates.
19
+ - `get_preview_index_from_screen`: Converts mouse/screen coordinates into the index of the shape preview slot being pointed at.
20
+ - **Visual Properties:**
21
+ - [`colors.py`](colors.py): Defines a centralized palette of named color constants (RGB tuples).
22
+ - [`fonts.py`](fonts.py): Contains the `load_fonts` function to load and manage Pygame font objects.
23
+
24
+ ## Exposed Interfaces
25
+
26
+ - **Classes:**
27
+ - `Visualizer` (Import directly from `visualizer.py`, not exported here)
28
+ - **Functions:**
29
+ - `calculate_interactive_layout(...) -> Dict[str, pygame.Rect]`
30
+ - `calculate_training_layout(...) -> Dict[str, pygame.Rect]` (Kept for potential future use)
31
+ - `load_fonts() -> Dict[str, Optional[pygame.font.Font]]`
32
+ - `get_grid_coords_from_screen(...) -> Optional[Tuple[int, int]]`
33
+ - `get_preview_index_from_screen(...) -> Optional[int]`
34
+ - **Modules:**
35
+ - `colors`: Provides color constants (e.g., `colors.RED`).
36
+ - `layout`: Provides layout calculation functions.
37
+ - `coord_mapper`: Provides coordinate mapping functions.
38
+ - `fonts`: Provides font loading functions.
39
+
40
+ ## Dependencies
41
+
42
+ - **`trianglengin` (core):** `GameState`, `EnvConfig`, `Shape`.
43
+ - **`trianglengin.ui.config`**: `DisplayConfig`.
44
+ - **`trianglengin.utils`**: `geometry`.
45
+ - **`trianglengin.ui.visualization.drawing`**: Drawing functions are called by `Visualizer`.
46
+ - **`pygame`**: Used for surfaces, rectangles, fonts, display management.
47
+ - **Standard Libraries:** `typing`, `logging`, `math`.
48
+
49
+ ---
50
+
51
+ **Note:** Please keep this README updated when changing the core rendering logic, layout calculations, coordinate mapping, or the interfaces of the renderers.
@@ -0,0 +1,16 @@
1
+ """Core visualization components: renderers, layout, fonts, colors, coordinate mapping."""
2
+
3
+ # Direct imports - if pygame is missing, these will fail, which is intended behavior
4
+ # for the optional UI package.
5
+ from . import colors, coord_mapper, fonts, layout # Import modules needed by others
6
+
7
+ # Visualizer is NOT exported from here to avoid circular imports.
8
+ # Import it directly: from trianglengin.ui.visualization.core.visualizer import Visualizer
9
+
10
+ __all__ = [
11
+ # "Visualizer", # REMOVED
12
+ "layout",
13
+ "fonts",
14
+ "colors",
15
+ "coord_mapper",
16
+ ]
@@ -0,0 +1,115 @@
1
+ """
2
+ Defines color constants and mappings used throughout the application,
3
+ especially for visualization.
4
+ """
5
+
6
+ # Define the Color type alias (RGB)
7
+ Color = tuple[int, int, int]
8
+ # Define RGBA type alias for clarity where alpha is used
9
+ ColorRGBA = tuple[int, int, int, int]
10
+
11
+ # --- Standard Colors ---
12
+ WHITE: Color = (255, 255, 255)
13
+ BLACK: Color = (0, 0, 0)
14
+ GRAY: Color = (128, 128, 128)
15
+ LIGHT_GRAY: Color = (200, 200, 200)
16
+ DARK_GRAY: Color = (50, 50, 50)
17
+ RED: Color = (255, 0, 0)
18
+ GREEN: Color = (0, 255, 0)
19
+ BLUE: Color = (0, 0, 255)
20
+ YELLOW: Color = (255, 255, 0)
21
+ CYAN: Color = (0, 255, 255)
22
+ MAGENTA: Color = (255, 0, 255)
23
+ ORANGE: Color = (255, 165, 0)
24
+ PURPLE: Color = (128, 0, 128)
25
+ TEAL: Color = (0, 128, 128)
26
+ OLIVE: Color = (128, 128, 0)
27
+
28
+ # --- Game Specific Colors ---
29
+ # Colors used for the placeable shapes
30
+ # These should align with the colors used/generated by the C++ core if specified there,
31
+ # or be used to interpret the color_id returned by the C++ core.
32
+ SHAPE_COLORS: tuple[Color, ...] = (
33
+ (220, 40, 40), # 0: Red
34
+ (60, 60, 220), # 1: Blue
35
+ (40, 200, 40), # 2: Green
36
+ (230, 230, 40), # 3: Yellow
37
+ (240, 150, 20), # 4: Orange
38
+ (140, 40, 140), # 5: Purple
39
+ (40, 200, 200), # 6: Cyan
40
+ (200, 100, 180), # 7: Pink
41
+ (100, 180, 200), # 8: Light Blue
42
+ )
43
+
44
+ # Mapping from Color ID (returned by C++) to RGB tuple for visualization.
45
+ ID_TO_COLOR_MAP: dict[int, Color] = dict(enumerate(SHAPE_COLORS))
46
+
47
+ # Special Color IDs (ensure these match C++ constants)
48
+ NO_COLOR_ID: int = -1
49
+ DEBUG_COLOR_ID: int = -2
50
+
51
+ # Grid background colors
52
+ GRID_BG_LIGHT: Color = (40, 40, 40)
53
+ GRID_BG_DARK: Color = (30, 30, 30)
54
+ GRID_LINE_COLOR: Color = (80, 80, 80)
55
+ DEATH_ZONE_COLOR: Color = (60, 0, 0)
56
+ TRIANGLE_EMPTY_COLOR: Color = GRAY
57
+ GRID_BG_DEFAULT: Color = DARK_GRAY
58
+ GRID_BG_GAME_OVER: Color = (70, 30, 30)
59
+
60
+ # UI Colors
61
+ HUD_BG_COLOR: Color = (20, 20, 20)
62
+ HUD_TEXT_COLOR: Color = WHITE
63
+ PREVIEW_BG_COLOR: Color = (25, 25, 25)
64
+ PREVIEW_BORDER: Color = GRAY
65
+ PREVIEW_SELECTED_BORDER: Color = WHITE
66
+
67
+ # Highlight Colors
68
+ HIGHLIGHT_VALID_COLOR: Color = GREEN
69
+ HIGHLIGHT_INVALID_COLOR: Color = RED
70
+ PLACEMENT_VALID_COLOR: Color = GREEN
71
+ PLACEMENT_INVALID_COLOR: Color = RED
72
+
73
+ # Debug Colors
74
+ DEBUG_TOGGLE_COLOR: Color = MAGENTA
75
+
76
+ __all__ = [
77
+ "Color",
78
+ "ColorRGBA",
79
+ "WHITE",
80
+ "BLACK",
81
+ "GRAY",
82
+ "LIGHT_GRAY",
83
+ "DARK_GRAY",
84
+ "RED",
85
+ "GREEN",
86
+ "BLUE",
87
+ "YELLOW",
88
+ "CYAN",
89
+ "MAGENTA",
90
+ "ORANGE",
91
+ "PURPLE",
92
+ "TEAL",
93
+ "OLIVE",
94
+ "SHAPE_COLORS", # Keep if needed elsewhere, but ID_TO_COLOR_MAP is primary
95
+ "ID_TO_COLOR_MAP",
96
+ "NO_COLOR_ID",
97
+ "DEBUG_COLOR_ID",
98
+ "GRID_BG_LIGHT",
99
+ "GRID_BG_DARK",
100
+ "GRID_LINE_COLOR",
101
+ "DEATH_ZONE_COLOR",
102
+ "TRIANGLE_EMPTY_COLOR",
103
+ "GRID_BG_DEFAULT",
104
+ "GRID_BG_GAME_OVER",
105
+ "HUD_BG_COLOR",
106
+ "HUD_TEXT_COLOR",
107
+ "PREVIEW_BG_COLOR",
108
+ "PREVIEW_BORDER",
109
+ "PREVIEW_SELECTED_BORDER",
110
+ "HIGHLIGHT_VALID_COLOR",
111
+ "HIGHLIGHT_INVALID_COLOR",
112
+ "PLACEMENT_VALID_COLOR",
113
+ "PLACEMENT_INVALID_COLOR",
114
+ "DEBUG_TOGGLE_COLOR",
115
+ ]
@@ -0,0 +1,85 @@
1
+ # Guard UI imports
2
+ try:
3
+ import pygame
4
+ except ImportError as e:
5
+ raise ImportError(
6
+ "UI components require 'pygame'. Install with 'pip install trianglengin[ui]'."
7
+ ) from e
8
+
9
+ # Use absolute imports for core components
10
+ from trianglengin.config import EnvConfig
11
+ from trianglengin.utils import geometry
12
+
13
+ # Correct the import path for the drawing utility (relative within UI)
14
+ from ..drawing.utils import get_triangle_points
15
+
16
+
17
+ def _calculate_render_params(
18
+ width: int, height: int, config: EnvConfig
19
+ ) -> tuple[float, float, float, float]:
20
+ """Calculates scale (cw, ch) and offset (ox, oy) for rendering the grid."""
21
+ rows, cols = config.ROWS, config.COLS
22
+ cols_eff = cols * 0.75 + 0.25 if cols > 0 else 1
23
+ scale_w = width / cols_eff if cols_eff > 0 else 1
24
+ scale_h = height / rows if rows > 0 else 1
25
+ scale = max(1.0, min(scale_w, scale_h))
26
+ cell_size = scale
27
+ grid_w_px = cols_eff * cell_size
28
+ grid_h_px = rows * cell_size
29
+ offset_x = (width - grid_w_px) / 2
30
+ offset_y = (height - grid_h_px) / 2
31
+ return cell_size, cell_size, offset_x, offset_y
32
+
33
+
34
+ def get_grid_coords_from_screen(
35
+ screen_pos: tuple[int, int], grid_area_rect: pygame.Rect, config: EnvConfig
36
+ ) -> tuple[int, int] | None:
37
+ """Maps screen coordinates (relative to screen) to grid row/column."""
38
+ if not grid_area_rect or not grid_area_rect.collidepoint(screen_pos):
39
+ return None
40
+
41
+ local_x = screen_pos[0] - grid_area_rect.left
42
+ local_y = screen_pos[1] - grid_area_rect.top
43
+ cw, ch, ox, oy = _calculate_render_params(
44
+ grid_area_rect.width, grid_area_rect.height, config
45
+ )
46
+ if cw <= 0 or ch <= 0:
47
+ return None
48
+
49
+ # Estimate row/col based on overall position
50
+ row = int((local_y - oy) / ch) if ch > 0 else -1
51
+ # Approximate column based on horizontal position, accounting for 0.75 width factor
52
+ approx_col_center_index = (local_x - ox - cw / 4) / (cw * 0.75) if cw > 0 else -1
53
+ col = int(round(approx_col_center_index))
54
+
55
+ # Check the estimated cell and its immediate neighbors using point-in-polygon
56
+ # This is more accurate near triangle boundaries
57
+ for r_check in [row, row - 1, row + 1]:
58
+ if not (0 <= r_check < config.ROWS):
59
+ continue
60
+ # Check a slightly wider range of columns due to triangular nature
61
+ for c_check in [col - 1, col, col + 1, col + 2]:
62
+ if not (0 <= c_check < config.COLS):
63
+ continue
64
+ is_up = (r_check + c_check) % 2 != 0
65
+ pts = get_triangle_points(r_check, c_check, is_up, ox, oy, cw, ch)
66
+ # Use the geometry utility function
67
+ if geometry.is_point_in_polygon((local_x, local_y), pts):
68
+ return r_check, c_check
69
+
70
+ # Fallback if point-in-polygon check fails (should be rare)
71
+ if 0 <= row < config.ROWS and 0 <= col < config.COLS:
72
+ return row, col
73
+ return None
74
+
75
+
76
+ def get_preview_index_from_screen(
77
+ screen_pos: tuple[int, int], preview_rects: dict[int, pygame.Rect]
78
+ ) -> int | None:
79
+ """Maps screen coordinates to a shape preview index."""
80
+ if not preview_rects:
81
+ return None
82
+ for idx, rect in preview_rects.items():
83
+ if rect and rect.collidepoint(screen_pos):
84
+ return idx
85
+ return None
@@ -0,0 +1,65 @@
1
+ import logging
2
+
3
+ # Guard UI imports
4
+ try:
5
+ import pygame
6
+
7
+ pygame.font.init() # Initialize font module here
8
+ except ImportError as e:
9
+ raise ImportError(
10
+ "UI components require 'pygame'. Install with 'pip install trianglengin[ui]'."
11
+ ) from e
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ DEFAULT_FONT_NAME = None
16
+ FALLBACK_FONT_NAME = "arial,freesans"
17
+
18
+
19
+ def load_single_font(name: str | None, size: int) -> pygame.font.Font | None:
20
+ """Loads a single font, handling potential errors."""
21
+ try:
22
+ font = pygame.font.SysFont(name, size)
23
+ return font
24
+ except Exception as e:
25
+ logger.error(f"Error loading font '{name}' size {size}: {e}")
26
+ if name != FALLBACK_FONT_NAME:
27
+ logger.warning(f"Attempting fallback font: {FALLBACK_FONT_NAME}")
28
+ try:
29
+ font = pygame.font.SysFont(FALLBACK_FONT_NAME, size)
30
+ logger.info(f"Loaded fallback font: {FALLBACK_FONT_NAME} size {size}")
31
+ return font
32
+ except Exception as e_fallback:
33
+ logger.error(f"Fallback font failed: {e_fallback}")
34
+ return None
35
+ return None
36
+
37
+
38
+ def load_fonts(
39
+ font_sizes: dict[str, int] | None = None,
40
+ ) -> dict[str, pygame.font.Font | None]:
41
+ """Loads standard game fonts."""
42
+ if font_sizes is None:
43
+ font_sizes = {
44
+ "ui": 24,
45
+ "score": 30,
46
+ "help": 18,
47
+ "title": 48,
48
+ # Add debug font size if needed, though DisplayConfig handles the object
49
+ "debug": 12,
50
+ }
51
+
52
+ fonts: dict[str, pygame.font.Font | None] = {}
53
+ required_fonts = ["score", "help"]
54
+
55
+ logger.info("Loading fonts...")
56
+ for name, size in font_sizes.items():
57
+ fonts[name] = load_single_font(DEFAULT_FONT_NAME, size)
58
+
59
+ for name in required_fonts:
60
+ if fonts.get(name) is None:
61
+ logger.critical(
62
+ f"Essential font '{name}' failed to load. Text rendering will be affected."
63
+ )
64
+
65
+ return fonts
@@ -0,0 +1,77 @@
1
+ """
2
+ Functions to calculate the layout of UI elements based on screen size and config.
3
+ """
4
+
5
+ import logging
6
+
7
+ import pygame # Required dependency
8
+
9
+ # Use absolute imports
10
+ from trianglengin.ui.config import DisplayConfig
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def calculate_interactive_layout(
16
+ screen_width: int, screen_height: int, config: DisplayConfig
17
+ ) -> dict[str, pygame.Rect]:
18
+ """
19
+ Calculates layout rectangles for interactive modes (play/debug).
20
+
21
+ Args:
22
+ screen_width: Current width of the screen/window.
23
+ screen_height: Current height of the screen/window.
24
+ config: The DisplayConfig object.
25
+
26
+ Returns:
27
+ A dictionary mapping area names ('grid', 'preview') to pygame.Rect objects.
28
+ """
29
+ pad = config.PADDING
30
+ hud_h = config.HUD_HEIGHT
31
+ preview_w = config.PREVIEW_AREA_WIDTH
32
+
33
+ # Available height after accounting for top/bottom padding and HUD
34
+ available_h = screen_height - 2 * pad - hud_h
35
+ # Available width after accounting for left/right padding and preview area
36
+ available_w = screen_width - 3 * pad - preview_w
37
+
38
+ if available_w <= 0 or available_h <= 0:
39
+ logger.warning(
40
+ f"Screen size ({screen_width}x{screen_height}) too small for layout."
41
+ )
42
+ # Return minimal valid rects to avoid errors downstream
43
+ return {
44
+ "grid": pygame.Rect(pad, pad, 1, 1),
45
+ "preview": pygame.Rect(screen_width - pad - preview_w, pad, 1, 1),
46
+ }
47
+
48
+ # Grid area takes up the main left space
49
+ grid_rect = pygame.Rect(pad, pad, available_w, available_h)
50
+
51
+ # Preview area is on the right
52
+ preview_rect = pygame.Rect(grid_rect.right + pad, pad, preview_w, available_h)
53
+
54
+ # HUD area (optional, could be drawn directly without a rect)
55
+ # hud_rect = pygame.Rect(pad, grid_rect.bottom + pad, screen_width - 2 * pad, hud_h)
56
+
57
+ return {"grid": grid_rect, "preview": preview_rect}
58
+
59
+
60
+ def calculate_training_layout(
61
+ screen_width: int, screen_height: int, config: DisplayConfig
62
+ ) -> dict[str, pygame.Rect]:
63
+ """
64
+ Calculates layout rectangles for training/headless visualization (if needed).
65
+ Currently simple, just uses the whole screen for the grid.
66
+
67
+ Args:
68
+ screen_width: Current width of the screen/window.
69
+ screen_height: Current height of the screen/window.
70
+ config: The DisplayConfig object.
71
+
72
+ Returns:
73
+ A dictionary mapping area names ('grid') to pygame.Rect objects.
74
+ """
75
+ pad = config.PADDING
76
+ grid_rect = pygame.Rect(pad, pad, screen_width - 2 * pad, screen_height - 2 * pad)
77
+ return {"grid": grid_rect}