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,52 @@
1
+
2
+ # Visualization Core Submodule (`trianglengin.visualization.core`)
3
+
4
+ ## Purpose and Architecture
5
+
6
+ This submodule contains the central classes and foundational elements for the **interactive** visualization system within the `trianglengin` library. It orchestrates rendering for play/debug modes, manages layout and coordinate systems, and defines core visual properties like colors and fonts.
7
+
8
+ **Note:** Training-specific visualization components (like dashboards, plots) would typically reside in a separate project using this engine.
9
+
10
+ - **Render Orchestration:**
11
+ - [`Visualizer`](visualizer.py): The main class for rendering in **interactive modes** ("play", "debug"). It maintains the Pygame screen, calculates layout using `layout.py`, manages cached preview area rectangles, and calls appropriate drawing functions from [`trianglengin.visualization.drawing`](../drawing/README.md). **It receives interaction state (hover position, selected index) via its `render` method to display visual feedback.**
12
+ - **Layout Management:**
13
+ - [`layout.py`](layout.py): Contains functions (`calculate_interactive_layout`, `calculate_training_layout`) to determine the size and position of the main UI areas based on the screen dimensions, mode, and `DisplayConfig`.
14
+ - **Coordinate System:**
15
+ - [`coord_mapper.py`](coord_mapper.py): Provides essential mapping functions:
16
+ - `_calculate_render_params`: Internal helper to get scaling and offset for grid rendering.
17
+ - `get_grid_coords_from_screen`: Converts mouse/screen coordinates into logical grid (row, column) coordinates.
18
+ - `get_preview_index_from_screen`: Converts mouse/screen coordinates into the index of the shape preview slot being pointed at.
19
+ - **Visual Properties:**
20
+ - [`colors.py`](colors.py): Defines a centralized palette of named color constants (RGB tuples).
21
+ - [`fonts.py`](fonts.py): Contains the `load_fonts` function to load and manage Pygame font objects.
22
+
23
+ ## Exposed Interfaces
24
+
25
+ - **Classes:**
26
+ - `Visualizer`: Renderer for interactive modes.
27
+ - `__init__(...)`
28
+ - `render(game_state: GameState, mode: str, **interaction_state)`: Renders based on game state and interaction hints.
29
+ - `ensure_layout() -> Dict[str, pygame.Rect]`
30
+ - `screen`: Public attribute (Pygame Surface).
31
+ - `preview_rects`: Public attribute (cached preview area rects).
32
+ - **Functions:**
33
+ - `calculate_interactive_layout(...) -> Dict[str, pygame.Rect]`
34
+ - `calculate_training_layout(...) -> Dict[str, pygame.Rect]` (Kept for potential future use)
35
+ - `load_fonts() -> Dict[str, Optional[pygame.font.Font]]`
36
+ - `get_grid_coords_from_screen(...) -> Optional[Tuple[int, int]]`
37
+ - `get_preview_index_from_screen(...) -> Optional[int]`
38
+ - **Modules:**
39
+ - `colors`: Provides color constants (e.g., `colors.RED`).
40
+
41
+ ## Dependencies
42
+
43
+ - **`trianglengin.core`**: `GameState`, `EnvConfig`, `GridData`, `Shape`, `Triangle`.
44
+ - **`trianglengin.config`**: `DisplayConfig`.
45
+ - **`trianglengin.utils`**: `geometry` (Planned).
46
+ - **[`trianglengin.visualization.drawing`](../drawing/README.md)**: Drawing functions are called by `Visualizer`.
47
+ - **`pygame`**: Used for surfaces, rectangles, fonts, display management.
48
+ - **Standard Libraries:** `typing`, `logging`, `math`.
49
+
50
+ ---
51
+
52
+ **Note:** Please keep this README updated when changing the core rendering logic, layout calculations, coordinate mapping, or the interfaces of the renderers. Accurate documentation is crucial for maintainability.
@@ -0,0 +1,12 @@
1
+ """Core visualization components: renderers, layout, fonts, colors, coordinate mapping."""
2
+
3
+ from . import colors, coord_mapper, fonts, layout
4
+ from .visualizer import Visualizer
5
+
6
+ __all__ = [
7
+ "Visualizer",
8
+ "layout",
9
+ "fonts",
10
+ "colors",
11
+ "coord_mapper",
12
+ ]
@@ -0,0 +1,117 @@
1
+ # trianglengin/visualization/core/colors.py
2
+ """
3
+ Defines color constants and mappings used throughout the application,
4
+ especially for visualization.
5
+ """
6
+
7
+ # Define the Color type alias (RGB)
8
+ Color = tuple[int, int, int]
9
+ # Define RGBA type alias for clarity where alpha is used
10
+ ColorRGBA = tuple[int, int, int, int]
11
+
12
+ # --- Standard Colors ---
13
+ WHITE: Color = (255, 255, 255)
14
+ BLACK: Color = (0, 0, 0)
15
+ GRAY: Color = (128, 128, 128)
16
+ LIGHT_GRAY: Color = (200, 200, 200)
17
+ DARK_GRAY: Color = (50, 50, 50)
18
+ RED: Color = (255, 0, 0)
19
+ GREEN: Color = (0, 255, 0)
20
+ BLUE: Color = (0, 0, 255)
21
+ YELLOW: Color = (255, 255, 0)
22
+ CYAN: Color = (0, 255, 255)
23
+ MAGENTA: Color = (255, 0, 255)
24
+ ORANGE: Color = (255, 165, 0)
25
+ PURPLE: Color = (128, 0, 128)
26
+ TEAL: Color = (0, 128, 128)
27
+ OLIVE: Color = (128, 128, 0)
28
+
29
+ # --- Game Specific Colors ---
30
+ # Colors used for the placeable shapes (ensure these match constants.py if needed)
31
+ SHAPE_COLORS: tuple[Color, ...] = (
32
+ (220, 40, 40), # 0: Red
33
+ (60, 60, 220), # 1: Blue
34
+ (40, 200, 40), # 2: Green
35
+ (230, 230, 40), # 3: Yellow
36
+ (240, 150, 20), # 4: Orange
37
+ (140, 40, 140), # 5: Purple
38
+ (40, 200, 200), # 6: Cyan
39
+ (200, 100, 180), # 7: Pink
40
+ (100, 180, 200), # 8: Light Blue
41
+ )
42
+
43
+ # Mapping from shape colors to integer IDs (0 to N-1)
44
+ COLOR_TO_ID_MAP: dict[Color, int] = {color: i for i, color in enumerate(SHAPE_COLORS)}
45
+ # Reverse mapping for convenience (e.g., visualization)
46
+ ID_TO_COLOR_MAP: dict[int, Color] = dict(enumerate(SHAPE_COLORS))
47
+
48
+ # Special Color IDs (ensure these match constants.py)
49
+ NO_COLOR_ID: int = -1
50
+ DEBUG_COLOR_ID: int = -2
51
+
52
+ # Grid background colors
53
+ GRID_BG_LIGHT: Color = (40, 40, 40)
54
+ GRID_BG_DARK: Color = (30, 30, 30)
55
+ GRID_LINE_COLOR: Color = (80, 80, 80)
56
+ DEATH_ZONE_COLOR: Color = (60, 0, 0)
57
+ TRIANGLE_EMPTY_COLOR: Color = GRAY # Color for empty grid cells
58
+ GRID_BG_DEFAULT: Color = DARK_GRAY # Default grid background
59
+ GRID_BG_GAME_OVER: Color = (70, 30, 30) # BG when game is over
60
+
61
+ # UI Colors
62
+ HUD_BG_COLOR: Color = (20, 20, 20)
63
+ HUD_TEXT_COLOR: Color = WHITE
64
+ PREVIEW_BG_COLOR: Color = (25, 25, 25) # Added missing constant
65
+ PREVIEW_BORDER: Color = GRAY
66
+ PREVIEW_SELECTED_BORDER: Color = WHITE
67
+
68
+ # Highlight Colors (RGB only, alpha handled separately in drawing)
69
+ HIGHLIGHT_VALID_COLOR: Color = GREEN
70
+ HIGHLIGHT_INVALID_COLOR: Color = RED
71
+ PLACEMENT_VALID_COLOR: Color = GREEN # Alias
72
+ PLACEMENT_INVALID_COLOR: Color = RED # Alias
73
+
74
+ # Debug Colors
75
+ DEBUG_TOGGLE_COLOR: Color = MAGENTA
76
+
77
+ __all__ = [
78
+ "Color",
79
+ "ColorRGBA",
80
+ "WHITE",
81
+ "BLACK",
82
+ "GRAY",
83
+ "LIGHT_GRAY",
84
+ "DARK_GRAY",
85
+ "RED",
86
+ "GREEN",
87
+ "BLUE",
88
+ "YELLOW",
89
+ "CYAN",
90
+ "MAGENTA",
91
+ "ORANGE",
92
+ "PURPLE",
93
+ "TEAL",
94
+ "OLIVE",
95
+ "SHAPE_COLORS",
96
+ "COLOR_TO_ID_MAP",
97
+ "ID_TO_COLOR_MAP",
98
+ "NO_COLOR_ID",
99
+ "DEBUG_COLOR_ID",
100
+ "GRID_BG_LIGHT",
101
+ "GRID_BG_DARK",
102
+ "GRID_LINE_COLOR",
103
+ "DEATH_ZONE_COLOR",
104
+ "TRIANGLE_EMPTY_COLOR",
105
+ "GRID_BG_DEFAULT",
106
+ "GRID_BG_GAME_OVER",
107
+ "HUD_BG_COLOR",
108
+ "HUD_TEXT_COLOR",
109
+ "PREVIEW_BG_COLOR",
110
+ "PREVIEW_BORDER",
111
+ "PREVIEW_SELECTED_BORDER",
112
+ "HIGHLIGHT_VALID_COLOR",
113
+ "HIGHLIGHT_INVALID_COLOR",
114
+ "PLACEMENT_VALID_COLOR",
115
+ "PLACEMENT_INVALID_COLOR",
116
+ "DEBUG_TOGGLE_COLOR",
117
+ ]
@@ -0,0 +1,73 @@
1
+ import pygame
2
+
3
+ # Use internal imports
4
+ from ...config import EnvConfig
5
+ from ...core.structs import Triangle
6
+ from ...utils import geometry
7
+
8
+
9
+ def _calculate_render_params(
10
+ width: int, height: int, config: EnvConfig
11
+ ) -> tuple[float, float, float, float]:
12
+ """Calculates scale (cw, ch) and offset (ox, oy) for rendering the grid."""
13
+ rows, cols = config.ROWS, config.COLS
14
+ cols_eff = cols * 0.75 + 0.25 if cols > 0 else 1
15
+ scale_w = width / cols_eff if cols_eff > 0 else 1
16
+ scale_h = height / rows if rows > 0 else 1
17
+ scale = max(1.0, min(scale_w, scale_h))
18
+ cell_size = scale
19
+ grid_w_px = cols_eff * cell_size
20
+ grid_h_px = rows * cell_size
21
+ offset_x = (width - grid_w_px) / 2
22
+ offset_y = (height - grid_h_px) / 2
23
+ return cell_size, cell_size, offset_x, offset_y
24
+
25
+
26
+ def get_grid_coords_from_screen(
27
+ screen_pos: tuple[int, int], grid_area_rect: pygame.Rect, config: EnvConfig
28
+ ) -> tuple[int, int] | None:
29
+ """Maps screen coordinates (relative to screen) to grid row/column."""
30
+ if not grid_area_rect or not grid_area_rect.collidepoint(screen_pos):
31
+ return None
32
+
33
+ local_x = screen_pos[0] - grid_area_rect.left
34
+ local_y = screen_pos[1] - grid_area_rect.top
35
+ cw, ch, ox, oy = _calculate_render_params(
36
+ grid_area_rect.width, grid_area_rect.height, config
37
+ )
38
+ if cw <= 0 or ch <= 0:
39
+ return None
40
+
41
+ row = int((local_y - oy) / ch) if ch > 0 else -1
42
+ approx_col_center_index = (local_x - ox - cw / 4) / (cw * 0.75) if cw > 0 else -1
43
+ col = int(round(approx_col_center_index))
44
+
45
+ for r_check in [row, row - 1, row + 1]:
46
+ if not (0 <= r_check < config.ROWS):
47
+ continue
48
+ for c_check in [col, col - 1, col + 1]:
49
+ if not (0 <= c_check < config.COLS):
50
+ continue
51
+ # Use corrected orientation check
52
+ is_up = (r_check + c_check) % 2 != 0
53
+ temp_tri = Triangle(r_check, c_check, is_up)
54
+ pts = temp_tri.get_points(ox, oy, cw, ch)
55
+ # Use geometry from utils
56
+ if geometry.is_point_in_polygon((local_x, local_y), pts):
57
+ return r_check, c_check
58
+
59
+ if 0 <= row < config.ROWS and 0 <= col < config.COLS:
60
+ return row, col
61
+ return None
62
+
63
+
64
+ def get_preview_index_from_screen(
65
+ screen_pos: tuple[int, int], preview_rects: dict[int, pygame.Rect]
66
+ ) -> int | None:
67
+ """Maps screen coordinates to a shape preview index."""
68
+ if not preview_rects:
69
+ return None
70
+ for idx, rect in preview_rects.items():
71
+ if rect and rect.collidepoint(screen_pos):
72
+ return idx
73
+ return None
@@ -0,0 +1,55 @@
1
+ import logging
2
+
3
+ import pygame
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ DEFAULT_FONT_NAME = None
8
+ FALLBACK_FONT_NAME = "arial,freesans"
9
+
10
+
11
+ def load_single_font(name: str | None, size: int) -> pygame.font.Font | None:
12
+ """Loads a single font, handling potential errors."""
13
+ try:
14
+ font = pygame.font.SysFont(name, size)
15
+ return font
16
+ except Exception as e:
17
+ logger.error(f"Error loading font '{name}' size {size}: {e}")
18
+ if name != FALLBACK_FONT_NAME:
19
+ logger.warning(f"Attempting fallback font: {FALLBACK_FONT_NAME}")
20
+ try:
21
+ font = pygame.font.SysFont(FALLBACK_FONT_NAME, size)
22
+ logger.info(f"Loaded fallback font: {FALLBACK_FONT_NAME} size {size}")
23
+ return font
24
+ except Exception as e_fallback:
25
+ logger.error(f"Fallback font failed: {e_fallback}")
26
+ return None
27
+ return None
28
+
29
+
30
+ def load_fonts(
31
+ font_sizes: dict[str, int] | None = None,
32
+ ) -> dict[str, pygame.font.Font | None]:
33
+ """Loads standard game fonts."""
34
+ if font_sizes is None:
35
+ font_sizes = {
36
+ "ui": 24,
37
+ "score": 30,
38
+ "help": 18,
39
+ "title": 48,
40
+ }
41
+
42
+ fonts: dict[str, pygame.font.Font | None] = {}
43
+ required_fonts = ["score", "help"]
44
+
45
+ logger.info("Loading fonts...")
46
+ for name, size in font_sizes.items():
47
+ fonts[name] = load_single_font(DEFAULT_FONT_NAME, size)
48
+
49
+ for name in required_fonts:
50
+ if fonts.get(name) is None:
51
+ logger.critical(
52
+ f"Essential font '{name}' failed to load. Text rendering will be affected."
53
+ )
54
+
55
+ return fonts
@@ -0,0 +1,101 @@
1
+ import logging
2
+
3
+ import pygame
4
+
5
+ # Import DisplayConfig from trianglengin
6
+ from trianglengin.config import DisplayConfig
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def calculate_interactive_layout(
12
+ screen_width: int, screen_height: int, display_config: DisplayConfig
13
+ ) -> dict[str, pygame.Rect]:
14
+ """
15
+ Calculates layout rectangles for interactive modes (play/debug).
16
+ Places grid on the left and preview on the right.
17
+ Uses DisplayConfig for padding and dimensions.
18
+ """
19
+ sw, sh = screen_width, screen_height
20
+ pad = display_config.PADDING
21
+ hud_h = display_config.HUD_HEIGHT
22
+ preview_w = display_config.PREVIEW_AREA_WIDTH
23
+
24
+ available_h = max(0, sh - hud_h - 2 * pad)
25
+ available_w = max(0, sw - 3 * pad)
26
+
27
+ grid_w = max(0, available_w - preview_w)
28
+ grid_h = available_h
29
+
30
+ grid_rect = pygame.Rect(pad, pad, grid_w, grid_h)
31
+ preview_rect = pygame.Rect(grid_rect.right + pad, pad, preview_w, grid_h)
32
+
33
+ screen_rect = pygame.Rect(0, 0, sw, sh)
34
+ grid_rect = grid_rect.clip(screen_rect)
35
+ preview_rect = preview_rect.clip(screen_rect)
36
+
37
+ logger.debug(
38
+ f"Interactive Layout calculated: Grid={grid_rect}, Preview={preview_rect}"
39
+ )
40
+
41
+ return {
42
+ "grid": grid_rect,
43
+ "preview": preview_rect,
44
+ }
45
+
46
+
47
+ def calculate_training_layout(
48
+ screen_width: int,
49
+ screen_height: int,
50
+ display_config: DisplayConfig,
51
+ progress_bars_total_height: int, # Height needed for progress bars
52
+ ) -> dict[str, pygame.Rect]:
53
+ """
54
+ Calculates layout rectangles for a potential training visualization mode.
55
+ MINIMAL SPACING. Worker grid top, progress bars bottom (above HUD), plots fill middle.
56
+ Uses DisplayConfig for padding and dimensions.
57
+ """
58
+ sw, sh = screen_width, screen_height
59
+ pad = 2 # Minimal padding
60
+ hud_h = display_config.HUD_HEIGHT
61
+
62
+ # --- Worker Grid Area (Top) ---
63
+ total_available_h_for_grid_plots_bars = max(0, sh - hud_h - 2 * pad)
64
+ top_area_h = min(
65
+ int(total_available_h_for_grid_plots_bars * 0.10), 80
66
+ ) # 10% or 80px max
67
+ top_area_w = sw - 2 * pad
68
+ worker_grid_rect = pygame.Rect(pad, pad, top_area_w, top_area_h)
69
+
70
+ # --- Progress Bar Area (Bottom, above HUD) ---
71
+ pb_area_y = sh - hud_h - pad - progress_bars_total_height
72
+ pb_area_w = sw - 2 * pad
73
+ progress_bar_area_rect = pygame.Rect(
74
+ pad, pb_area_y, pb_area_w, progress_bars_total_height
75
+ )
76
+
77
+ # --- Plot Area (Middle) ---
78
+ plot_area_y = worker_grid_rect.bottom + pad
79
+ plot_area_w = sw - 2 * pad
80
+ plot_area_h = max(0, progress_bar_area_rect.top - plot_area_y - pad) # Fill space
81
+ plot_rect = pygame.Rect(pad, plot_area_y, plot_area_w, plot_area_h)
82
+
83
+ # Clip all rects to screen bounds
84
+ screen_rect = pygame.Rect(0, 0, sw, sh)
85
+ worker_grid_rect = worker_grid_rect.clip(screen_rect)
86
+ plot_rect = plot_rect.clip(screen_rect)
87
+ progress_bar_area_rect = progress_bar_area_rect.clip(screen_rect)
88
+
89
+ logger.debug(
90
+ f"Training Layout calculated: WorkerGrid={worker_grid_rect}, PlotRect={plot_rect}, ProgressBarArea={progress_bar_area_rect}"
91
+ )
92
+
93
+ return {
94
+ "worker_grid": worker_grid_rect,
95
+ "plots": plot_rect,
96
+ "progress_bar_area": progress_bar_area_rect,
97
+ }
98
+
99
+
100
+ # Default export can remain if desired, or be removed if only specific layouts are used.
101
+ calculate_layout = calculate_interactive_layout
@@ -0,0 +1,232 @@
1
+ # File: trianglengin/visualization/core/visualizer.py
2
+ import logging
3
+
4
+ import pygame
5
+
6
+ # Use internal imports
7
+ from trianglengin.config import DisplayConfig, EnvConfig
8
+ from trianglengin.core.environment import GameState
9
+ from trianglengin.core.structs import Shape
10
+
11
+ # Import coord_mapper module itself
12
+ from trianglengin.visualization.core import colors, coord_mapper, layout
13
+ from trianglengin.visualization.drawing import grid as grid_drawing
14
+ from trianglengin.visualization.drawing import highlight as highlight_drawing
15
+ from trianglengin.visualization.drawing import hud as hud_drawing
16
+ from trianglengin.visualization.drawing import previews as preview_drawing
17
+ from trianglengin.visualization.drawing.previews import (
18
+ draw_floating_preview,
19
+ draw_placement_preview,
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class Visualizer:
26
+ """
27
+ Orchestrates rendering of a single game state for interactive modes.
28
+ Receives interaction state (hover, selection) via render parameters.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ screen: pygame.Surface,
34
+ display_config: DisplayConfig,
35
+ env_config: EnvConfig,
36
+ fonts: dict[str, pygame.font.Font | None],
37
+ ):
38
+ self.screen = screen
39
+ self.display_config = display_config
40
+ self.env_config = env_config
41
+ self.fonts = fonts
42
+ self.layout_rects: dict[str, pygame.Rect] | None = None
43
+ self.preview_rects: dict[int, pygame.Rect] = {}
44
+ self._layout_calculated_for_size: tuple[int, int] = (0, 0)
45
+ self.ensure_layout()
46
+
47
+ def ensure_layout(self) -> dict[str, pygame.Rect]:
48
+ """Returns cached layout or calculates it if needed."""
49
+ current_w, current_h = self.screen.get_size()
50
+ current_size = (current_w, current_h)
51
+
52
+ if (
53
+ self.layout_rects is None
54
+ or self._layout_calculated_for_size != current_size
55
+ ):
56
+ # TODO: Align VisConfig/DisplayConfig usage in layout functions
57
+ # Assuming layout function can work with DisplayConfig directly or adapt
58
+ self.layout_rects = layout.calculate_interactive_layout(
59
+ current_w,
60
+ current_h,
61
+ self.display_config, # type: ignore
62
+ )
63
+ self._layout_calculated_for_size = current_size
64
+ logger.info(
65
+ f"Recalculated interactive layout for size {current_size}: {self.layout_rects}"
66
+ )
67
+ self.preview_rects = {}
68
+
69
+ return self.layout_rects if self.layout_rects is not None else {}
70
+
71
+ def render(
72
+ self,
73
+ game_state: GameState,
74
+ mode: str,
75
+ selected_shape_idx: int = -1,
76
+ hover_shape: Shape | None = None,
77
+ hover_grid_coord: tuple[int, int] | None = None,
78
+ hover_is_valid: bool = False,
79
+ hover_screen_pos: tuple[int, int] | None = None,
80
+ debug_highlight_coord: tuple[int, int] | None = None,
81
+ ):
82
+ """Renders the entire game visualization for interactive modes."""
83
+ self.screen.fill(colors.GRID_BG_DEFAULT)
84
+ layout_rects = self.ensure_layout()
85
+ grid_rect = layout_rects.get("grid")
86
+ preview_rect = layout_rects.get("preview")
87
+
88
+ if grid_rect and grid_rect.width > 0 and grid_rect.height > 0:
89
+ try:
90
+ grid_surf = self.screen.subsurface(grid_rect)
91
+ # Calculate render params for the grid area
92
+ cw, ch, ox, oy = coord_mapper._calculate_render_params(
93
+ grid_rect.width, grid_rect.height, self.env_config
94
+ )
95
+ self._render_grid_area(
96
+ grid_surf,
97
+ game_state,
98
+ mode,
99
+ grid_rect,
100
+ hover_shape,
101
+ hover_grid_coord,
102
+ hover_is_valid,
103
+ hover_screen_pos,
104
+ debug_highlight_coord,
105
+ cw,
106
+ ch,
107
+ ox,
108
+ oy, # Pass calculated params
109
+ )
110
+ except ValueError as e:
111
+ logger.error(f"Error creating grid subsurface ({grid_rect}): {e}")
112
+ pygame.draw.rect(self.screen, colors.RED, grid_rect, 1)
113
+
114
+ if preview_rect and preview_rect.width > 0 and preview_rect.height > 0:
115
+ try:
116
+ preview_surf = self.screen.subsurface(preview_rect)
117
+ self._render_preview_area(
118
+ preview_surf, game_state, mode, preview_rect, selected_shape_idx
119
+ )
120
+ except ValueError as e:
121
+ logger.error(f"Error creating preview subsurface ({preview_rect}): {e}")
122
+ pygame.draw.rect(self.screen, colors.RED, preview_rect, 1)
123
+
124
+ hud_drawing.render_hud(
125
+ surface=self.screen,
126
+ mode=mode,
127
+ fonts=self.fonts,
128
+ )
129
+
130
+ def _render_grid_area(
131
+ self,
132
+ grid_surf: pygame.Surface,
133
+ game_state: GameState,
134
+ mode: str,
135
+ grid_rect: pygame.Rect,
136
+ hover_shape: Shape | None,
137
+ hover_grid_coord: tuple[int, int] | None,
138
+ hover_is_valid: bool,
139
+ hover_screen_pos: tuple[int, int] | None,
140
+ debug_highlight_coord: tuple[int, int] | None,
141
+ cw: float,
142
+ ch: float,
143
+ ox: float,
144
+ oy: float, # Receive calculated params
145
+ ):
146
+ """Renders the main game grid and overlays onto the provided grid_surf."""
147
+ grid_drawing.draw_grid_background(
148
+ grid_surf,
149
+ self.env_config,
150
+ self.display_config,
151
+ cw,
152
+ ch,
153
+ ox,
154
+ oy, # Pass params
155
+ game_state.is_over(),
156
+ mode == "debug",
157
+ )
158
+
159
+ grid_drawing.draw_grid_state(
160
+ grid_surf,
161
+ game_state.grid_data,
162
+ cw,
163
+ ch,
164
+ ox,
165
+ oy, # Pass params
166
+ )
167
+
168
+ if mode == "play" and hover_shape:
169
+ if hover_grid_coord:
170
+ draw_placement_preview(
171
+ grid_surf,
172
+ hover_shape,
173
+ hover_grid_coord[0],
174
+ hover_grid_coord[1],
175
+ is_valid=hover_is_valid,
176
+ cw=cw,
177
+ ch=ch,
178
+ ox=ox,
179
+ oy=oy, # Pass params
180
+ )
181
+ elif hover_screen_pos:
182
+ local_hover_pos = (
183
+ hover_screen_pos[0] - grid_rect.left,
184
+ hover_screen_pos[1] - grid_rect.top,
185
+ )
186
+ if grid_surf.get_rect().collidepoint(local_hover_pos):
187
+ draw_floating_preview(
188
+ grid_surf,
189
+ hover_shape,
190
+ local_hover_pos,
191
+ # config and mapper removed
192
+ )
193
+
194
+ if mode == "debug" and debug_highlight_coord:
195
+ r, c = debug_highlight_coord
196
+ highlight_drawing.draw_debug_highlight(
197
+ grid_surf,
198
+ r,
199
+ c,
200
+ cw=cw,
201
+ ch=ch,
202
+ ox=ox,
203
+ oy=oy, # Pass params
204
+ )
205
+
206
+ score_font = self.fonts.get("score")
207
+ if score_font:
208
+ score_text = f"Score: {game_state.game_score():.0f}"
209
+ score_surf = score_font.render(score_text, True, colors.YELLOW)
210
+ score_rect = score_surf.get_rect(topleft=(5, 5))
211
+ grid_surf.blit(score_surf, score_rect)
212
+
213
+ def _render_preview_area(
214
+ self,
215
+ preview_surf: pygame.Surface,
216
+ game_state: GameState,
217
+ mode: str,
218
+ preview_rect: pygame.Rect,
219
+ selected_shape_idx: int,
220
+ ):
221
+ """Renders the shape preview slots onto preview_surf and caches rects."""
222
+ current_preview_rects = preview_drawing.render_previews(
223
+ preview_surf,
224
+ game_state,
225
+ preview_rect.topleft,
226
+ mode,
227
+ self.env_config,
228
+ self.display_config,
229
+ selected_shape_idx=selected_shape_idx,
230
+ )
231
+ if not self.preview_rects or self.preview_rects != current_preview_rects:
232
+ self.preview_rects = current_preview_rects
@@ -0,0 +1,45 @@
1
+
2
+
3
+ # Visualization Drawing Submodule (`trianglengin.visualization.drawing`)
4
+
5
+ ## Purpose and Architecture
6
+
7
+ This submodule contains specialized functions responsible for drawing specific visual elements of the game onto Pygame surfaces for the **interactive modes** within the `trianglengin` library. These functions are typically called by the core renderer (`Visualizer`) in [`trianglengin.visualization.core`](../core/README.md).
8
+
9
+ - **[`grid.py`](grid.py):** Functions for drawing the grid background (`draw_grid_background`), the individual triangles within it colored based on occupancy/emptiness (`draw_grid_state`), and optional debug overlays (`draw_debug_grid_overlay`).
10
+ - **[`shapes.py`](shapes.py):** Contains `draw_shape`, a function to render a given `Shape` object at a specific location on a surface (used primarily for previews).
11
+ - **[`previews.py`](previews.py):** Handles rendering related to shape previews:
12
+ - `render_previews`: Draws the dedicated preview area, including borders and the shapes within their slots, handling selection highlights.
13
+ - `draw_placement_preview`: Draws a semi-transparent version of a shape snapped to the grid, indicating a potential placement location (used in play mode hover).
14
+ - `draw_floating_preview`: Draws a semi-transparent shape directly under the mouse cursor when hovering over the grid but not snapped (used in play mode hover).
15
+ - **[`hud.py`](hud.py):** `render_hud` draws Heads-Up Display elements like help text onto the main screen surface (simplified for interactive modes).
16
+ - **[`highlight.py`](highlight.py):** `draw_debug_highlight` draws a distinct border around a specific triangle, used for visual feedback in debug mode.
17
+
18
+ ## Exposed Interfaces
19
+
20
+ - **Grid Drawing:**
21
+ - `draw_grid_background(...)`
22
+ - `draw_grid_state(...)`
23
+ - `draw_debug_grid_overlay(...)`
24
+ - **Shape Drawing:**
25
+ - `draw_shape(...)`
26
+ - **Preview Drawing:**
27
+ - `render_previews(...) -> Dict[int, pygame.Rect]`
28
+ - `draw_placement_preview(...)`
29
+ - `draw_floating_preview(...)`
30
+ - **HUD Drawing:**
31
+ - `render_hud(...)`
32
+ - **Highlight Drawing:**
33
+ - `draw_debug_highlight(...)`
34
+
35
+ ## Dependencies
36
+
37
+ - **[`trianglengin.visualization.core`](../core/README.md)**: `colors`, `coord_mapper`.
38
+ - **`trianglengin.core`**: `EnvConfig`, `GameState`, `GridData`, `Shape`, `Triangle`.
39
+ - **`trianglengin.config`**: `DisplayConfig`.
40
+ - **`pygame`**: The core library used for all drawing operations.
41
+ - **Standard Libraries:** `typing`, `logging`, `math`.
42
+
43
+ ---
44
+
45
+ **Note:** Please keep this README updated when adding new drawing functions, modifying existing ones, or changing their dependencies.