12 changed files with 401 additions and 14 deletions
@ -0,0 +1,40 @@
|
||||
# Project Guidelines |
||||
|
||||
## UI Preview Tool |
||||
|
||||
When editing the start menu, pause menu, or level intro UI, generate a real preview image before judging layout changes. |
||||
|
||||
Use [tools/render_menu_preview.py](tools/render_menu_preview.py) instead of relying on mental layout or ad-hoc screenshots. The tool renders the actual SDL scene and saves a PNG from the real renderer. |
||||
|
||||
Typical command: |
||||
|
||||
```bash |
||||
/home/enne2/dev/mice/.venv/bin/python tools/render_menu_preview.py \ |
||||
--output /tmp/mice_start_preview.png \ |
||||
--screen start \ |
||||
--difficulty normal \ |
||||
--resolution 1280x720 |
||||
``` |
||||
|
||||
Supported screens: |
||||
|
||||
- `start` |
||||
- `pause` |
||||
- `level_intro` |
||||
|
||||
Useful flags: |
||||
|
||||
- `--difficulty easy|normal|hard` |
||||
- `--resolution WIDTHxHEIGHT` |
||||
- `--output /path/to/file.png` |
||||
- `--seed N` for deterministic previews |
||||
- `--animation-ms N` to choose the GIF frame timestamp for the start menu |
||||
|
||||
Workflow when touching menu layout: |
||||
|
||||
1. Edit the menu code. |
||||
2. Run the preview tool for the relevant screen. |
||||
3. Inspect the generated PNG. |
||||
4. Iterate until spacing and readability are correct. |
||||
|
||||
The preview tool is intended for fast visual feedback and should be preferred before launching a full interactive game session for menu-only changes. |
||||
Binary file not shown.
Binary file not shown.
@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3 |
||||
|
||||
import argparse |
||||
import os |
||||
import random |
||||
import sys |
||||
import time |
||||
from pathlib import Path |
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1] |
||||
if str(PROJECT_ROOT) not in sys.path: |
||||
sys.path.insert(0, str(PROJECT_ROOT)) |
||||
|
||||
|
||||
def parse_args(): |
||||
parser = argparse.ArgumentParser(description="Render a real Mice menu screen to a PNG preview") |
||||
parser.add_argument("--output", default="/tmp/mice_menu_preview.png", help="Output PNG path") |
||||
parser.add_argument("--screen", choices=("start", "pause", "level_intro"), default="start") |
||||
parser.add_argument("--difficulty", choices=("easy", "normal", "hard", "medium"), default="hard") |
||||
parser.add_argument("--level", type=int, default=0, help="0-based level index") |
||||
parser.add_argument("--resolution", default="1280x720", help="Preview resolution, for example 1280x720") |
||||
parser.add_argument("--seed", type=int, default=1337, help="Random seed for deterministic previews") |
||||
parser.add_argument("--animation-ms", type=int, default=1200, help="Start-menu GIF timestamp in milliseconds") |
||||
return parser.parse_args() |
||||
|
||||
|
||||
def configure_environment(args): |
||||
os.environ.setdefault("MICE_PROJECT_ROOT", str(PROJECT_ROOT)) |
||||
os.environ.setdefault("SDL_VIDEODRIVER", "dummy") |
||||
os.environ.setdefault("SDL_AUDIODRIVER", "dummy") |
||||
os.environ.setdefault("SDL_RENDER_DRIVER", "software") |
||||
os.environ.setdefault("MICE_DISABLE_JOYSTICK", "1") |
||||
os.environ["RESOLUTION"] = args.resolution |
||||
|
||||
|
||||
def build_game(args): |
||||
from engine import maze, sdl2 as engine |
||||
from rats import MiceMaze |
||||
|
||||
original_show_intro = engine.GameWindow.show_intro |
||||
original_load_joystick = engine.GameWindow.load_joystick |
||||
original_start_game = MiceMaze.start_game |
||||
|
||||
def preview_start_game(self): |
||||
self.start_menu_animation_started_at = time.monotonic() |
||||
self.current_level_music = self._resolve_level_music(self.current_level) |
||||
self._valid_positions = None |
||||
self.run_recorded = False |
||||
self.ammo = { |
||||
"bomb": {"count": 2, "max": 8}, |
||||
"nuclear": {"count": 1, "max": 1}, |
||||
"mine": {"count": 2, "max": 4}, |
||||
"gas": {"count": 2, "max": 4}, |
||||
} |
||||
self.blood_stains = {} |
||||
self.background_texture = None |
||||
self.blood_layer_sprites.clear() |
||||
self.cave_foreground_tiles.clear() |
||||
self.game_end = (False, None) |
||||
self.game_status = "start_menu" |
||||
self.menu_screen = "start" |
||||
self.units.clear() |
||||
self.unit_positions.clear() |
||||
self.unit_positions_before.clear() |
||||
self.points = 0 |
||||
self.pointer = (self.map.width // 2, self.map.height // 2) |
||||
self.scroll_cursor() |
||||
|
||||
engine.GameWindow.show_intro = lambda self, *a, **k: None |
||||
engine.GameWindow.load_joystick = lambda self: None |
||||
MiceMaze.start_game = preview_start_game |
||||
try: |
||||
game = MiceMaze(maze.get_default_map_source(), level_index=args.level) |
||||
finally: |
||||
engine.GameWindow.show_intro = original_show_intro |
||||
engine.GameWindow.load_joystick = original_load_joystick |
||||
MiceMaze.start_game = original_start_game |
||||
|
||||
game.render_engine.audio = False |
||||
game._apply_difficulty(args.difficulty, persist=False) |
||||
game.start_menu_animation_started_at = time.monotonic() - args.animation_ms / 1000 |
||||
return game |
||||
|
||||
|
||||
def render_screen(game, screen_name): |
||||
renderer = game.render_engine.renderer |
||||
renderer.clear() |
||||
game.draw_maze() |
||||
|
||||
if screen_name == "start": |
||||
game.game_status = "start_menu" |
||||
game.menu_screen = "start" |
||||
game.render_start_menu() |
||||
elif screen_name == "pause": |
||||
game.game_status = "paused" |
||||
game.menu_screen = None |
||||
game.render_pause_menu() |
||||
else: |
||||
game.game_status = "start_menu" |
||||
game.menu_screen = "level_intro" |
||||
game.render_engine.dialog( |
||||
f"Level {game.current_level + 1}", |
||||
subtitle=f"Points: {game.points}\nPress Return to begin", |
||||
image=game.assets["BMP_WEWIN"], |
||||
) |
||||
|
||||
renderer.present() |
||||
|
||||
|
||||
def main(): |
||||
args = parse_args() |
||||
configure_environment(args) |
||||
random.seed(args.seed) |
||||
|
||||
game = build_game(args) |
||||
output_path = Path(args.output).expanduser() |
||||
|
||||
try: |
||||
render_screen(game, args.screen) |
||||
saved_path = game.render_engine.save_frame(output_path) |
||||
print(saved_path) |
||||
finally: |
||||
game.render_engine.close() |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
sys.exit(main()) |
||||
Loading…
Reference in new issue