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.
Files changed (58) hide show
  1. trianglengin/__init__.py +35 -0
  2. trianglengin/config/README.md +38 -0
  3. trianglengin/config/__init__.py +8 -0
  4. trianglengin/config/display_config.py +47 -0
  5. trianglengin/config/env_config.py +62 -0
  6. trianglengin/core/__init__.py +10 -0
  7. trianglengin/cpp/CMakeLists.txt +42 -0
  8. trianglengin/cpp/bindings.cpp +211 -0
  9. trianglengin/cpp/config.h +28 -0
  10. trianglengin/cpp/game_state.cpp +327 -0
  11. trianglengin/cpp/game_state.h +73 -0
  12. trianglengin/cpp/grid_data.cpp +239 -0
  13. trianglengin/cpp/grid_data.h +78 -0
  14. trianglengin/cpp/grid_logic.cpp +125 -0
  15. trianglengin/cpp/grid_logic.h +30 -0
  16. trianglengin/cpp/shape_logic.cpp +100 -0
  17. trianglengin/cpp/shape_logic.h +28 -0
  18. trianglengin/cpp/structs.h +40 -0
  19. trianglengin/game_interface.py +222 -0
  20. trianglengin/py.typed +0 -0
  21. trianglengin/trianglengin_cpp.cpython-310-darwin.so +0 -0
  22. trianglengin/ui/README.md +35 -0
  23. trianglengin/ui/__init__.py +21 -0
  24. trianglengin/ui/app.py +107 -0
  25. trianglengin/ui/cli.py +123 -0
  26. trianglengin/ui/config.py +44 -0
  27. trianglengin/ui/interaction/README.md +44 -0
  28. trianglengin/ui/interaction/__init__.py +19 -0
  29. trianglengin/ui/interaction/debug_mode_handler.py +72 -0
  30. trianglengin/ui/interaction/event_processor.py +49 -0
  31. trianglengin/ui/interaction/input_handler.py +89 -0
  32. trianglengin/ui/interaction/play_mode_handler.py +156 -0
  33. trianglengin/ui/visualization/README.md +42 -0
  34. trianglengin/ui/visualization/__init__.py +58 -0
  35. trianglengin/ui/visualization/core/README.md +51 -0
  36. trianglengin/ui/visualization/core/__init__.py +16 -0
  37. trianglengin/ui/visualization/core/colors.py +115 -0
  38. trianglengin/ui/visualization/core/coord_mapper.py +85 -0
  39. trianglengin/ui/visualization/core/fonts.py +65 -0
  40. trianglengin/ui/visualization/core/layout.py +77 -0
  41. trianglengin/ui/visualization/core/visualizer.py +248 -0
  42. trianglengin/ui/visualization/drawing/README.md +49 -0
  43. trianglengin/ui/visualization/drawing/__init__.py +43 -0
  44. trianglengin/ui/visualization/drawing/grid.py +213 -0
  45. trianglengin/ui/visualization/drawing/highlight.py +31 -0
  46. trianglengin/ui/visualization/drawing/hud.py +43 -0
  47. trianglengin/ui/visualization/drawing/previews.py +181 -0
  48. trianglengin/ui/visualization/drawing/shapes.py +46 -0
  49. trianglengin/ui/visualization/drawing/utils.py +23 -0
  50. trianglengin/utils/__init__.py +9 -0
  51. trianglengin/utils/geometry.py +62 -0
  52. trianglengin/utils/types.py +10 -0
  53. trianglengin-2.0.1.dist-info/METADATA +250 -0
  54. trianglengin-2.0.1.dist-info/RECORD +58 -0
  55. trianglengin-2.0.1.dist-info/WHEEL +5 -0
  56. trianglengin-2.0.1.dist-info/entry_points.txt +2 -0
  57. trianglengin-2.0.1.dist-info/licenses/LICENSE +22 -0
  58. trianglengin-2.0.1.dist-info/top_level.txt +1 -0
trianglengin/ui/app.py ADDED
@@ -0,0 +1,107 @@
1
+ # File: src/trianglengin/ui/app.py
2
+ import logging
3
+
4
+ # UI imports are now direct as dependencies are required
5
+ import pygame
6
+
7
+ # Use absolute imports from core engine
8
+ from trianglengin.config import EnvConfig
9
+ from trianglengin.game_interface import GameState
10
+ from trianglengin.ui import config as ui_config
11
+ from trianglengin.ui import interaction, visualization
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class Application:
17
+ """Main application integrating visualization and interaction for trianglengin."""
18
+
19
+ def __init__(self, mode: str = "play") -> None:
20
+ self.display_config = ui_config.DisplayConfig() # Use UI config
21
+ self.env_config = EnvConfig() # Use core EnvConfig
22
+ self.mode = mode
23
+
24
+ pygame.init()
25
+ pygame.font.init()
26
+ self.screen = self._setup_screen()
27
+ self.clock = pygame.time.Clock()
28
+ self.fonts = visualization.load_fonts() # From ui.visualization
29
+
30
+ if self.mode in ["play", "debug"]:
31
+ # GameState comes from the core engine
32
+ self.game_state = GameState(self.env_config)
33
+ # Visualizer and InputHandler come from the UI package
34
+ self.visualizer = visualization.Visualizer(
35
+ self.screen,
36
+ self.display_config,
37
+ self.env_config,
38
+ self.fonts,
39
+ )
40
+ self.input_handler = interaction.InputHandler(
41
+ self.game_state, self.visualizer, self.mode, self.env_config
42
+ )
43
+ else:
44
+ logger.error(f"Unsupported application mode: {self.mode}")
45
+ raise ValueError(f"Unsupported application mode: {self.mode}")
46
+
47
+ self.running = True
48
+
49
+ def _setup_screen(self) -> pygame.Surface:
50
+ """Initializes the Pygame screen."""
51
+ screen = pygame.display.set_mode(
52
+ (
53
+ self.display_config.SCREEN_WIDTH,
54
+ self.display_config.SCREEN_HEIGHT,
55
+ ),
56
+ pygame.RESIZABLE,
57
+ )
58
+ pygame.display.set_caption(f"Triangle Engine - {self.mode.capitalize()} Mode")
59
+ return screen
60
+
61
+ def run(self) -> None:
62
+ """Main application loop."""
63
+ logger.info(f"Starting application in {self.mode} mode.")
64
+ while self.running:
65
+ self.clock.tick(self.display_config.FPS)
66
+
67
+ if self.input_handler:
68
+ self.running = self.input_handler.handle_input()
69
+ if not self.running:
70
+ break
71
+ else:
72
+ # Basic event handling if input_handler fails (shouldn't happen)
73
+ for event in pygame.event.get():
74
+ if event.type == pygame.QUIT:
75
+ self.running = False
76
+ if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
77
+ self.running = False
78
+ if event.type == pygame.VIDEORESIZE and self.visualizer:
79
+ try:
80
+ w, h = max(320, event.w), max(240, event.h)
81
+ self.visualizer.screen = pygame.display.set_mode(
82
+ (w, h), pygame.RESIZABLE
83
+ )
84
+ self.visualizer.layout_rects = None
85
+ except pygame.error as e:
86
+ logger.error(f"Error resizing window: {e}")
87
+ if not self.running:
88
+ break
89
+
90
+ if (
91
+ self.mode in ["play", "debug"]
92
+ and self.visualizer
93
+ and self.game_state
94
+ and self.input_handler
95
+ ):
96
+ interaction_render_state = (
97
+ self.input_handler.get_render_interaction_state()
98
+ )
99
+ self.visualizer.render(
100
+ self.game_state,
101
+ self.mode,
102
+ **interaction_render_state,
103
+ )
104
+ pygame.display.flip()
105
+
106
+ logger.info("Application loop finished.")
107
+ pygame.quit()
trianglengin/ui/cli.py ADDED
@@ -0,0 +1,123 @@
1
+ # File: src/trianglengin/ui/cli.py
2
+ import logging
3
+ import random
4
+ import sys
5
+ from typing import Annotated
6
+
7
+ import numpy as np
8
+ import typer # Now a required dependency
9
+
10
+ # Use absolute imports from core engine
11
+ from trianglengin.config import EnvConfig
12
+
13
+ # Import Application directly
14
+ from trianglengin.ui.app import Application
15
+
16
+ app = typer.Typer(
17
+ name="trianglengin",
18
+ help="Core Triangle Engine - Interactive Modes.",
19
+ add_completion=False,
20
+ )
21
+
22
+ LogLevelOption = Annotated[
23
+ str,
24
+ typer.Option(
25
+ "--log-level",
26
+ "-l",
27
+ help="Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL).",
28
+ case_sensitive=False,
29
+ ),
30
+ ]
31
+
32
+ SeedOption = Annotated[
33
+ int,
34
+ typer.Option(
35
+ "--seed",
36
+ "-s",
37
+ help="Random seed for C++ engine initialization.",
38
+ ),
39
+ ]
40
+
41
+
42
+ def setup_logging(log_level_str: str) -> None:
43
+ """Configures root logger based on string level."""
44
+ log_level_str = log_level_str.upper()
45
+ log_level_map = {
46
+ "DEBUG": logging.DEBUG,
47
+ "INFO": logging.INFO,
48
+ "WARNING": logging.WARNING,
49
+ "ERROR": logging.ERROR,
50
+ "CRITICAL": logging.CRITICAL,
51
+ }
52
+ log_level = log_level_map.get(log_level_str, logging.INFO)
53
+ logging.basicConfig(
54
+ level=log_level,
55
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
56
+ handlers=[logging.StreamHandler(sys.stdout)],
57
+ force=True,
58
+ )
59
+ # Set pygame log level (pygame is now a required dependency)
60
+
61
+ logging.getLogger("pygame").setLevel(logging.WARNING)
62
+ logging.info(f"Root logger level set to {logging.getLevelName(log_level)}")
63
+
64
+
65
+ def run_interactive_mode(mode: str, seed: int, log_level: str) -> None:
66
+ """Runs the interactive application."""
67
+ setup_logging(log_level)
68
+ logger = logging.getLogger(__name__)
69
+ logger.info(f"Running Triangle Engine in {mode.capitalize()} mode...")
70
+
71
+ # --- Seeding ---
72
+ try:
73
+ random.seed(seed)
74
+ # Use modern NumPy seeding
75
+ np.random.default_rng(seed)
76
+ logger.info(
77
+ f"Set Python random seed to {seed}. NumPy RNG initialized. C++ engine seeded separately."
78
+ )
79
+ except Exception as e:
80
+ logger.error(f"Error setting Python/NumPy seeds: {e}")
81
+ # --- End Seeding ---
82
+
83
+ try:
84
+ # Validate core EnvConfig
85
+ _ = EnvConfig()
86
+ logger.info("EnvConfig validated.")
87
+ except Exception as e:
88
+ logger.critical(f"EnvConfig validation failed: {e}", exc_info=True)
89
+ sys.exit(1)
90
+
91
+ try:
92
+ # Application is part of the UI package
93
+ app_instance = Application(mode=mode)
94
+ app_instance.run()
95
+ # Keep general exception handling
96
+ except Exception as e:
97
+ logger.critical(f"An unhandled error occurred: {e}", exc_info=True)
98
+ sys.exit(1)
99
+
100
+ logger.info("Exiting.")
101
+ sys.exit(0)
102
+
103
+
104
+ @app.command()
105
+ def play(
106
+ log_level: LogLevelOption = "INFO",
107
+ seed: SeedOption = 42,
108
+ ) -> None:
109
+ """Run the game in interactive Play mode."""
110
+ run_interactive_mode(mode="play", seed=seed, log_level=log_level)
111
+
112
+
113
+ @app.command()
114
+ def debug(
115
+ log_level: LogLevelOption = "DEBUG",
116
+ seed: SeedOption = 42,
117
+ ) -> None:
118
+ """Run the game in interactive Debug mode."""
119
+ run_interactive_mode(mode="debug", seed=seed, log_level=log_level)
120
+
121
+
122
+ if __name__ == "__main__":
123
+ app()
@@ -0,0 +1,44 @@
1
+ # src/trianglengin/ui/config.py
2
+ """
3
+ Configuration specific to display and visualization settings for the UI.
4
+ """
5
+
6
+ import pygame # Direct import
7
+ from pydantic import BaseModel, Field
8
+
9
+ # Initialize Pygame font module if not already done (safe to call multiple times)
10
+ pygame.font.init()
11
+
12
+ # Define a placeholder font loading function or load directly here
13
+ # In a real app, this might load from files or use system fonts more robustly.
14
+ try:
15
+ DEBUG_FONT_DEFAULT = pygame.font.SysFont("monospace", 12)
16
+ except Exception:
17
+ DEBUG_FONT_DEFAULT = pygame.font.Font(None, 15) # Fallback default pygame font
18
+
19
+
20
+ class DisplayConfig(BaseModel):
21
+ """Configuration for visualization display settings."""
22
+
23
+ # Screen and Layout
24
+ SCREEN_WIDTH: int = Field(default=1024, gt=0)
25
+ SCREEN_HEIGHT: int = Field(default=768, gt=0)
26
+ FPS: int = Field(default=60, gt=0)
27
+ PADDING: int = Field(default=10, ge=0)
28
+ HUD_HEIGHT: int = Field(default=30, ge=0)
29
+ PREVIEW_AREA_WIDTH: int = Field(default=150, ge=50)
30
+ PREVIEW_PADDING: int = Field(default=5, ge=0)
31
+ PREVIEW_INNER_PADDING: int = Field(default=3, ge=0)
32
+ PREVIEW_BORDER_WIDTH: int = Field(default=1, ge=0)
33
+ PREVIEW_SELECTED_BORDER_WIDTH: int = Field(default=3, ge=0)
34
+
35
+ # Fonts
36
+ # Use default=None and validate/load later if pygame is available
37
+ DEBUG_FONT: pygame.font.Font | None = Field(default=DEBUG_FONT_DEFAULT)
38
+
39
+ class Config:
40
+ arbitrary_types_allowed = True # Allow pygame.font.Font
41
+
42
+
43
+ # Optional: Create a default instance for easy import elsewhere
44
+ DEFAULT_DISPLAY_CONFIG = DisplayConfig()
@@ -0,0 +1,44 @@
1
+
2
+ # UI Interaction Module (`trianglengin.ui.interaction`)
3
+
4
+ **Requires:** `pygame`
5
+
6
+ ## Purpose
7
+
8
+ This submodule handles user input (keyboard and mouse) for the interactive modes ("play", "debug") provided by the `trianglengin.ui` package. It bridges the gap between raw Pygame events and actions within the game simulation (`GameState` from the core engine).
9
+
10
+ - **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.
11
+ - **Input Handler:** The [`InputHandler`](input_handler.py) class is the main entry point for this submodule.
12
+ - It receives Pygame events (via the `event_processor`).
13
+ - It **manages interaction-specific state** internally (e.g., `selected_shape_idx`, `hover_grid_coord`, `debug_highlight_coord`).
14
+ - It determines the current interaction mode ("play" or "debug") and delegates event handling and hover updates to specific handler functions ([`play_mode_handler.py`](play_mode_handler.py), [`debug_mode_handler.py`](debug_mode_handler.py)).
15
+ - It provides the necessary interaction state to the `trianglengin.ui.visualization.Visualizer` for rendering feedback (hover previews, selection highlights).
16
+ - **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 core `GameState`.
17
+ - `play`: Handles selecting shapes, checking placement validity, and triggering `GameState.step` on valid clicks. Updates hover state in the `InputHandler`.
18
+ - `debug`: Handles toggling the state of individual triangles directly using `GameState.debug_toggle_cell`. Updates hover state in the `InputHandler`.
19
+ - **Decoupling:** It separates input handling logic from the core game simulation (`trianglengin`) and rendering (`trianglengin.ui.visualization`), although it needs references to both to function within the UI application.
20
+
21
+ ## Exposed Interfaces
22
+
23
+ - **Classes:**
24
+ - `InputHandler`:
25
+ - `__init__(game_state: GameState, visualizer: Visualizer, mode: str, env_config: EnvConfig)`
26
+ - `handle_input() -> bool`: Processes events for one frame, returns `False` if quitting.
27
+ - `get_render_interaction_state() -> dict`: Returns interaction state needed by `Visualizer.render`.
28
+ - **Functions:**
29
+ - `process_pygame_events(visualizer: Visualizer) -> Generator[pygame.event.Event, Any, bool]`: Processes common events, yields others.
30
+ - `handle_play_click(event: pygame.event.Event, handler: InputHandler)`: Handles clicks in play mode.
31
+ - `update_play_hover(handler: InputHandler)`: Updates hover state in play mode.
32
+ - `handle_debug_click(event: pygame.event.Event, handler: InputHandler)`: Handles clicks in debug mode.
33
+ - `update_debug_hover(handler: InputHandler)`: Updates hover state in debug mode.
34
+
35
+ ## Dependencies
36
+
37
+ - **`trianglengin` (core):** `GameState`, `EnvConfig`, `Shape`.
38
+ - **`trianglengin.ui.visualization`**: `Visualizer`, `coord_mapper`.
39
+ - **`pygame`**: Relies heavily on Pygame for event handling (`pygame.event`, `pygame.mouse`) and constants (`MOUSEBUTTONDOWN`, `KEYDOWN`, etc.).
40
+ - **Standard Libraries:** `typing`, `logging`.
41
+
42
+ ---
43
+
44
+ **Note:** Please keep this README updated when adding new interaction modes, changing input handling logic, or modifying the interfaces between interaction, environment, and visualization.
@@ -0,0 +1,19 @@
1
+ # src/trianglengin/ui/interaction/__init__.py
2
+ """
3
+ Interaction handling module for interactive modes (play/debug).
4
+ """
5
+
6
+ # Direct imports - if pygame is missing, these will fail.
7
+ from .debug_mode_handler import handle_debug_click, update_debug_hover
8
+ from .event_processor import process_pygame_events
9
+ from .input_handler import InputHandler
10
+ from .play_mode_handler import handle_play_click, update_play_hover
11
+
12
+ __all__ = [
13
+ "InputHandler",
14
+ "process_pygame_events",
15
+ "handle_play_click",
16
+ "update_play_hover",
17
+ "handle_debug_click",
18
+ "update_debug_hover",
19
+ ]
@@ -0,0 +1,72 @@
1
+ # src/trianglengin/ui/interaction/debug_mode_handler.py
2
+ import logging
3
+ from typing import TYPE_CHECKING
4
+
5
+ # Imports are now direct
6
+ import pygame
7
+
8
+ # Use absolute imports for core components
9
+ from trianglengin.ui.visualization import core as vis_core
10
+
11
+ if TYPE_CHECKING:
12
+ # Use absolute import for InputHandler as well
13
+ from trianglengin.game_interface import GameState
14
+ from trianglengin.ui.interaction.input_handler import InputHandler
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def handle_debug_click(event: pygame.event.Event, handler: "InputHandler") -> None:
20
+ """Handles mouse clicks in debug mode (toggle triangle state via C++)."""
21
+ if not (event.type == pygame.MOUSEBUTTONDOWN and event.button == 1):
22
+ return
23
+
24
+ game_state: GameState = handler.game_state # Type hint uses wrapper
25
+ visualizer = handler.visualizer
26
+ mouse_pos = handler.mouse_pos
27
+
28
+ layout_rects = visualizer.ensure_layout()
29
+ grid_rect = layout_rects.get("grid")
30
+ if not grid_rect:
31
+ logger.error("Grid layout rectangle not available for debug click.")
32
+ return
33
+
34
+ grid_coords = vis_core.coord_mapper.get_grid_coords_from_screen(
35
+ mouse_pos, grid_rect, game_state.env_config
36
+ )
37
+ if not grid_coords:
38
+ return
39
+
40
+ r, c = grid_coords
41
+ try:
42
+ # Call the debug toggle method on the wrapper, which calls C++
43
+ game_state.debug_toggle_cell(r, c)
44
+ logger.debug(f"Debug: Toggled cell ({r},{c}) via C++ backend.")
45
+ except Exception as e:
46
+ logger.error(f"Error calling debug_toggle_cell for ({r},{c}): {e}")
47
+
48
+
49
+ def update_debug_hover(handler: "InputHandler") -> None:
50
+ """Updates the debug highlight position within the InputHandler."""
51
+ handler.debug_highlight_coord = None
52
+
53
+ game_state: GameState = handler.game_state # Type hint uses wrapper
54
+ visualizer = handler.visualizer
55
+ mouse_pos = handler.mouse_pos
56
+
57
+ layout_rects = visualizer.ensure_layout()
58
+ grid_rect = layout_rects.get("grid")
59
+ if not grid_rect or not grid_rect.collidepoint(mouse_pos):
60
+ return
61
+
62
+ grid_coords = vis_core.coord_mapper.get_grid_coords_from_screen(
63
+ mouse_pos, grid_rect, game_state.env_config
64
+ )
65
+
66
+ if grid_coords:
67
+ r, c = grid_coords
68
+ # Get grid data to check validity and death zone status
69
+ grid_data_np = game_state.get_grid_data_np()
70
+ rows, cols = grid_data_np["death"].shape
71
+ if 0 <= r < rows and 0 <= c < cols and not grid_data_np["death"][r, c]:
72
+ handler.debug_highlight_coord = grid_coords
@@ -0,0 +1,49 @@
1
+ import logging
2
+ from collections.abc import Generator
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ # Guard UI imports
6
+ try:
7
+ import pygame
8
+ except ImportError as e:
9
+ raise ImportError(
10
+ "UI components require 'pygame'. Install with 'pip install trianglengin[ui]'."
11
+ ) from e
12
+
13
+ if TYPE_CHECKING:
14
+ # Use internal import relative to UI package
15
+ from ..visualization.core.visualizer import Visualizer
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def process_pygame_events(
21
+ visualizer: "Visualizer",
22
+ ) -> Generator[pygame.event.Event, Any, bool]:
23
+ """
24
+ Processes basic Pygame events like QUIT, ESCAPE, VIDEORESIZE.
25
+ Yields other events for mode-specific handlers.
26
+ Returns False via StopIteration value if the application should quit, True otherwise.
27
+ """
28
+ should_quit = False
29
+ for event in pygame.event.get():
30
+ if event.type == pygame.QUIT:
31
+ logger.info("Received QUIT event.")
32
+ should_quit = True
33
+ break
34
+ if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
35
+ logger.info("Received ESCAPE key press.")
36
+ should_quit = True
37
+ break
38
+ if event.type == pygame.VIDEORESIZE:
39
+ try:
40
+ w, h = max(320, event.w), max(240, event.h)
41
+ visualizer.screen = pygame.display.set_mode((w, h), pygame.RESIZABLE)
42
+ visualizer.layout_rects = None
43
+ logger.info(f"Window resized to {w}x{h}")
44
+ except pygame.error as e:
45
+ logger.error(f"Error resizing window: {e}")
46
+ yield event
47
+ else:
48
+ yield event
49
+ return not should_quit
@@ -0,0 +1,89 @@
1
+ import logging
2
+
3
+ # Use absolute imports for core components
4
+ from trianglengin.config import EnvConfig
5
+ from trianglengin.game_interface import GameState, Shape
6
+
7
+ # Guard UI imports
8
+ try:
9
+ import pygame
10
+
11
+ from ..visualization import Visualizer # Relative import within UI package
12
+ from . import debug_mode_handler, event_processor, play_mode_handler # Relative
13
+ except ImportError as e:
14
+ raise ImportError(
15
+ "UI components require 'pygame'. Install with 'pip install trianglengin[ui]'."
16
+ ) from e
17
+
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class InputHandler:
23
+ """
24
+ Handles user input, manages interaction state (selection, hover),
25
+ and delegates actions to mode-specific handlers. Uses the GameState wrapper.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ game_state: GameState, # Uses the core GameState wrapper
31
+ visualizer: Visualizer, # Uses the UI Visualizer
32
+ mode: str,
33
+ env_config: EnvConfig, # Uses the core EnvConfig
34
+ ):
35
+ self.game_state = game_state
36
+ self.visualizer = visualizer
37
+ self.mode = mode
38
+ self.env_config = env_config
39
+
40
+ # Interaction state managed here
41
+ self.selected_shape_idx: int = -1
42
+ self.hover_grid_coord: tuple[int, int] | None = None
43
+ self.hover_is_valid: bool = False
44
+ self.hover_shape: Shape | None = None # Uses the core Shape struct
45
+ self.debug_highlight_coord: tuple[int, int] | None = None
46
+ self.mouse_pos: tuple[int, int] = (0, 0)
47
+
48
+ def handle_input(self) -> bool:
49
+ """Processes Pygame events and updates state based on mode. Returns False to quit."""
50
+ self.mouse_pos = pygame.mouse.get_pos()
51
+
52
+ # Reset hover/highlight state each frame before processing events/updates
53
+ self.hover_grid_coord = None
54
+ self.hover_is_valid = False
55
+ self.hover_shape = None
56
+ self.debug_highlight_coord = None
57
+
58
+ running = True
59
+ event_generator = event_processor.process_pygame_events(self.visualizer)
60
+ try:
61
+ while True:
62
+ event = next(event_generator)
63
+ # Pass self to handlers so they can modify interaction state
64
+ if self.mode == "play":
65
+ play_mode_handler.handle_play_click(event, self)
66
+ elif self.mode == "debug":
67
+ debug_mode_handler.handle_debug_click(event, self)
68
+ except StopIteration as e:
69
+ running = e.value # False if quit requested
70
+
71
+ # Update hover state after processing events
72
+ if running:
73
+ if self.mode == "play":
74
+ play_mode_handler.update_play_hover(self)
75
+ elif self.mode == "debug":
76
+ debug_mode_handler.update_debug_hover(self)
77
+
78
+ return running
79
+
80
+ def get_render_interaction_state(self) -> dict:
81
+ """Returns interaction state needed by Visualizer.render"""
82
+ return {
83
+ "selected_shape_idx": self.selected_shape_idx,
84
+ "hover_shape": self.hover_shape,
85
+ "hover_grid_coord": self.hover_grid_coord,
86
+ "hover_is_valid": self.hover_is_valid,
87
+ "hover_screen_pos": self.mouse_pos,
88
+ "debug_highlight_coord": self.debug_highlight_coord,
89
+ }