trianglengin 2.0.1__cp312-cp312-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-312-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
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
|
+
}
|