trianglengin 2.0.1__cp310-cp310-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-310-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,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}
|