trianglengin 1.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tests/__init__.py +0 -0
- tests/conftest.py +108 -0
- tests/core/__init__.py +2 -0
- tests/core/environment/README.md +47 -0
- tests/core/environment/__init__.py +2 -0
- tests/core/environment/test_action_codec.py +50 -0
- tests/core/environment/test_game_state.py +483 -0
- tests/core/environment/test_grid_data.py +205 -0
- tests/core/environment/test_grid_logic.py +362 -0
- tests/core/environment/test_shape_logic.py +171 -0
- tests/core/environment/test_step.py +372 -0
- tests/core/structs/__init__.py +0 -0
- tests/core/structs/test_shape.py +83 -0
- tests/core/structs/test_triangle.py +97 -0
- tests/utils/__init__.py +0 -0
- tests/utils/test_geometry.py +93 -0
- trianglengin/__init__.py +18 -0
- trianglengin/app.py +110 -0
- trianglengin/cli.py +134 -0
- trianglengin/config/__init__.py +9 -0
- trianglengin/config/display_config.py +47 -0
- trianglengin/config/env_config.py +103 -0
- trianglengin/core/__init__.py +8 -0
- trianglengin/core/environment/__init__.py +31 -0
- trianglengin/core/environment/action_codec.py +37 -0
- trianglengin/core/environment/game_state.py +217 -0
- trianglengin/core/environment/grid/README.md +46 -0
- trianglengin/core/environment/grid/__init__.py +18 -0
- trianglengin/core/environment/grid/grid_data.py +140 -0
- trianglengin/core/environment/grid/line_cache.py +189 -0
- trianglengin/core/environment/grid/logic.py +131 -0
- trianglengin/core/environment/logic/__init__.py +3 -0
- trianglengin/core/environment/logic/actions.py +38 -0
- trianglengin/core/environment/logic/step.py +134 -0
- trianglengin/core/environment/shapes/__init__.py +19 -0
- trianglengin/core/environment/shapes/logic.py +84 -0
- trianglengin/core/environment/shapes/templates.py +587 -0
- trianglengin/core/structs/__init__.py +27 -0
- trianglengin/core/structs/constants.py +28 -0
- trianglengin/core/structs/shape.py +61 -0
- trianglengin/core/structs/triangle.py +48 -0
- trianglengin/interaction/README.md +45 -0
- trianglengin/interaction/__init__.py +17 -0
- trianglengin/interaction/debug_mode_handler.py +96 -0
- trianglengin/interaction/event_processor.py +43 -0
- trianglengin/interaction/input_handler.py +82 -0
- trianglengin/interaction/play_mode_handler.py +141 -0
- trianglengin/utils/__init__.py +9 -0
- trianglengin/utils/geometry.py +73 -0
- trianglengin/utils/types.py +10 -0
- trianglengin/visualization/README.md +44 -0
- trianglengin/visualization/__init__.py +61 -0
- trianglengin/visualization/core/README.md +52 -0
- trianglengin/visualization/core/__init__.py +12 -0
- trianglengin/visualization/core/colors.py +117 -0
- trianglengin/visualization/core/coord_mapper.py +73 -0
- trianglengin/visualization/core/fonts.py +55 -0
- trianglengin/visualization/core/layout.py +101 -0
- trianglengin/visualization/core/visualizer.py +232 -0
- trianglengin/visualization/drawing/README.md +45 -0
- trianglengin/visualization/drawing/__init__.py +30 -0
- trianglengin/visualization/drawing/grid.py +156 -0
- trianglengin/visualization/drawing/highlight.py +30 -0
- trianglengin/visualization/drawing/hud.py +39 -0
- trianglengin/visualization/drawing/previews.py +172 -0
- trianglengin/visualization/drawing/shapes.py +36 -0
- trianglengin-1.0.6.dist-info/METADATA +367 -0
- trianglengin-1.0.6.dist-info/RECORD +72 -0
- trianglengin-1.0.6.dist-info/WHEEL +5 -0
- trianglengin-1.0.6.dist-info/entry_points.txt +2 -0
- trianglengin-1.0.6.dist-info/licenses/LICENSE +22 -0
- trianglengin-1.0.6.dist-info/top_level.txt +2 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
|
4
|
+
class Triangle:
|
5
|
+
"""Represents a single triangular cell on the grid."""
|
6
|
+
|
7
|
+
def __init__(self, row: int, col: int, is_up: bool, is_death: bool = False):
|
8
|
+
self.row = row
|
9
|
+
self.col = col
|
10
|
+
self.is_up = is_up
|
11
|
+
self.is_death = is_death
|
12
|
+
self.is_occupied = is_death
|
13
|
+
self.color: tuple[int, int, int] | None = None
|
14
|
+
|
15
|
+
self.neighbor_left: Triangle | None = None
|
16
|
+
self.neighbor_right: Triangle | None = None
|
17
|
+
self.neighbor_vert: Triangle | None = None
|
18
|
+
|
19
|
+
def get_points(
|
20
|
+
self, ox: float, oy: float, cw: float, ch: float
|
21
|
+
) -> list[tuple[float, float]]:
|
22
|
+
"""Calculates vertex points for drawing, relative to origin (ox, oy)."""
|
23
|
+
x = ox + self.col * (cw * 0.75)
|
24
|
+
y = oy + self.row * ch
|
25
|
+
if self.is_up:
|
26
|
+
return [(x, y + ch), (x + cw, y + ch), (x + cw / 2, y)]
|
27
|
+
else:
|
28
|
+
return [(x, y), (x + cw, y), (x + cw / 2, y + ch)]
|
29
|
+
|
30
|
+
def copy(self) -> Triangle:
|
31
|
+
"""Creates a copy of the Triangle object's state (neighbors are not copied)."""
|
32
|
+
new_tri = Triangle(self.row, self.col, self.is_up, self.is_death)
|
33
|
+
new_tri.is_occupied = self.is_occupied
|
34
|
+
new_tri.color = self.color
|
35
|
+
return new_tri
|
36
|
+
|
37
|
+
def __repr__(self) -> str:
|
38
|
+
state = "D" if self.is_death else ("O" if self.is_occupied else ".")
|
39
|
+
orient = "^" if self.is_up else "v"
|
40
|
+
return f"T({self.row},{self.col} {orient}{state})"
|
41
|
+
|
42
|
+
def __hash__(self):
|
43
|
+
return hash((self.row, self.col))
|
44
|
+
|
45
|
+
def __eq__(self, other):
|
46
|
+
if not isinstance(other, Triangle):
|
47
|
+
return NotImplemented
|
48
|
+
return self.row == other.row and self.col == other.col
|
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
# Interaction Module (`trianglengin.interaction`)
|
3
|
+
|
4
|
+
## Purpose and Architecture
|
5
|
+
|
6
|
+
This module handles user input (keyboard and mouse) for the interactive modes ("play", "debug") of the `trianglengin` library. It bridges the gap between raw Pygame events and actions within the game simulation (`GameState`).
|
7
|
+
|
8
|
+
- **Event Processing:** [`event_processor.py`](event_processor.py) handles common Pygame events like quitting (QUIT, ESC) and window resizing. It acts as a generator, yielding other events for mode-specific processing.
|
9
|
+
- **Input Handler:** The [`InputHandler`](input_handler.py) class is the main entry point.
|
10
|
+
- It receives Pygame events (via the `event_processor`).
|
11
|
+
- It **manages interaction-specific state** internally (e.g., `selected_shape_idx`, `hover_grid_coord`, `debug_highlight_coord`).
|
12
|
+
- It determines the current interaction mode ("play" or "debug") and delegates event handling and hover updates to specific handler functions ([`play_mode_handler`](play_mode_handler.py), [`debug_mode_handler`](debug_mode_handler.py)).
|
13
|
+
- It provides the necessary interaction state to the [`Visualizer`](../visualization/core/visualizer.py) for rendering feedback (hover previews, selection highlights).
|
14
|
+
- **Mode-Specific Handlers:** `play_mode_handler.py` and `debug_mode_handler.py` contain the logic specific to each mode, operating on the `InputHandler`'s state and the `GameState`.
|
15
|
+
- `play`: Handles selecting shapes, checking placement validity, and triggering `GameState.step` on valid clicks. Updates hover state in the `InputHandler`.
|
16
|
+
- `debug`: Handles toggling the state of individual triangles directly on the `GameState.grid_data`. Updates hover state in the `InputHandler`.
|
17
|
+
- **Decoupling:** It separates input handling logic from the core game simulation ([`core.environment`](../core/environment/README.md)) and rendering ([`visualization`](../visualization/README.md)), although it needs references to both to function.
|
18
|
+
|
19
|
+
## Exposed Interfaces
|
20
|
+
|
21
|
+
- **Classes:**
|
22
|
+
- `InputHandler`:
|
23
|
+
- `__init__(game_state: GameState, visualizer: Visualizer, mode: str, env_config: EnvConfig)`
|
24
|
+
- `handle_input() -> bool`: Processes events for one frame, returns `False` if quitting.
|
25
|
+
- `get_render_interaction_state() -> dict`: Returns interaction state needed by `Visualizer.render`.
|
26
|
+
- **Functions:**
|
27
|
+
- `process_pygame_events(visualizer: Visualizer) -> Generator[pygame.event.Event, Any, bool]`: Processes common events, yields others.
|
28
|
+
- `handle_play_click(event: pygame.event.Event, handler: InputHandler)`: Handles clicks in play mode.
|
29
|
+
- `update_play_hover(handler: InputHandler)`: Updates hover state in play mode.
|
30
|
+
- `handle_debug_click(event: pygame.event.Event, handler: InputHandler)`: Handles clicks in debug mode.
|
31
|
+
- `update_debug_hover(handler: InputHandler)`: Updates hover state in debug mode.
|
32
|
+
|
33
|
+
## Dependencies
|
34
|
+
|
35
|
+
- **[`trianglengin.core`](../core/README.md)**:
|
36
|
+
- `GameState`, `EnvConfig`, `GridLogic`, `ActionCodec`, `Shape`, `Triangle`, `DEBUG_COLOR_ID`, `NO_COLOR_ID`.
|
37
|
+
- **[`trianglengin.visualization`](../visualization/README.md)**:
|
38
|
+
- `Visualizer`, `VisConfig`, `coord_mapper`.
|
39
|
+
- **`pygame`**:
|
40
|
+
- Relies heavily on Pygame for event handling (`pygame.event`, `pygame.mouse`) and constants (`MOUSEBUTTONDOWN`, `KEYDOWN`, etc.).
|
41
|
+
- **Standard Libraries:** `typing`, `logging`.
|
42
|
+
|
43
|
+
---
|
44
|
+
|
45
|
+
**Note:** Please keep this README updated when adding new interaction modes, changing input handling logic, or modifying the interfaces between interaction, environment, and visualization. Accurate documentation is crucial for maintainability.
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"""
|
2
|
+
Interaction handling module for interactive modes (play/debug).
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .debug_mode_handler import handle_debug_click, update_debug_hover
|
6
|
+
from .event_processor import process_pygame_events
|
7
|
+
from .input_handler import InputHandler
|
8
|
+
from .play_mode_handler import handle_play_click, update_play_hover
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"InputHandler",
|
12
|
+
"process_pygame_events",
|
13
|
+
"handle_play_click",
|
14
|
+
"update_play_hover",
|
15
|
+
"handle_debug_click",
|
16
|
+
"update_debug_hover",
|
17
|
+
]
|
@@ -0,0 +1,96 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import TYPE_CHECKING
|
3
|
+
|
4
|
+
import pygame
|
5
|
+
|
6
|
+
# Use internal imports
|
7
|
+
from ..core import environment as tg_env
|
8
|
+
from ..core import structs as tg_structs
|
9
|
+
from ..visualization import core as vis_core
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from .input_handler import InputHandler
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
def handle_debug_click(event: pygame.event.Event, handler: "InputHandler") -> None:
|
18
|
+
"""Handles mouse clicks in debug mode (toggle triangle state using NumPy arrays)."""
|
19
|
+
if not (event.type == pygame.MOUSEBUTTONDOWN and event.button == 1):
|
20
|
+
return
|
21
|
+
|
22
|
+
game_state = handler.game_state
|
23
|
+
visualizer = handler.visualizer
|
24
|
+
mouse_pos = handler.mouse_pos
|
25
|
+
|
26
|
+
layout_rects = visualizer.ensure_layout()
|
27
|
+
grid_rect = layout_rects.get("grid")
|
28
|
+
if not grid_rect:
|
29
|
+
logger.error("Grid layout rectangle not available for debug click.")
|
30
|
+
return
|
31
|
+
|
32
|
+
grid_coords = vis_core.coord_mapper.get_grid_coords_from_screen(
|
33
|
+
mouse_pos, grid_rect, game_state.env_config
|
34
|
+
)
|
35
|
+
if not grid_coords:
|
36
|
+
return
|
37
|
+
|
38
|
+
r, c = grid_coords
|
39
|
+
if game_state.grid_data.valid(r, c):
|
40
|
+
# Check death zone first
|
41
|
+
if not game_state.grid_data._death_np[r, c]:
|
42
|
+
# Toggle occupancy state in NumPy array
|
43
|
+
current_occupied_state = game_state.grid_data._occupied_np[r, c]
|
44
|
+
new_occupied_state = not current_occupied_state
|
45
|
+
game_state.grid_data._occupied_np[r, c] = new_occupied_state
|
46
|
+
|
47
|
+
# Update color ID based on new state
|
48
|
+
new_color_id = (
|
49
|
+
tg_structs.DEBUG_COLOR_ID
|
50
|
+
if new_occupied_state
|
51
|
+
else tg_structs.NO_COLOR_ID
|
52
|
+
)
|
53
|
+
game_state.grid_data._color_id_np[r, c] = new_color_id
|
54
|
+
|
55
|
+
logger.debug(
|
56
|
+
f": Toggled triangle ({r},{c}) -> {'Occupied' if new_occupied_state else 'Empty'}"
|
57
|
+
)
|
58
|
+
|
59
|
+
# Check for line clears if the cell became occupied
|
60
|
+
if new_occupied_state:
|
61
|
+
# Pass the coordinate tuple in a set
|
62
|
+
lines_cleared, unique_tris_coords, _ = (
|
63
|
+
tg_env.GridLogic.check_and_clear_lines(
|
64
|
+
game_state.grid_data, newly_occupied_coords={(r, c)}
|
65
|
+
)
|
66
|
+
)
|
67
|
+
if lines_cleared > 0:
|
68
|
+
logger.debug(
|
69
|
+
f"Cleared {lines_cleared} lines ({len(unique_tris_coords)} coords) after toggle."
|
70
|
+
)
|
71
|
+
else:
|
72
|
+
logger.info(f"Clicked on death cell ({r},{c}). No action.")
|
73
|
+
|
74
|
+
|
75
|
+
def update_debug_hover(handler: "InputHandler") -> None:
|
76
|
+
"""Updates the debug highlight position within the InputHandler."""
|
77
|
+
handler.debug_highlight_coord = None # Reset hover state
|
78
|
+
|
79
|
+
game_state = handler.game_state
|
80
|
+
visualizer = handler.visualizer
|
81
|
+
mouse_pos = handler.mouse_pos
|
82
|
+
|
83
|
+
layout_rects = visualizer.ensure_layout()
|
84
|
+
grid_rect = layout_rects.get("grid")
|
85
|
+
if not grid_rect or not grid_rect.collidepoint(mouse_pos):
|
86
|
+
return # Not hovering over grid
|
87
|
+
|
88
|
+
grid_coords = vis_core.coord_mapper.get_grid_coords_from_screen(
|
89
|
+
mouse_pos, grid_rect, game_state.env_config
|
90
|
+
)
|
91
|
+
|
92
|
+
if grid_coords:
|
93
|
+
r, c = grid_coords
|
94
|
+
# Highlight only valid, non-death cells
|
95
|
+
if game_state.grid_data.valid(r, c) and not game_state.grid_data.is_death(r, c):
|
96
|
+
handler.debug_highlight_coord = grid_coords
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import logging
|
2
|
+
from collections.abc import Generator
|
3
|
+
from typing import TYPE_CHECKING, Any
|
4
|
+
|
5
|
+
import pygame
|
6
|
+
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
# Use internal import
|
9
|
+
from ..visualization.core.visualizer import Visualizer
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
def process_pygame_events(
|
15
|
+
visualizer: "Visualizer",
|
16
|
+
) -> Generator[pygame.event.Event, Any, bool]:
|
17
|
+
"""
|
18
|
+
Processes basic Pygame events like QUIT, ESCAPE, VIDEORESIZE.
|
19
|
+
Yields other events for mode-specific handlers.
|
20
|
+
Returns False via StopIteration value if the application should quit, True otherwise.
|
21
|
+
"""
|
22
|
+
should_quit = False
|
23
|
+
for event in pygame.event.get():
|
24
|
+
if event.type == pygame.QUIT:
|
25
|
+
logger.info("Received QUIT event.")
|
26
|
+
should_quit = True
|
27
|
+
break
|
28
|
+
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
|
29
|
+
logger.info("Received ESCAPE key press.")
|
30
|
+
should_quit = True
|
31
|
+
break
|
32
|
+
if event.type == pygame.VIDEORESIZE:
|
33
|
+
try:
|
34
|
+
w, h = max(320, event.w), max(240, event.h)
|
35
|
+
visualizer.screen = pygame.display.set_mode((w, h), pygame.RESIZABLE)
|
36
|
+
visualizer.layout_rects = None
|
37
|
+
logger.info(f"Window resized to {w}x{h}")
|
38
|
+
except pygame.error as e:
|
39
|
+
logger.error(f"Error resizing window: {e}")
|
40
|
+
yield event
|
41
|
+
else:
|
42
|
+
yield event
|
43
|
+
return not should_quit
|
@@ -0,0 +1,82 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
import pygame
|
4
|
+
|
5
|
+
# Use internal imports
|
6
|
+
from ..config import EnvConfig
|
7
|
+
from ..core import environment as tg_env
|
8
|
+
from ..core import structs as tg_structs
|
9
|
+
from ..visualization import Visualizer
|
10
|
+
from . import debug_mode_handler, event_processor, play_mode_handler
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class InputHandler:
|
16
|
+
"""
|
17
|
+
Handles user input, manages interaction state (selection, hover),
|
18
|
+
and delegates actions to mode-specific handlers.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
game_state: tg_env.GameState,
|
24
|
+
visualizer: Visualizer,
|
25
|
+
mode: str,
|
26
|
+
env_config: EnvConfig,
|
27
|
+
):
|
28
|
+
self.game_state = game_state
|
29
|
+
self.visualizer = visualizer
|
30
|
+
self.mode = mode
|
31
|
+
self.env_config = env_config
|
32
|
+
|
33
|
+
# Interaction state managed here
|
34
|
+
self.selected_shape_idx: int = -1
|
35
|
+
self.hover_grid_coord: tuple[int, int] | None = None
|
36
|
+
self.hover_is_valid: bool = False
|
37
|
+
self.hover_shape: tg_structs.Shape | None = None
|
38
|
+
self.debug_highlight_coord: tuple[int, int] | None = None
|
39
|
+
self.mouse_pos: tuple[int, int] = (0, 0)
|
40
|
+
|
41
|
+
def handle_input(self) -> bool:
|
42
|
+
"""Processes Pygame events and updates state based on mode. Returns False to quit."""
|
43
|
+
self.mouse_pos = pygame.mouse.get_pos()
|
44
|
+
|
45
|
+
# Reset hover/highlight state each frame before processing events/updates
|
46
|
+
self.hover_grid_coord = None
|
47
|
+
self.hover_is_valid = False
|
48
|
+
self.hover_shape = None
|
49
|
+
self.debug_highlight_coord = None
|
50
|
+
|
51
|
+
running = True
|
52
|
+
event_generator = event_processor.process_pygame_events(self.visualizer)
|
53
|
+
try:
|
54
|
+
while True:
|
55
|
+
event = next(event_generator)
|
56
|
+
# Pass self to handlers so they can modify interaction state
|
57
|
+
if self.mode == "play":
|
58
|
+
play_mode_handler.handle_play_click(event, self)
|
59
|
+
elif self.mode == "debug":
|
60
|
+
debug_mode_handler.handle_debug_click(event, self)
|
61
|
+
except StopIteration as e:
|
62
|
+
running = e.value # False if quit requested
|
63
|
+
|
64
|
+
# Update hover state after processing events
|
65
|
+
if running:
|
66
|
+
if self.mode == "play":
|
67
|
+
play_mode_handler.update_play_hover(self)
|
68
|
+
elif self.mode == "debug":
|
69
|
+
debug_mode_handler.update_debug_hover(self)
|
70
|
+
|
71
|
+
return running
|
72
|
+
|
73
|
+
def get_render_interaction_state(self) -> dict:
|
74
|
+
"""Returns interaction state needed by Visualizer.render"""
|
75
|
+
return {
|
76
|
+
"selected_shape_idx": self.selected_shape_idx,
|
77
|
+
"hover_shape": self.hover_shape,
|
78
|
+
"hover_grid_coord": self.hover_grid_coord,
|
79
|
+
"hover_is_valid": self.hover_is_valid,
|
80
|
+
"hover_screen_pos": self.mouse_pos, # Pass current mouse pos
|
81
|
+
"debug_highlight_coord": self.debug_highlight_coord,
|
82
|
+
}
|
@@ -0,0 +1,141 @@
|
|
1
|
+
import logging
|
2
|
+
from typing import TYPE_CHECKING
|
3
|
+
|
4
|
+
import pygame
|
5
|
+
|
6
|
+
# Use internal imports
|
7
|
+
from ..core import environment as tg_env
|
8
|
+
from ..core import structs as tg_structs
|
9
|
+
from ..visualization import core as vis_core
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from .input_handler import InputHandler
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
def handle_play_click(event: pygame.event.Event, handler: "InputHandler") -> None:
|
18
|
+
"""Handles mouse clicks in play mode (select preview, place shape). Modifies handler state."""
|
19
|
+
if not (event.type == pygame.MOUSEBUTTONDOWN and event.button == 1):
|
20
|
+
return
|
21
|
+
|
22
|
+
game_state = handler.game_state
|
23
|
+
visualizer = handler.visualizer
|
24
|
+
mouse_pos = handler.mouse_pos
|
25
|
+
|
26
|
+
if game_state.is_over():
|
27
|
+
logger.info("Game is over, ignoring click.")
|
28
|
+
return
|
29
|
+
|
30
|
+
layout_rects = visualizer.ensure_layout()
|
31
|
+
grid_rect = layout_rects.get("grid")
|
32
|
+
# Get preview rects from visualizer cache
|
33
|
+
preview_rects = visualizer.preview_rects
|
34
|
+
|
35
|
+
# 1. Check for clicks on shape previews
|
36
|
+
preview_idx = vis_core.coord_mapper.get_preview_index_from_screen(
|
37
|
+
mouse_pos, preview_rects
|
38
|
+
)
|
39
|
+
if preview_idx is not None:
|
40
|
+
if handler.selected_shape_idx == preview_idx:
|
41
|
+
# Clicked selected shape again: deselect
|
42
|
+
handler.selected_shape_idx = -1
|
43
|
+
handler.hover_grid_coord = None # Clear hover state on deselect
|
44
|
+
handler.hover_shape = None
|
45
|
+
logger.info("Deselected shape.")
|
46
|
+
elif (
|
47
|
+
0 <= preview_idx < len(game_state.shapes) and game_state.shapes[preview_idx]
|
48
|
+
):
|
49
|
+
# Clicked a valid, available shape: select it
|
50
|
+
handler.selected_shape_idx = preview_idx
|
51
|
+
logger.info(f"Selected shape index: {preview_idx}")
|
52
|
+
# Immediately update hover based on current mouse pos after selection
|
53
|
+
update_play_hover(handler) # Update hover state within handler
|
54
|
+
else:
|
55
|
+
# Clicked an empty or invalid slot
|
56
|
+
logger.info(f"Clicked empty/invalid preview slot: {preview_idx}")
|
57
|
+
# Deselect if clicking an empty slot while another is selected
|
58
|
+
if handler.selected_shape_idx != -1:
|
59
|
+
handler.selected_shape_idx = -1
|
60
|
+
handler.hover_grid_coord = None
|
61
|
+
handler.hover_shape = None
|
62
|
+
return # Handled preview click
|
63
|
+
|
64
|
+
# 2. Check for clicks on the grid (if a shape is selected)
|
65
|
+
selected_idx = handler.selected_shape_idx
|
66
|
+
if selected_idx != -1 and grid_rect and grid_rect.collidepoint(mouse_pos):
|
67
|
+
# A shape is selected, and the click is within the grid area.
|
68
|
+
grid_coords = vis_core.coord_mapper.get_grid_coords_from_screen(
|
69
|
+
mouse_pos, grid_rect, game_state.env_config
|
70
|
+
)
|
71
|
+
# Use Shape from trianglengin
|
72
|
+
shape_to_place: tg_structs.Shape | None = game_state.shapes[selected_idx]
|
73
|
+
|
74
|
+
# Check if the placement is valid *at the clicked location*
|
75
|
+
if (
|
76
|
+
grid_coords
|
77
|
+
and shape_to_place
|
78
|
+
and tg_env.GridLogic.can_place(
|
79
|
+
game_state.grid_data, shape_to_place, grid_coords[0], grid_coords[1]
|
80
|
+
)
|
81
|
+
):
|
82
|
+
# Valid placement click!
|
83
|
+
r, c = grid_coords
|
84
|
+
action = tg_env.encode_action(selected_idx, r, c, game_state.env_config)
|
85
|
+
# Execute the step using the game state's method
|
86
|
+
reward, done = game_state.step(action) # Now returns (reward, done)
|
87
|
+
logger.info(
|
88
|
+
f"Placed shape {selected_idx} at {grid_coords}. R={reward:.1f}, Done={done}"
|
89
|
+
)
|
90
|
+
# Deselect shape after successful placement
|
91
|
+
handler.selected_shape_idx = -1
|
92
|
+
handler.hover_grid_coord = None # Clear hover state
|
93
|
+
handler.hover_shape = None
|
94
|
+
else:
|
95
|
+
# Clicked grid, shape selected, but not a valid placement spot for the click
|
96
|
+
logger.info(f"Clicked grid at {grid_coords}, but placement invalid.")
|
97
|
+
|
98
|
+
|
99
|
+
def update_play_hover(handler: "InputHandler") -> None:
|
100
|
+
"""Updates the hover state within the InputHandler."""
|
101
|
+
# Reset hover state first
|
102
|
+
handler.hover_grid_coord = None
|
103
|
+
handler.hover_is_valid = False
|
104
|
+
handler.hover_shape = None
|
105
|
+
|
106
|
+
game_state = handler.game_state
|
107
|
+
visualizer = handler.visualizer
|
108
|
+
mouse_pos = handler.mouse_pos
|
109
|
+
|
110
|
+
if game_state.is_over() or handler.selected_shape_idx == -1:
|
111
|
+
return # No hover if game over or no shape selected
|
112
|
+
|
113
|
+
layout_rects = visualizer.ensure_layout()
|
114
|
+
grid_rect = layout_rects.get("grid")
|
115
|
+
if not grid_rect or not grid_rect.collidepoint(mouse_pos):
|
116
|
+
return # Not hovering over grid
|
117
|
+
|
118
|
+
shape_idx = handler.selected_shape_idx
|
119
|
+
if not (0 <= shape_idx < len(game_state.shapes)):
|
120
|
+
return
|
121
|
+
# Use Shape from trianglengin
|
122
|
+
shape: tg_structs.Shape | None = game_state.shapes[shape_idx]
|
123
|
+
if not shape:
|
124
|
+
return
|
125
|
+
|
126
|
+
# Get grid coordinates under mouse
|
127
|
+
grid_coords = vis_core.coord_mapper.get_grid_coords_from_screen(
|
128
|
+
mouse_pos, grid_rect, game_state.env_config
|
129
|
+
)
|
130
|
+
|
131
|
+
if grid_coords:
|
132
|
+
# Check if placement is valid at these coordinates
|
133
|
+
is_valid = tg_env.GridLogic.can_place(
|
134
|
+
game_state.grid_data, shape, grid_coords[0], grid_coords[1]
|
135
|
+
)
|
136
|
+
# Update handler's hover state
|
137
|
+
handler.hover_grid_coord = grid_coords
|
138
|
+
handler.hover_is_valid = is_valid
|
139
|
+
handler.hover_shape = shape # Store the shape being hovered
|
140
|
+
else:
|
141
|
+
handler.hover_shape = shape # Store shape for floating preview
|
@@ -0,0 +1,73 @@
|
|
1
|
+
def is_point_in_polygon(
|
2
|
+
point: tuple[float, float], polygon: list[tuple[float, float]]
|
3
|
+
) -> bool:
|
4
|
+
"""
|
5
|
+
Checks if a point is inside a polygon using the Winding Number algorithm.
|
6
|
+
Handles points on the boundary correctly.
|
7
|
+
|
8
|
+
Args:
|
9
|
+
point: Tuple (x, y) representing the point coordinates.
|
10
|
+
polygon: List of tuples [(x1, y1), (x2, y2), ...] representing polygon vertices in order.
|
11
|
+
|
12
|
+
Returns:
|
13
|
+
True if the point is inside or on the boundary of the polygon, False otherwise.
|
14
|
+
"""
|
15
|
+
x, y = point
|
16
|
+
n = len(polygon)
|
17
|
+
if n < 3: # Need at least 3 vertices for a polygon
|
18
|
+
return False
|
19
|
+
|
20
|
+
wn = 0 # the winding number counter
|
21
|
+
epsilon = 1e-9 # Tolerance for floating point comparisons
|
22
|
+
|
23
|
+
# loop through all edges of the polygon
|
24
|
+
for i in range(n):
|
25
|
+
p1 = polygon[i]
|
26
|
+
p2 = polygon[(i + 1) % n] # Wrap around to the first vertex
|
27
|
+
|
28
|
+
# Check if point is on the vertex P1
|
29
|
+
if abs(p1[0] - x) < epsilon and abs(p1[1] - y) < epsilon:
|
30
|
+
return True
|
31
|
+
|
32
|
+
# Check if point is on the horizontal edge P1P2
|
33
|
+
if (
|
34
|
+
abs(p1[1] - p2[1]) < epsilon
|
35
|
+
and abs(p1[1] - y) < epsilon
|
36
|
+
and min(p1[0], p2[0]) - epsilon <= x <= max(p1[0], p2[0]) + epsilon
|
37
|
+
):
|
38
|
+
return True
|
39
|
+
|
40
|
+
# Check if point is on the vertical edge P1P2
|
41
|
+
if (
|
42
|
+
abs(p1[0] - p2[0]) < epsilon
|
43
|
+
and abs(p1[0] - x) < epsilon
|
44
|
+
and min(p1[1], p2[1]) - epsilon <= y <= max(p1[1], p2[1]) + epsilon
|
45
|
+
):
|
46
|
+
return True
|
47
|
+
|
48
|
+
# Check for intersection using winding number logic
|
49
|
+
# Check y range (inclusive min, exclusive max for upward crossing)
|
50
|
+
y_in_upward_range = p1[1] <= y + epsilon < p2[1] + epsilon
|
51
|
+
y_in_downward_range = p2[1] <= y + epsilon < p1[1] + epsilon
|
52
|
+
|
53
|
+
if y_in_upward_range or y_in_downward_range:
|
54
|
+
# Calculate orientation: > 0 for left turn (counter-clockwise), < 0 for right turn
|
55
|
+
orientation = (p2[0] - p1[0]) * (y - p1[1]) - (x - p1[0]) * (p2[1] - p1[1])
|
56
|
+
|
57
|
+
if (
|
58
|
+
y_in_upward_range and orientation > epsilon
|
59
|
+
): # Upward crossing, P left of edge
|
60
|
+
wn += 1
|
61
|
+
elif (
|
62
|
+
y_in_downward_range and orientation < -epsilon
|
63
|
+
): # Downward crossing, P right of edge
|
64
|
+
wn -= 1
|
65
|
+
elif (
|
66
|
+
abs(orientation) < epsilon
|
67
|
+
and min(p1[0], p2[0]) - epsilon <= x <= max(p1[0], p2[0]) + epsilon
|
68
|
+
and min(p1[1], p2[1]) - epsilon <= y <= max(p1[1], p2[1]) + epsilon
|
69
|
+
):
|
70
|
+
return True # Point is on the edge segment
|
71
|
+
|
72
|
+
# wn == 0 only when P is outside
|
73
|
+
return wn != 0
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# File: trianglengin/visualization/README.md
|
2
|
+
|
3
|
+
# Visualization Module (`trianglengin.visualization`)
|
4
|
+
|
5
|
+
## Purpose and Architecture
|
6
|
+
|
7
|
+
This module is responsible for rendering the game state visually using the Pygame library, specifically for the **interactive modes** (play/debug) provided directly by the `trianglengin` library.
|
8
|
+
|
9
|
+
- **Core Components ([`core/README.md`](core/README.md)):**
|
10
|
+
- `Visualizer`: Orchestrates the rendering process for interactive modes.
|
11
|
+
- `layout`: Calculates the screen positions and sizes for different UI areas.
|
12
|
+
- `fonts`: Loads necessary font files.
|
13
|
+
- `colors`: Defines a centralized palette of RGB color tuples.
|
14
|
+
- `coord_mapper`: Provides functions to map screen coordinates to grid coordinates and preview indices.
|
15
|
+
- **Drawing Components ([`drawing/README.md`](drawing/README.md)):**
|
16
|
+
- Contains specific functions for drawing different elements onto Pygame surfaces (grid, shapes, previews, HUD, highlights).
|
17
|
+
|
18
|
+
**Note:** More advanced visualization components related to training (e.g., dashboards, plots, progress bars) would typically reside in a separate project that uses this engine.
|
19
|
+
|
20
|
+
## Exposed Interfaces
|
21
|
+
|
22
|
+
- **Core Classes & Functions:**
|
23
|
+
- `Visualizer`: Main renderer for interactive modes.
|
24
|
+
- `calculate_interactive_layout`, `calculate_training_layout`: Calculates UI layout rectangles.
|
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.visualization.drawing`)
|
30
|
+
- **Config:**
|
31
|
+
- `DisplayConfig`: Configuration class (re-exported from `trianglengin.config`).
|
32
|
+
- `EnvConfig`: Configuration class (re-exported from `trianglengin.config`).
|
33
|
+
|
34
|
+
## Dependencies
|
35
|
+
|
36
|
+
- **`trianglengin.core`**: `GameState`, `EnvConfig`, `GridData`, `Shape`, `Triangle`.
|
37
|
+
- **`trianglengin.config`**: `DisplayConfig`.
|
38
|
+
- **`trianglengin.utils`**: `geometry` (Planned).
|
39
|
+
- **`pygame`**: The core library used for all drawing, surface manipulation, and font rendering.
|
40
|
+
- **Standard Libraries:** `typing`, `logging`, `math`.
|
41
|
+
|
42
|
+
---
|
43
|
+
|
44
|
+
**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,61 @@
|
|
1
|
+
# trianglengin/visualization/__init__.py
|
2
|
+
"""
|
3
|
+
Visualization module for rendering the game state using Pygame.
|
4
|
+
Provides components for interactive play/debug modes.
|
5
|
+
"""
|
6
|
+
|
7
|
+
# Import core components needed externally
|
8
|
+
# Import DisplayConfig from the new location
|
9
|
+
from ..config import DisplayConfig, EnvConfig
|
10
|
+
from .core import colors
|
11
|
+
from .core.coord_mapper import (
|
12
|
+
get_grid_coords_from_screen,
|
13
|
+
get_preview_index_from_screen,
|
14
|
+
)
|
15
|
+
from .core.fonts import load_fonts
|
16
|
+
from .core.layout import (
|
17
|
+
calculate_interactive_layout,
|
18
|
+
calculate_training_layout,
|
19
|
+
)
|
20
|
+
from .core.visualizer import Visualizer
|
21
|
+
|
22
|
+
# Import drawing functions that might be useful externally (optional)
|
23
|
+
from .drawing.grid import (
|
24
|
+
draw_debug_grid_overlay,
|
25
|
+
draw_grid_background,
|
26
|
+
# draw_grid_indices, # Removed
|
27
|
+
draw_grid_state, # Renamed
|
28
|
+
)
|
29
|
+
from .drawing.highlight import draw_debug_highlight
|
30
|
+
from .drawing.hud import render_hud
|
31
|
+
from .drawing.previews import (
|
32
|
+
draw_floating_preview,
|
33
|
+
draw_placement_preview,
|
34
|
+
render_previews,
|
35
|
+
)
|
36
|
+
from .drawing.shapes import draw_shape
|
37
|
+
|
38
|
+
__all__ = [
|
39
|
+
# Core Renderer & Layout
|
40
|
+
"Visualizer",
|
41
|
+
"calculate_interactive_layout",
|
42
|
+
"calculate_training_layout",
|
43
|
+
"load_fonts",
|
44
|
+
"colors",
|
45
|
+
"get_grid_coords_from_screen",
|
46
|
+
"get_preview_index_from_screen",
|
47
|
+
# Drawing Functions
|
48
|
+
"draw_grid_background",
|
49
|
+
"draw_grid_state", # Export renamed
|
50
|
+
"draw_debug_grid_overlay",
|
51
|
+
# "draw_grid_indices", # Removed
|
52
|
+
"draw_shape",
|
53
|
+
"render_previews",
|
54
|
+
"draw_placement_preview",
|
55
|
+
"draw_floating_preview",
|
56
|
+
"render_hud",
|
57
|
+
"draw_debug_highlight",
|
58
|
+
# Config
|
59
|
+
"DisplayConfig", # Export DisplayConfig
|
60
|
+
"EnvConfig",
|
61
|
+
]
|