trianglengin 1.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tests/__init__.py +0 -0
- tests/conftest.py +108 -0
- tests/core/__init__.py +2 -0
- tests/core/environment/README.md +47 -0
- tests/core/environment/__init__.py +2 -0
- tests/core/environment/test_action_codec.py +50 -0
- tests/core/environment/test_game_state.py +483 -0
- tests/core/environment/test_grid_data.py +205 -0
- tests/core/environment/test_grid_logic.py +362 -0
- tests/core/environment/test_shape_logic.py +171 -0
- tests/core/environment/test_step.py +372 -0
- tests/core/structs/__init__.py +0 -0
- tests/core/structs/test_shape.py +83 -0
- tests/core/structs/test_triangle.py +97 -0
- tests/utils/__init__.py +0 -0
- tests/utils/test_geometry.py +93 -0
- trianglengin/__init__.py +18 -0
- trianglengin/app.py +110 -0
- trianglengin/cli.py +134 -0
- trianglengin/config/__init__.py +9 -0
- trianglengin/config/display_config.py +47 -0
- trianglengin/config/env_config.py +103 -0
- trianglengin/core/__init__.py +8 -0
- trianglengin/core/environment/__init__.py +31 -0
- trianglengin/core/environment/action_codec.py +37 -0
- trianglengin/core/environment/game_state.py +217 -0
- trianglengin/core/environment/grid/README.md +46 -0
- trianglengin/core/environment/grid/__init__.py +18 -0
- trianglengin/core/environment/grid/grid_data.py +140 -0
- trianglengin/core/environment/grid/line_cache.py +189 -0
- trianglengin/core/environment/grid/logic.py +131 -0
- trianglengin/core/environment/logic/__init__.py +3 -0
- trianglengin/core/environment/logic/actions.py +38 -0
- trianglengin/core/environment/logic/step.py +134 -0
- trianglengin/core/environment/shapes/__init__.py +19 -0
- trianglengin/core/environment/shapes/logic.py +84 -0
- trianglengin/core/environment/shapes/templates.py +587 -0
- trianglengin/core/structs/__init__.py +27 -0
- trianglengin/core/structs/constants.py +28 -0
- trianglengin/core/structs/shape.py +61 -0
- trianglengin/core/structs/triangle.py +48 -0
- trianglengin/interaction/README.md +45 -0
- trianglengin/interaction/__init__.py +17 -0
- trianglengin/interaction/debug_mode_handler.py +96 -0
- trianglengin/interaction/event_processor.py +43 -0
- trianglengin/interaction/input_handler.py +82 -0
- trianglengin/interaction/play_mode_handler.py +141 -0
- trianglengin/utils/__init__.py +9 -0
- trianglengin/utils/geometry.py +73 -0
- trianglengin/utils/types.py +10 -0
- trianglengin/visualization/README.md +44 -0
- trianglengin/visualization/__init__.py +61 -0
- trianglengin/visualization/core/README.md +52 -0
- trianglengin/visualization/core/__init__.py +12 -0
- trianglengin/visualization/core/colors.py +117 -0
- trianglengin/visualization/core/coord_mapper.py +73 -0
- trianglengin/visualization/core/fonts.py +55 -0
- trianglengin/visualization/core/layout.py +101 -0
- trianglengin/visualization/core/visualizer.py +232 -0
- trianglengin/visualization/drawing/README.md +45 -0
- trianglengin/visualization/drawing/__init__.py +30 -0
- trianglengin/visualization/drawing/grid.py +156 -0
- trianglengin/visualization/drawing/highlight.py +30 -0
- trianglengin/visualization/drawing/hud.py +39 -0
- trianglengin/visualization/drawing/previews.py +172 -0
- trianglengin/visualization/drawing/shapes.py +36 -0
- trianglengin-1.0.6.dist-info/METADATA +367 -0
- trianglengin-1.0.6.dist-info/RECORD +72 -0
- trianglengin-1.0.6.dist-info/WHEEL +5 -0
- trianglengin-1.0.6.dist-info/entry_points.txt +2 -0
- trianglengin-1.0.6.dist-info/licenses/LICENSE +22 -0
- trianglengin-1.0.6.dist-info/top_level.txt +2 -0
@@ -0,0 +1,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.
|