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.
Files changed (72) hide show
  1. tests/__init__.py +0 -0
  2. tests/conftest.py +108 -0
  3. tests/core/__init__.py +2 -0
  4. tests/core/environment/README.md +47 -0
  5. tests/core/environment/__init__.py +2 -0
  6. tests/core/environment/test_action_codec.py +50 -0
  7. tests/core/environment/test_game_state.py +483 -0
  8. tests/core/environment/test_grid_data.py +205 -0
  9. tests/core/environment/test_grid_logic.py +362 -0
  10. tests/core/environment/test_shape_logic.py +171 -0
  11. tests/core/environment/test_step.py +372 -0
  12. tests/core/structs/__init__.py +0 -0
  13. tests/core/structs/test_shape.py +83 -0
  14. tests/core/structs/test_triangle.py +97 -0
  15. tests/utils/__init__.py +0 -0
  16. tests/utils/test_geometry.py +93 -0
  17. trianglengin/__init__.py +18 -0
  18. trianglengin/app.py +110 -0
  19. trianglengin/cli.py +134 -0
  20. trianglengin/config/__init__.py +9 -0
  21. trianglengin/config/display_config.py +47 -0
  22. trianglengin/config/env_config.py +103 -0
  23. trianglengin/core/__init__.py +8 -0
  24. trianglengin/core/environment/__init__.py +31 -0
  25. trianglengin/core/environment/action_codec.py +37 -0
  26. trianglengin/core/environment/game_state.py +217 -0
  27. trianglengin/core/environment/grid/README.md +46 -0
  28. trianglengin/core/environment/grid/__init__.py +18 -0
  29. trianglengin/core/environment/grid/grid_data.py +140 -0
  30. trianglengin/core/environment/grid/line_cache.py +189 -0
  31. trianglengin/core/environment/grid/logic.py +131 -0
  32. trianglengin/core/environment/logic/__init__.py +3 -0
  33. trianglengin/core/environment/logic/actions.py +38 -0
  34. trianglengin/core/environment/logic/step.py +134 -0
  35. trianglengin/core/environment/shapes/__init__.py +19 -0
  36. trianglengin/core/environment/shapes/logic.py +84 -0
  37. trianglengin/core/environment/shapes/templates.py +587 -0
  38. trianglengin/core/structs/__init__.py +27 -0
  39. trianglengin/core/structs/constants.py +28 -0
  40. trianglengin/core/structs/shape.py +61 -0
  41. trianglengin/core/structs/triangle.py +48 -0
  42. trianglengin/interaction/README.md +45 -0
  43. trianglengin/interaction/__init__.py +17 -0
  44. trianglengin/interaction/debug_mode_handler.py +96 -0
  45. trianglengin/interaction/event_processor.py +43 -0
  46. trianglengin/interaction/input_handler.py +82 -0
  47. trianglengin/interaction/play_mode_handler.py +141 -0
  48. trianglengin/utils/__init__.py +9 -0
  49. trianglengin/utils/geometry.py +73 -0
  50. trianglengin/utils/types.py +10 -0
  51. trianglengin/visualization/README.md +44 -0
  52. trianglengin/visualization/__init__.py +61 -0
  53. trianglengin/visualization/core/README.md +52 -0
  54. trianglengin/visualization/core/__init__.py +12 -0
  55. trianglengin/visualization/core/colors.py +117 -0
  56. trianglengin/visualization/core/coord_mapper.py +73 -0
  57. trianglengin/visualization/core/fonts.py +55 -0
  58. trianglengin/visualization/core/layout.py +101 -0
  59. trianglengin/visualization/core/visualizer.py +232 -0
  60. trianglengin/visualization/drawing/README.md +45 -0
  61. trianglengin/visualization/drawing/__init__.py +30 -0
  62. trianglengin/visualization/drawing/grid.py +156 -0
  63. trianglengin/visualization/drawing/highlight.py +30 -0
  64. trianglengin/visualization/drawing/hud.py +39 -0
  65. trianglengin/visualization/drawing/previews.py +172 -0
  66. trianglengin/visualization/drawing/shapes.py +36 -0
  67. trianglengin-1.0.6.dist-info/METADATA +367 -0
  68. trianglengin-1.0.6.dist-info/RECORD +72 -0
  69. trianglengin-1.0.6.dist-info/WHEEL +5 -0
  70. trianglengin-1.0.6.dist-info/entry_points.txt +2 -0
  71. trianglengin-1.0.6.dist-info/licenses/LICENSE +22 -0
  72. 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,9 @@
1
+ """
2
+ General utilities for the Triangle Engine.
3
+ """
4
+
5
+ # --- ADDED: Export geometry ---
6
+ from . import geometry
7
+ from .types import ActionType
8
+
9
+ __all__ = ["ActionType", "geometry"]
@@ -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,10 @@
1
+ # File: trianglengin/trianglengin/utils/types.py
2
+ # Placeholder for Phase 1
3
+ """
4
+ Shared type definitions for the Triangle Engine.
5
+ """
6
+
7
+ from typing import TypeAlias
8
+
9
+ # Action representation (integer index)
10
+ ActionType: TypeAlias = int
@@ -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
+ ]