diff --git a/explanation/headless-automation.md b/explanation/headless-automation.md deleted file mode 100644 index 3f7fcd5..0000000 --- a/explanation/headless-automation.md +++ /dev/null @@ -1,240 +0,0 @@ -# Headless Mode and Automation - -McRogueFace supports headless operation for automated testing, CI/CD pipelines, and programmatic control without a display. - -## Running Headless - -Launch with the `--headless` flag: - -```bash -mcrogueface --headless --exec game.py -``` - -Or use xvfb for a virtual framebuffer (required for rendering): - -```bash -xvfb-run -a -s "-screen 0 1024x768x24" mcrogueface --headless --exec game.py -``` - -## Time Control with mcrfpy.step() - -In headless mode, you control time explicitly rather than waiting for real-time to pass. The `step()` function takes **seconds** as a float: - -```python -import mcrfpy - -# Advance simulation by 100ms -mcrfpy.step(0.1) - -# Advance by 1 second -mcrfpy.step(1.0) - -# Advance by 16ms (~60fps frame) -mcrfpy.step(0.016) -``` - -### Why This Matters - -Traditional timer-based code waits for real time and the game loop: - -```python -# OLD PATTERN - waits for game loop, subject to timeouts -def delayed_action(timer, runtime): - print("Action!") - sys.exit(0) - -mcrfpy.Timer("delay", delayed_action, 500, once=True) -# Script ends, game loop runs, timer eventually fires -``` - -With `mcrfpy.step()`, you control the clock synchronously: - -```python -# NEW PATTERN - instant, deterministic -mcrfpy.Timer("delay", delayed_action, 500, once=True) -mcrfpy.step(0.6) # Advance 600ms - timer fires during this call -``` - -### Timer Behavior - -- Timers fire **once per `step()` call** if their interval has elapsed -- To fire a timer multiple times, call `step()` multiple times: - -```python -count = 0 -def tick(timer, runtime): - global count - count += 1 - -timer = mcrfpy.Timer("tick", tick, 100) # Fire every 100ms - -# Each step() processes timers once -for i in range(5): - mcrfpy.step(0.1) # 100ms each - -print(count) # 5 -``` - -## Screenshots - -The `automation.screenshot()` function captures the current frame: - -```python -from mcrfpy import automation - -# Capture screenshot (synchronous in headless mode) -result = automation.screenshot("output.png") -print(f"Screenshot saved: {result}") -``` - -**Key insight:** In headless mode, `screenshot()` is synchronous - no timer dance needed. - -## Testing Patterns - -### Synchronous Test Structure - -```python -#!/usr/bin/env python3 -import mcrfpy -import sys - -# Setup scene -scene = mcrfpy.Scene("test") -scene.activate() -mcrfpy.step(0.1) # Initialize scene - -# Create test objects -frame = mcrfpy.Frame(pos=(100, 100), size=(50, 50)) -scene.children.append(frame) - -# Verify state -if frame.x != 100: - print("FAIL: frame.x should be 100") - sys.exit(1) - -print("PASS") -sys.exit(0) -``` - -### Testing Animations - -```python -# Create frame and start animation -frame = mcrfpy.Frame(pos=(100, 100), size=(50, 50)) -scene.children.append(frame) - -anim = mcrfpy.Animation("x", 200.0, 1.0, "linear") # 1 second duration -anim.start(frame) - -# Advance to midpoint (0.5 seconds) -mcrfpy.step(0.5) -# frame.x should be ~150 (halfway between 100 and 200) - -# Advance to completion -mcrfpy.step(0.6) # Past the 1.0s duration -# frame.x should be 200 - -if frame.x == 200.0: - print("PASS: Animation completed") - sys.exit(0) -``` - -### Testing Timers - -```python -callback_count = 0 - -def on_timer(timer, runtime): - """Timer callbacks receive (timer_object, runtime_ms)""" - global callback_count - callback_count += 1 - print(f"Timer fired! Count: {callback_count}, runtime: {runtime}ms") - -# Create timer that fires every 100ms -timer = mcrfpy.Timer("test", on_timer, 100) - -# Advance time - each step() can fire the timer once -mcrfpy.step(0.15) # First fire at ~100ms -mcrfpy.step(0.15) # Second fire at ~200ms - -if callback_count >= 2: - print("PASS") - sys.exit(0) -``` - -### Testing with once=True Timers - -```python -fired = False - -def one_shot(timer, runtime): - global fired - fired = True - print(f"One-shot timer fired! once={timer.once}") - -# Create one-shot timer -timer = mcrfpy.Timer("oneshot", one_shot, 100, once=True) - -mcrfpy.step(0.15) # Should fire -mcrfpy.step(0.15) # Should NOT fire again - -if fired: - print("PASS: One-shot timer worked") -``` - -## Pattern Comparison - -| Aspect | Timer-based (old) | step()-based (new) | -|--------|-------------------|-------------------| -| Execution | Async (game loop) | Sync (immediate) | -| Timeout risk | High | None | -| Determinism | Variable | Consistent | -| Script flow | Callbacks | Linear | - -## LLM Agent Integration - -Headless mode enables AI agents to interact with McRogueFace programmatically: - -1. **Observe**: Capture screenshots, read game state -2. **Decide**: Process with vision models or state analysis -3. **Act**: Send input commands, modify game state -4. **Verify**: Check results, capture new state - -```python -from mcrfpy import automation - -# Agent loop -while not game_over: - automation.screenshot("state.png") - action = agent.decide("state.png") - execute_action(action) - mcrfpy.step(0.1) # Let action take effect -``` - -## Best Practices - -1. **Use `mcrfpy.step()`** instead of real-time waiting for all headless tests -2. **Initialize scenes** with a brief `step(0.1)` after `activate()` -3. **Be deterministic** - same inputs should produce same outputs -4. **Test incrementally** - advance time in small steps to catch intermediate states -5. **Use `sys.exit(0/1)`** for clear pass/fail signals to test runners -6. **Multiple `step()` calls** to fire repeating timers multiple times - -## Running the Test Suite - -```bash -# Quick run with pytest wrapper -pytest tests/ -q --mcrf-timeout=5 - -# Using the original runner with xvfb -xvfb-run -a python3 tests/run_tests.py -q - -# Run specific test directly -xvfb-run -a mcrogueface --headless --exec tests/unit/test_animation.py -``` - -## Related Topics - -- [Animation System](animation.md) - How animations work -- [Scene API](scene-api.md) - Managing scenes -- [Timer API](timer-api.md) - Timer details diff --git a/tests/KNOWN_ISSUES.md b/tests/KNOWN_ISSUES.md deleted file mode 100644 index 9fab74a..0000000 --- a/tests/KNOWN_ISSUES.md +++ /dev/null @@ -1,134 +0,0 @@ -# McRogueFace Test Suite - Known Issues - -## Test Results Summary - -As of 2026-01-14, with `--mcrf-timeout=5`: -- **120 passed** (67%) -- **59 failed** (33%) - - 40 timeout failures (tests requiring timers/animations) - - 19 actual failures (API changes, missing features, or bugs) - -## Synchronous Testing with `mcrfpy.step()` - -**RECOMMENDED:** Use `mcrfpy.step(t)` to advance simulation time synchronously instead of relying on Timer callbacks and the game loop. This eliminates timeout issues and makes tests deterministic. - -### Old Pattern (Timer-based, async) - -```python -# OLD: Requires game loop, subject to timeouts -def run_tests(timer, runtime): - # tests here - sys.exit(0) - -mcrfpy.Timer("run", run_tests, 100, once=True) -# Script ends, game loop runs, timer eventually fires -``` - -### New Pattern (step-based, sync) - -```python -# NEW: Synchronous, no timeouts -import mcrfpy -import sys - -# Setup scene -scene = mcrfpy.Scene("test") -scene.activate() -mcrfpy.step(0.1) # Initialize scene - -# Run tests directly -frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100)) -scene.children.append(frame) - -# Start animation -anim = mcrfpy.Animation("x", 500.0, 1.0, "linear") -anim.start(frame) - -# Advance simulation to complete animation -mcrfpy.step(1.5) # Advances 1.5 seconds synchronously - -# Verify results -if frame.x == 500.0: - print("PASS") - sys.exit(0) -else: - print("FAIL") - sys.exit(1) -``` - -### Key Differences - -| Aspect | Timer-based | step()-based | -|--------|-------------|--------------| -| Execution | Async (game loop) | Sync (immediate) | -| Timeout risk | High | None | -| Determinism | Variable | Consistent | -| Timer firing | Once per step() call | Per elapsed interval | - -### Timer Behavior with `step()` - -- Timers fire once per `step()` call if their interval has elapsed -- To fire a timer multiple times, call `step()` multiple times: - -```python -# Timer fires every 100ms -timer = mcrfpy.Timer("tick", callback, 100) - -# This fires the timer ~6 times -for i in range(6): - mcrfpy.step(0.1) # Each step processes timers once -``` - -## Refactored Tests - -The following tests have been converted to use `mcrfpy.step()`: -- simple_timer_screenshot_test.py -- test_animation_callback_simple.py -- test_animation_property_locking.py -- test_animation_raii.py -- test_animation_removal.py -- test_timer_callback.py -- test_timer_once.py -- test_simple_callback.py -- test_empty_animation_manager.py -- test_frame_clipping.py -- test_frame_clipping_advanced.py -- test_grid_children.py -- test_color_helpers.py -- test_no_arg_constructors.py -- test_properties_quick.py -- test_simple_drawable.py -- test_python_object_cache.py -- WORKING_automation_test_example.py - -## Remaining Timeout Failures - -These tests still use Timer-based async patterns: -- benchmark_logging_test.py -- keypress_scene_validation_test.py - -**Headless mode tests:** -- test_headless_detection.py -- test_headless_modes.py - -## Running Tests - -```bash -# Quick run (5s timeout, many timeouts expected) -pytest tests/ -q --mcrf-timeout=5 - -# Full run (30s timeout, should pass most timing tests) -pytest tests/ -q --mcrf-timeout=30 - -# Filter by pattern -pytest tests/ -k "bsp" -q - -# Run original runner -python3 tests/run_tests.py -q -``` - -## Recommendations - -1. **For CI:** Use `--mcrf-timeout=10` and accept ~30% timeout failures -2. **For local dev:** Use `--mcrf-timeout=30` for comprehensive testing -3. **For quick validation:** Use `-k "not animation and not timer"` to skip slow tests diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 981e78f..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -Pytest configuration for McRogueFace tests. - -Provides fixtures for running McRogueFace scripts in headless mode. - -Usage: - pytest tests/ -q # Run all tests quietly - pytest tests/ -k "bsp" # Run tests matching "bsp" - pytest tests/ -x # Stop on first failure - pytest tests/ --tb=short # Short tracebacks -""" -import os -import subprocess -import pytest -from pathlib import Path - -# Paths -TESTS_DIR = Path(__file__).parent -BUILD_DIR = TESTS_DIR.parent / "build" -LIB_DIR = TESTS_DIR.parent / "__lib" -MCROGUEFACE = BUILD_DIR / "mcrogueface" - -# Default timeout for tests (can be overridden with --timeout) -DEFAULT_TIMEOUT = 10 - - -def pytest_addoption(parser): - """Add custom command line options.""" - parser.addoption( - "--mcrf-timeout", - action="store", - default=DEFAULT_TIMEOUT, - type=int, - help="Timeout in seconds for each McRogueFace test" - ) - - -@pytest.fixture -def mcrf_timeout(request): - """Get the configured timeout.""" - return request.config.getoption("--mcrf-timeout") - - -@pytest.fixture -def mcrf_env(): - """Environment with LD_LIBRARY_PATH set for McRogueFace.""" - env = os.environ.copy() - existing_ld = env.get('LD_LIBRARY_PATH', '') - env['LD_LIBRARY_PATH'] = f"{LIB_DIR}:{existing_ld}" if existing_ld else str(LIB_DIR) - return env - - -@pytest.fixture -def mcrf_exec(mcrf_env, mcrf_timeout): - """ - Fixture that returns a function to execute McRogueFace scripts. - - Usage in tests: - def test_something(mcrf_exec): - passed, output = mcrf_exec("unit/my_test.py") - assert passed - """ - def _exec(script_path, timeout=None): - """ - Execute a McRogueFace script in headless mode. - - Args: - script_path: Path relative to tests/ directory - timeout: Override default timeout - - Returns: - (passed: bool, output: str) - """ - if timeout is None: - timeout = mcrf_timeout - - full_path = TESTS_DIR / script_path - - try: - result = subprocess.run( - [str(MCROGUEFACE), '--headless', '--exec', str(full_path)], - capture_output=True, - text=True, - timeout=timeout, - cwd=str(BUILD_DIR), - env=mcrf_env - ) - - output = result.stdout + result.stderr - passed = result.returncode == 0 - - # Check for PASS/FAIL in output - if 'FAIL' in output and 'PASS' not in output.split('FAIL')[-1]: - passed = False - - return passed, output - - except subprocess.TimeoutExpired: - return False, "TIMEOUT" - except Exception as e: - return False, str(e) - - return _exec - - -def pytest_collect_file(parent, file_path): - """Auto-discover McRogueFace test scripts.""" - # Only collect from unit/, integration/, regression/ directories - try: - rel_path = file_path.relative_to(TESTS_DIR) - except ValueError: - return None - - if rel_path.parts and rel_path.parts[0] in ('unit', 'integration', 'regression', 'docs', 'demo'): - if file_path.suffix == '.py' and file_path.name not in ('__init__.py', 'conftest.py'): - return McRFTestFile.from_parent(parent, path=file_path) - return None - - -def pytest_ignore_collect(collection_path, config): - """Prevent pytest from trying to import test scripts as Python modules.""" - try: - rel_path = collection_path.relative_to(TESTS_DIR) - if rel_path.parts and rel_path.parts[0] in ('unit', 'integration', 'regression', 'docs', 'demo'): - # Let our custom collector handle these, don't import them - return False # Don't ignore - we'll collect them our way - except ValueError: - pass - return None - - -class McRFTestFile(pytest.File): - """Custom test file for McRogueFace scripts.""" - - def collect(self): - """Yield a single test item for this script.""" - yield McRFTestItem.from_parent(self, name=self.path.stem) - - -class McRFTestItem(pytest.Item): - """Test item that runs a McRogueFace script.""" - - def runtest(self): - """Run the McRogueFace script.""" - timeout = self.config.getoption("--mcrf-timeout") - - env = os.environ.copy() - existing_ld = env.get('LD_LIBRARY_PATH', '') - env['LD_LIBRARY_PATH'] = f"{LIB_DIR}:{existing_ld}" if existing_ld else str(LIB_DIR) - - try: - result = subprocess.run( - [str(MCROGUEFACE), '--headless', '--exec', str(self.path)], - capture_output=True, - text=True, - timeout=timeout, - cwd=str(BUILD_DIR), - env=env - ) - - self.output = result.stdout + result.stderr - passed = result.returncode == 0 - - if 'FAIL' in self.output and 'PASS' not in self.output.split('FAIL')[-1]: - passed = False - - if not passed: - raise McRFTestException(self.output) - - except subprocess.TimeoutExpired: - self.output = "TIMEOUT" - raise McRFTestException("TIMEOUT") - - def repr_failure(self, excinfo): - """Format failure output.""" - if isinstance(excinfo.value, McRFTestException): - output = str(excinfo.value) - # Show last 10 lines - lines = output.strip().split('\n')[-10:] - return '\n'.join(lines) - return super().repr_failure(excinfo) - - def reportinfo(self): - """Report test location.""" - return self.path, None, f"mcrf:{self.path.relative_to(TESTS_DIR)}" - - -class McRFTestException(Exception): - """Exception raised when a McRogueFace test fails.""" - pass diff --git a/tests/demo/cookbook_showcase.py b/tests/demo/cookbook_showcase.py deleted file mode 100644 index 9095414..0000000 --- a/tests/demo/cookbook_showcase.py +++ /dev/null @@ -1,495 +0,0 @@ -#!/usr/bin/env python3 -""" -Cookbook Screenshot Showcase - Visual examples for cookbook recipes! - -Generates beautiful screenshots for cookbook pages. -Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/cookbook_showcase.py - -In headless mode, automation.screenshot() is SYNCHRONOUS - no timer dance needed! -""" -import mcrfpy -from mcrfpy import automation -import sys -import os - -# Output directory - in the docs site images folder -OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/cookbook" - -# Tile sprites from the labeled tileset -TILES = { - 'player_knight': 84, - 'player_mage': 85, - 'player_rogue': 86, - 'player_warrior': 87, - 'enemy_slime': 108, - 'enemy_orc': 120, - 'enemy_skeleton': 123, - 'floor_stone': 42, - 'wall_stone': 30, - 'wall_brick': 14, - 'torch': 72, - 'chest_closed': 89, - 'item_potion': 113, -} - - -def screenshot_health_bar(): - """Create a health bar showcase.""" - scene = mcrfpy.Scene("health_bar") - - # Dark background - bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600)) - bg.fill_color = mcrfpy.Color(20, 20, 30) - scene.children.append(bg) - - # Title - title = mcrfpy.Caption(text="Health Bar Recipe", pos=(50, 30)) - title.fill_color = mcrfpy.Color(255, 255, 255) - title.font_size = 28 - scene.children.append(title) - - subtitle = mcrfpy.Caption(text="Nested frames for dynamic UI elements", pos=(50, 60)) - subtitle.fill_color = mcrfpy.Color(180, 180, 200) - subtitle.font_size = 16 - scene.children.append(subtitle) - - # Example health bars at different levels - y_start = 120 - bar_configs = [ - ("Player - Full Health", 100, 100, mcrfpy.Color(50, 200, 50)), - ("Player - Damaged", 65, 100, mcrfpy.Color(200, 200, 50)), - ("Player - Critical", 20, 100, mcrfpy.Color(200, 50, 50)), - ("Boss - 3/4 Health", 750, 1000, mcrfpy.Color(150, 50, 150)), - ] - - for i, (label, current, maximum, color) in enumerate(bar_configs): - y = y_start + i * 100 - - # Label - lbl = mcrfpy.Caption(text=label, pos=(50, y)) - lbl.fill_color = mcrfpy.Color(220, 220, 220) - lbl.font_size = 18 - scene.children.append(lbl) - - # Background bar - bar_bg = mcrfpy.Frame(pos=(50, y + 30), size=(400, 30)) - bar_bg.fill_color = mcrfpy.Color(40, 40, 50) - bar_bg.outline = 2 - bar_bg.outline_color = mcrfpy.Color(80, 80, 100) - scene.children.append(bar_bg) - - # Fill bar (scaled to current/maximum) - fill_width = int(400 * (current / maximum)) - bar_fill = mcrfpy.Frame(pos=(50, y + 30), size=(fill_width, 30)) - bar_fill.fill_color = color - scene.children.append(bar_fill) - - # Text overlay - hp_text = mcrfpy.Caption(text=f"{current}/{maximum}", pos=(60, y + 35)) - hp_text.fill_color = mcrfpy.Color(255, 255, 255) - hp_text.font_size = 16 - scene.children.append(hp_text) - - scene.activate() - output_path = os.path.join(OUTPUT_DIR, "ui_health_bar.png") - automation.screenshot(output_path) - print(f" -> {output_path}") - - -def screenshot_fog_of_war(): - """Create a fog of war showcase.""" - scene = mcrfpy.Scene("fog_of_war") - - texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - grid = mcrfpy.Grid( - pos=(50, 80), - size=(700, 480), - grid_size=(16, 12), - texture=texture, - zoom=2.8 - ) - grid.fill_color = mcrfpy.Color(0, 0, 0) # Black for unknown areas - scene.children.append(grid) - - # Title - title = mcrfpy.Caption(text="Fog of War Recipe", pos=(50, 20)) - title.fill_color = mcrfpy.Color(255, 255, 255) - title.font_size = 28 - scene.children.append(title) - - subtitle = mcrfpy.Caption(text="Visible, discovered, and unknown areas", pos=(50, 50)) - subtitle.fill_color = mcrfpy.Color(180, 180, 200) - subtitle.font_size = 16 - scene.children.append(subtitle) - - # Fill floor - for y in range(12): - for x in range(16): - grid.at(x, y).tilesprite = TILES['floor_stone'] - - # Add walls - for x in range(16): - grid.at(x, 0).tilesprite = TILES['wall_stone'] - grid.at(x, 11).tilesprite = TILES['wall_stone'] - for y in range(12): - grid.at(0, y).tilesprite = TILES['wall_stone'] - grid.at(15, y).tilesprite = TILES['wall_stone'] - - # Interior walls (to break LOS) - for y in range(3, 8): - grid.at(8, y).tilesprite = TILES['wall_brick'] - - # Player (mage with light) - player = mcrfpy.Entity(grid_pos=(4, 6), texture=texture, sprite_index=TILES['player_mage']) - grid.entities.append(player) - - # Hidden enemies on the other side - enemy1 = mcrfpy.Entity(grid_pos=(12, 4), texture=texture, sprite_index=TILES['enemy_orc']) - grid.entities.append(enemy1) - enemy2 = mcrfpy.Entity(grid_pos=(13, 8), texture=texture, sprite_index=TILES['enemy_skeleton']) - grid.entities.append(enemy2) - - # Torch in visible area - torch = mcrfpy.Entity(grid_pos=(2, 3), texture=texture, sprite_index=TILES['torch']) - grid.entities.append(torch) - - grid.center = (4 * 16 + 8, 6 * 16 + 8) - - scene.activate() - output_path = os.path.join(OUTPUT_DIR, "grid_fog_of_war.png") - automation.screenshot(output_path) - print(f" -> {output_path}") - - -def screenshot_combat_melee(): - """Create a melee combat showcase.""" - scene = mcrfpy.Scene("combat_melee") - - texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - grid = mcrfpy.Grid( - pos=(50, 80), - size=(700, 480), - grid_size=(12, 9), - texture=texture, - zoom=3.5 - ) - grid.fill_color = mcrfpy.Color(20, 20, 30) - scene.children.append(grid) - - # Title - title = mcrfpy.Caption(text="Melee Combat Recipe", pos=(50, 20)) - title.fill_color = mcrfpy.Color(255, 255, 255) - title.font_size = 28 - scene.children.append(title) - - subtitle = mcrfpy.Caption(text="Bump-to-attack mechanics with damage calculation", pos=(50, 50)) - subtitle.fill_color = mcrfpy.Color(180, 180, 200) - subtitle.font_size = 16 - scene.children.append(subtitle) - - # Fill with dirt floor (battle arena feel) - for y in range(9): - for x in range(12): - grid.at(x, y).tilesprite = 50 # dirt - - # Brick walls - for x in range(12): - grid.at(x, 0).tilesprite = TILES['wall_brick'] - grid.at(x, 8).tilesprite = TILES['wall_brick'] - for y in range(9): - grid.at(0, y).tilesprite = TILES['wall_brick'] - grid.at(11, y).tilesprite = TILES['wall_brick'] - - # Player knight engaging orc! - player = mcrfpy.Entity(grid_pos=(4, 4), texture=texture, sprite_index=TILES['player_knight']) - grid.entities.append(player) - - enemy = mcrfpy.Entity(grid_pos=(6, 4), texture=texture, sprite_index=TILES['enemy_orc']) - grid.entities.append(enemy) - - # Fallen enemy (bones) - bones = mcrfpy.Entity(grid_pos=(8, 6), texture=texture, sprite_index=75) # bones - grid.entities.append(bones) - - # Potion for healing - potion = mcrfpy.Entity(grid_pos=(3, 2), texture=texture, sprite_index=TILES['item_potion']) - grid.entities.append(potion) - - grid.center = (5 * 16 + 8, 4 * 16 + 8) - - # Combat log UI - log_frame = mcrfpy.Frame(pos=(50, 520), size=(700, 60)) - log_frame.fill_color = mcrfpy.Color(30, 30, 40, 220) - log_frame.outline = 1 - log_frame.outline_color = mcrfpy.Color(60, 60, 80) - scene.children.append(log_frame) - - msg1 = mcrfpy.Caption(text="You hit the Orc for 8 damage!", pos=(10, 10)) - msg1.fill_color = mcrfpy.Color(255, 200, 100) - msg1.font_size = 14 - log_frame.children.append(msg1) - - msg2 = mcrfpy.Caption(text="The Orc hits you for 4 damage!", pos=(10, 30)) - msg2.fill_color = mcrfpy.Color(255, 100, 100) - msg2.font_size = 14 - log_frame.children.append(msg2) - - scene.activate() - output_path = os.path.join(OUTPUT_DIR, "combat_melee.png") - automation.screenshot(output_path) - print(f" -> {output_path}") - - -def screenshot_dungeon_generator(): - """Create a dungeon generator showcase.""" - scene = mcrfpy.Scene("dungeon_gen") - - texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - grid = mcrfpy.Grid( - pos=(50, 80), - size=(700, 480), - grid_size=(24, 16), - texture=texture, - zoom=2.0 - ) - grid.fill_color = mcrfpy.Color(10, 10, 15) - scene.children.append(grid) - - # Title - title = mcrfpy.Caption(text="Dungeon Generator Recipe", pos=(50, 20)) - title.fill_color = mcrfpy.Color(255, 255, 255) - title.font_size = 28 - scene.children.append(title) - - subtitle = mcrfpy.Caption(text="Procedural rooms connected by corridors", pos=(50, 50)) - subtitle.fill_color = mcrfpy.Color(180, 180, 200) - subtitle.font_size = 16 - scene.children.append(subtitle) - - # Fill with walls - for y in range(16): - for x in range(24): - grid.at(x, y).tilesprite = TILES['wall_stone'] - - # Carve rooms - rooms = [ - (2, 2, 6, 5), # Room 1 - (10, 2, 7, 5), # Room 2 - (18, 3, 5, 4), # Room 3 - (2, 9, 5, 5), # Room 4 - (10, 10, 6, 5), # Room 5 - (18, 9, 5, 6), # Room 6 - ] - - for rx, ry, rw, rh in rooms: - for y in range(ry, ry + rh): - for x in range(rx, rx + rw): - if x < 24 and y < 16: - grid.at(x, y).tilesprite = TILES['floor_stone'] - - # Carve corridors (horizontal and vertical) - # Room 1 to Room 2 - for x in range(7, 11): - grid.at(x, 4).tilesprite = 50 # dirt corridor - # Room 2 to Room 3 - for x in range(16, 19): - grid.at(x, 4).tilesprite = 50 - # Room 1 to Room 4 - for y in range(6, 10): - grid.at(4, y).tilesprite = 50 - # Room 2 to Room 5 - for y in range(6, 11): - grid.at(13, y).tilesprite = 50 - # Room 3 to Room 6 - for y in range(6, 10): - grid.at(20, y).tilesprite = 50 - # Room 5 to Room 6 - for x in range(15, 19): - grid.at(x, 12).tilesprite = 50 - - # Add player in first room - player = mcrfpy.Entity(grid_pos=(4, 4), texture=texture, sprite_index=TILES['player_knight']) - grid.entities.append(player) - - # Add decorations - grid.entities.append(mcrfpy.Entity(grid_pos=(3, 3), texture=texture, sprite_index=TILES['torch'])) - grid.entities.append(mcrfpy.Entity(grid_pos=(12, 4), texture=texture, sprite_index=TILES['torch'])) - grid.entities.append(mcrfpy.Entity(grid_pos=(19, 11), texture=texture, sprite_index=TILES['chest_closed'])) - grid.entities.append(mcrfpy.Entity(grid_pos=(13, 12), texture=texture, sprite_index=TILES['enemy_slime'])) - grid.entities.append(mcrfpy.Entity(grid_pos=(20, 5), texture=texture, sprite_index=TILES['enemy_skeleton'])) - - grid.center = (12 * 16, 8 * 16) - - scene.activate() - output_path = os.path.join(OUTPUT_DIR, "grid_dungeon_generator.png") - automation.screenshot(output_path) - print(f" -> {output_path}") - - -def screenshot_floating_text(): - """Create a floating text/damage numbers showcase.""" - scene = mcrfpy.Scene("floating_text") - - texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - grid = mcrfpy.Grid( - pos=(50, 100), - size=(700, 420), - grid_size=(12, 8), - texture=texture, - zoom=3.5 - ) - grid.fill_color = mcrfpy.Color(20, 20, 30) - scene.children.append(grid) - - # Title - title = mcrfpy.Caption(text="Floating Text Recipe", pos=(50, 20)) - title.fill_color = mcrfpy.Color(255, 255, 255) - title.font_size = 28 - scene.children.append(title) - - subtitle = mcrfpy.Caption(text="Animated damage numbers and status messages", pos=(50, 50)) - subtitle.fill_color = mcrfpy.Color(180, 180, 200) - subtitle.font_size = 16 - scene.children.append(subtitle) - - # Fill floor - for y in range(8): - for x in range(12): - grid.at(x, y).tilesprite = TILES['floor_stone'] - - # Walls - for x in range(12): - grid.at(x, 0).tilesprite = TILES['wall_stone'] - grid.at(x, 7).tilesprite = TILES['wall_stone'] - for y in range(8): - grid.at(0, y).tilesprite = TILES['wall_stone'] - grid.at(11, y).tilesprite = TILES['wall_stone'] - - # Player and enemy in combat - player = mcrfpy.Entity(grid_pos=(4, 4), texture=texture, sprite_index=TILES['player_warrior']) - grid.entities.append(player) - - enemy = mcrfpy.Entity(grid_pos=(7, 4), texture=texture, sprite_index=TILES['enemy_orc']) - grid.entities.append(enemy) - - grid.center = (5.5 * 16, 4 * 16) - - # Floating damage numbers (as captions positioned over entities) - # These would normally animate upward - dmg1 = mcrfpy.Caption(text="-12", pos=(330, 240)) - dmg1.fill_color = mcrfpy.Color(255, 80, 80) - dmg1.font_size = 24 - scene.children.append(dmg1) - - dmg2 = mcrfpy.Caption(text="-5", pos=(500, 260)) - dmg2.fill_color = mcrfpy.Color(255, 100, 100) - dmg2.font_size = 20 - scene.children.append(dmg2) - - crit = mcrfpy.Caption(text="CRITICAL!", pos=(280, 200)) - crit.fill_color = mcrfpy.Color(255, 200, 50) - crit.font_size = 18 - scene.children.append(crit) - - heal = mcrfpy.Caption(text="+8", pos=(320, 280)) - heal.fill_color = mcrfpy.Color(100, 255, 100) - heal.font_size = 20 - scene.children.append(heal) - - scene.activate() - output_path = os.path.join(OUTPUT_DIR, "effects_floating_text.png") - automation.screenshot(output_path) - print(f" -> {output_path}") - - -def screenshot_message_log(): - """Create a message log showcase.""" - scene = mcrfpy.Scene("message_log") - - # Dark background - bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600)) - bg.fill_color = mcrfpy.Color(20, 20, 30) - scene.children.append(bg) - - # Title - title = mcrfpy.Caption(text="Message Log Recipe", pos=(50, 30)) - title.fill_color = mcrfpy.Color(255, 255, 255) - title.font_size = 28 - scene.children.append(title) - - subtitle = mcrfpy.Caption(text="Scrollable combat and event messages", pos=(50, 60)) - subtitle.fill_color = mcrfpy.Color(180, 180, 200) - subtitle.font_size = 16 - scene.children.append(subtitle) - - # Message log frame - log_frame = mcrfpy.Frame(pos=(50, 100), size=(700, 400)) - log_frame.fill_color = mcrfpy.Color(30, 30, 40) - log_frame.outline = 2 - log_frame.outline_color = mcrfpy.Color(60, 60, 80) - scene.children.append(log_frame) - - # Sample messages with colors - messages = [ - ("Welcome to the dungeon!", mcrfpy.Color(200, 200, 255)), - ("You see a dark corridor ahead.", mcrfpy.Color(180, 180, 180)), - ("A goblin appears!", mcrfpy.Color(255, 200, 100)), - ("You hit the Goblin for 8 damage!", mcrfpy.Color(255, 255, 150)), - ("The Goblin hits you for 3 damage!", mcrfpy.Color(255, 100, 100)), - ("You hit the Goblin for 12 damage! Critical hit!", mcrfpy.Color(255, 200, 50)), - ("The Goblin dies!", mcrfpy.Color(150, 255, 150)), - ("You found a Healing Potion.", mcrfpy.Color(100, 200, 255)), - ("An Orc blocks your path!", mcrfpy.Color(255, 150, 100)), - ("You drink the Healing Potion. +15 HP", mcrfpy.Color(100, 255, 100)), - ("You hit the Orc for 6 damage!", mcrfpy.Color(255, 255, 150)), - ("The Orc hits you for 8 damage!", mcrfpy.Color(255, 100, 100)), - ] - - for i, (msg, color) in enumerate(messages): - caption = mcrfpy.Caption(text=msg, pos=(15, 15 + i * 30)) - caption.fill_color = color - caption.font_size = 16 - log_frame.children.append(caption) - - # Scroll indicator - scroll = mcrfpy.Caption(text="▼ More messages below", pos=(580, 370)) - scroll.fill_color = mcrfpy.Color(100, 100, 120) - scroll.font_size = 12 - log_frame.children.append(scroll) - - scene.activate() - output_path = os.path.join(OUTPUT_DIR, "ui_message_log.png") - automation.screenshot(output_path) - print(f" -> {output_path}") - - -def main(): - """Generate all cookbook screenshots!""" - os.makedirs(OUTPUT_DIR, exist_ok=True) - - print("=== Cookbook Screenshot Showcase ===") - print(f"Output: {OUTPUT_DIR}\n") - - showcases = [ - ('Health Bar UI', screenshot_health_bar), - ('Fog of War', screenshot_fog_of_war), - ('Melee Combat', screenshot_combat_melee), - ('Dungeon Generator', screenshot_dungeon_generator), - ('Floating Text', screenshot_floating_text), - ('Message Log', screenshot_message_log), - ] - - for name, func in showcases: - print(f"Generating {name}...") - try: - func() - except Exception as e: - print(f" ERROR: {e}") - import traceback - traceback.print_exc() - - print("\n=== All cookbook screenshots generated! ===") - sys.exit(0) - - -main() diff --git a/tests/demo/new_features_showcase.py b/tests/demo/new_features_showcase.py deleted file mode 100644 index 29cdee0..0000000 --- a/tests/demo/new_features_showcase.py +++ /dev/null @@ -1,255 +0,0 @@ -#!/usr/bin/env python3 -""" -New Features Screenshot Showcase - Alignment + Dijkstra-to-HeightMap - -Generates screenshots for the new API cookbook recipes. -Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/new_features_showcase.py -""" -import mcrfpy -from mcrfpy import automation -import sys -import os - -OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/cookbook" - - -def screenshot_alignment(): - """Create an alignment system showcase.""" - scene = mcrfpy.Scene("alignment") - - # Dark background - bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600)) - bg.fill_color = mcrfpy.Color(20, 20, 30) - scene.children.append(bg) - - # Title - title = mcrfpy.Caption(text="UI Alignment System", pos=(50, 20)) - title.fill_color = mcrfpy.Color(255, 255, 255) - title.font_size = 28 - scene.children.append(title) - - subtitle = mcrfpy.Caption(text="Auto-positioning with reactive resize", pos=(50, 50)) - subtitle.fill_color = mcrfpy.Color(180, 180, 200) - subtitle.font_size = 16 - scene.children.append(subtitle) - - # Demo container - container = mcrfpy.Frame(pos=(100, 100), size=(600, 400)) - container.fill_color = mcrfpy.Color(40, 40, 50) - container.outline = 2 - container.outline_color = mcrfpy.Color(80, 80, 100) - scene.children.append(container) - - # Container label - container_label = mcrfpy.Caption(text="Parent Container (600x400)", pos=(10, 10)) - container_label.fill_color = mcrfpy.Color(100, 100, 120) - container_label.font_size = 12 - container.children.append(container_label) - - # 9 alignment positions demo - alignments = [ - (mcrfpy.Alignment.TOP_LEFT, "TL", mcrfpy.Color(200, 80, 80)), - (mcrfpy.Alignment.TOP_CENTER, "TC", mcrfpy.Color(200, 150, 80)), - (mcrfpy.Alignment.TOP_RIGHT, "TR", mcrfpy.Color(200, 200, 80)), - (mcrfpy.Alignment.CENTER_LEFT, "CL", mcrfpy.Color(80, 200, 80)), - (mcrfpy.Alignment.CENTER, "C", mcrfpy.Color(80, 200, 200)), - (mcrfpy.Alignment.CENTER_RIGHT, "CR", mcrfpy.Color(80, 80, 200)), - (mcrfpy.Alignment.BOTTOM_LEFT, "BL", mcrfpy.Color(150, 80, 200)), - (mcrfpy.Alignment.BOTTOM_CENTER, "BC", mcrfpy.Color(200, 80, 200)), - (mcrfpy.Alignment.BOTTOM_RIGHT, "BR", mcrfpy.Color(200, 80, 150)), - ] - - for align, label, color in alignments: - box = mcrfpy.Frame(pos=(0, 0), size=(60, 40)) - box.fill_color = color - box.outline = 1 - box.outline_color = mcrfpy.Color(255, 255, 255) - box.align = align - if align != mcrfpy.Alignment.CENTER: - box.margin = 15.0 - - # Label inside box - text = mcrfpy.Caption(text=label, pos=(0, 0)) - text.fill_color = mcrfpy.Color(255, 255, 255) - text.font_size = 16 - text.align = mcrfpy.Alignment.CENTER - box.children.append(text) - - container.children.append(box) - - # Legend - legend = mcrfpy.Caption(text="TL=TOP_LEFT TC=TOP_CENTER TR=TOP_RIGHT etc.", pos=(100, 520)) - legend.fill_color = mcrfpy.Color(150, 150, 170) - legend.font_size = 14 - scene.children.append(legend) - - legend2 = mcrfpy.Caption(text="All boxes have margin=15 except CENTER", pos=(100, 545)) - legend2.fill_color = mcrfpy.Color(150, 150, 170) - legend2.font_size = 14 - scene.children.append(legend2) - - scene.activate() - output_path = os.path.join(OUTPUT_DIR, "ui_alignment.png") - automation.screenshot(output_path) - print(f" -> {output_path}") - - -def screenshot_dijkstra_heightmap(): - """Create a dijkstra-to-heightmap showcase.""" - scene = mcrfpy.Scene("dijkstra_hmap") - - # Title - title = mcrfpy.Caption(text="Dijkstra to HeightMap", pos=(50, 20)) - title.fill_color = mcrfpy.Color(255, 255, 255) - title.font_size = 28 - scene.children.append(title) - - subtitle = mcrfpy.Caption(text="Distance-based gradients for fog, difficulty, and visualization", pos=(50, 50)) - subtitle.fill_color = mcrfpy.Color(180, 180, 200) - subtitle.font_size = 16 - scene.children.append(subtitle) - - # Create grid for dijkstra visualization - texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - grid = mcrfpy.Grid( - pos=(50, 90), - size=(350, 350), - grid_size=(16, 16), - texture=texture, - zoom=1.3 - ) - grid.fill_color = mcrfpy.Color(20, 20, 30) - scene.children.append(grid) - - # Initialize grid - for y in range(16): - for x in range(16): - grid.at((x, y)).walkable = True - grid.at((x, y)).tilesprite = 42 # floor - - # Add some walls - for i in range(5, 11): - grid.at((i, 5)).walkable = False - grid.at((i, 5)).tilesprite = 30 # wall - grid.at((5, i)).walkable = False - grid.at((5, i)).tilesprite = 30 - - # Player at center - player = mcrfpy.Entity(grid_pos=(8, 8), texture=texture, sprite_index=84) - grid.entities.append(player) - - # Get dijkstra and create color visualization - dijkstra = grid.get_dijkstra_map((8, 8)) - hmap = dijkstra.to_heightmap(unreachable=-1.0) - - # Find max for normalization - max_dist = 0 - for y in range(16): - for x in range(16): - d = hmap[(x, y)] - if d > max_dist and d >= 0: - max_dist = d - - # Second visualization panel - color gradient - viz_frame = mcrfpy.Frame(pos=(420, 90), size=(350, 350)) - viz_frame.fill_color = mcrfpy.Color(30, 30, 40) - viz_frame.outline = 2 - viz_frame.outline_color = mcrfpy.Color(60, 60, 80) - scene.children.append(viz_frame) - - viz_label = mcrfpy.Caption(text="Distance Visualization", pos=(80, 10)) - viz_label.fill_color = mcrfpy.Color(200, 200, 220) - viz_label.font_size = 16 - viz_frame.children.append(viz_label) - - # Draw colored squares for each cell - cell_size = 20 - offset_x = 15 - offset_y = 35 - - for y in range(16): - for x in range(16): - dist = hmap[(x, y)] - - if dist < 0: - # Unreachable - dark red - color = mcrfpy.Color(60, 0, 0) - elif dist == 0: - # Source - bright yellow - color = mcrfpy.Color(255, 255, 0) - else: - # Gradient: green (near) to blue (far) - t = min(1.0, dist / max_dist) - r = 0 - g = int(200 * (1 - t)) - b = int(200 * t) - color = mcrfpy.Color(r, g, b) - - cell = mcrfpy.Frame( - pos=(offset_x + x * cell_size, offset_y + y * cell_size), - size=(cell_size - 1, cell_size - 1) - ) - cell.fill_color = color - viz_frame.children.append(cell) - - # Legend - legend_frame = mcrfpy.Frame(pos=(50, 460), size=(720, 100)) - legend_frame.fill_color = mcrfpy.Color(30, 30, 40) - legend_frame.outline = 1 - legend_frame.outline_color = mcrfpy.Color(60, 60, 80) - scene.children.append(legend_frame) - - leg1 = mcrfpy.Caption(text="Use Cases:", pos=(15, 10)) - leg1.fill_color = mcrfpy.Color(255, 255, 255) - leg1.font_size = 16 - legend_frame.children.append(leg1) - - uses = [ - "Distance-based enemy difficulty", - "Fog intensity gradients", - "Pathfinding visualization", - "Influence maps for AI", - ] - for i, use in enumerate(uses): - txt = mcrfpy.Caption(text=f"- {use}", pos=(15 + (i // 2) * 350, 35 + (i % 2) * 25)) - txt.fill_color = mcrfpy.Color(180, 180, 200) - txt.font_size = 14 - legend_frame.children.append(txt) - - # Color key - key_label = mcrfpy.Caption(text="Yellow=Source Green=Near Blue=Far Red=Blocked", pos=(420, 450)) - key_label.fill_color = mcrfpy.Color(150, 150, 170) - key_label.font_size = 12 - scene.children.append(key_label) - - scene.activate() - output_path = os.path.join(OUTPUT_DIR, "grid_dijkstra_heightmap.png") - automation.screenshot(output_path) - print(f" -> {output_path}") - - -def main(): - os.makedirs(OUTPUT_DIR, exist_ok=True) - - print("=== New Features Screenshot Showcase ===") - print(f"Output: {OUTPUT_DIR}\n") - - showcases = [ - ('Alignment System', screenshot_alignment), - ('Dijkstra to HeightMap', screenshot_dijkstra_heightmap), - ] - - for name, func in showcases: - print(f"Generating {name}...") - try: - func() - except Exception as e: - print(f" ERROR: {e}") - import traceback - traceback.print_exc() - - print("\n=== New feature screenshots generated! ===") - sys.exit(0) - - -main() diff --git a/tests/demo/procgen_showcase.py b/tests/demo/procgen_showcase.py deleted file mode 100644 index 2cdba0a..0000000 --- a/tests/demo/procgen_showcase.py +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/env python3 -"""Generate screenshots for procgen cookbook recipes. - -Uses Frame-based visualization since Grid cell colors use ColorLayer API. -""" -import mcrfpy -from mcrfpy import automation -import sys - -OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/cookbook" - -# Simple PRNG -_seed = 42 - -def random(): - global _seed - _seed = (_seed * 1103515245 + 12345) & 0x7fffffff - return (_seed >> 16) / 32768.0 - -def seed(n): - global _seed - _seed = n - -def choice(lst): - return lst[int(random() * len(lst))] - - -def screenshot_cellular_caves(): - """Generate cellular automata caves visualization.""" - print("Generating cellular automata caves...") - - scene = mcrfpy.Scene("caves") - scene.activate() - mcrfpy.step(0.1) - - # Background - bg = mcrfpy.Frame(pos=(0, 0), size=(640, 500)) - bg.fill_color = mcrfpy.Color(15, 15, 25) - scene.children.append(bg) - - width, height = 50, 35 - cell_size = 12 - seed(42) - - # Store cell data - cells = [[False for _ in range(width)] for _ in range(height)] - - # Step 1: Random noise (45% walls) - for y in range(height): - for x in range(width): - if x == 0 or x == width-1 or y == 0 or y == height-1: - cells[y][x] = True # Border walls - else: - cells[y][x] = random() < 0.45 - - # Step 2: Smooth with cellular automata (5 iterations) - for _ in range(5): - new_cells = [[cells[y][x] for x in range(width)] for y in range(height)] - for y in range(1, height - 1): - for x in range(1, width - 1): - wall_count = sum( - 1 for dy in [-1, 0, 1] for dx in [-1, 0, 1] - if not (dx == 0 and dy == 0) and cells[y + dy][x + dx] - ) - if wall_count >= 5: - new_cells[y][x] = True - elif wall_count <= 3: - new_cells[y][x] = False - cells = new_cells - - # Find largest connected region - visited = set() - regions = [] - - def flood_fill(start_x, start_y): - result = [] - stack = [(start_x, start_y)] - while stack: - x, y = stack.pop() - if (x, y) in visited or x < 0 or x >= width or y < 0 or y >= height: - continue - if cells[y][x]: # Wall - continue - visited.add((x, y)) - result.append((x, y)) - stack.extend([(x+1, y), (x-1, y), (x, y+1), (x, y-1)]) - return result - - for y in range(height): - for x in range(width): - if (x, y) not in visited and not cells[y][x]: - region = flood_fill(x, y) - if region: - regions.append(region) - - largest = max(regions, key=len) if regions else [] - largest_set = set(largest) - - # Draw cells as colored frames - for y in range(height): - for x in range(width): - px = 20 + x * cell_size - py = 20 + y * cell_size - cell = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) - - if cells[y][x]: - cell.fill_color = mcrfpy.Color(60, 40, 30) # Wall - elif (x, y) in largest_set: - cell.fill_color = mcrfpy.Color(50, 90, 100) # Main cave - else: - cell.fill_color = mcrfpy.Color(45, 35, 30) # Filled region - - scene.children.append(cell) - - # Title - title = mcrfpy.Caption(text="Cellular Automata Caves", pos=(20, 445)) - title.fill_color = mcrfpy.Color(200, 200, 200) - title.font_size = 18 - scene.children.append(title) - - subtitle = mcrfpy.Caption(text="45% fill, 5 iterations, largest region preserved", pos=(20, 468)) - subtitle.fill_color = mcrfpy.Color(130, 130, 140) - subtitle.font_size = 12 - scene.children.append(subtitle) - - mcrfpy.step(0.1) - automation.screenshot(OUTPUT_DIR + "/procgen_cellular_caves.png") - print("Saved: procgen_cellular_caves.png") - - -def screenshot_wfc(): - """Generate WFC pattern visualization.""" - print("Generating WFC patterns...") - - scene = mcrfpy.Scene("wfc") - scene.activate() - mcrfpy.step(0.1) - - # Background - bg = mcrfpy.Frame(pos=(0, 0), size=(640, 500)) - bg.fill_color = mcrfpy.Color(15, 20, 15) - scene.children.append(bg) - - width, height = 40, 28 - cell_size = 15 - seed(123) - - GRASS, DIRT, WATER, SAND = 0, 1, 2, 3 - colors = { - GRASS: mcrfpy.Color(60, 120, 50), - DIRT: mcrfpy.Color(100, 70, 40), - WATER: mcrfpy.Color(40, 80, 140), - SAND: mcrfpy.Color(180, 160, 90) - } - - rules = { - GRASS: {'N': [GRASS, DIRT, SAND], 'S': [GRASS, DIRT, SAND], - 'E': [GRASS, DIRT, SAND], 'W': [GRASS, DIRT, SAND]}, - DIRT: {'N': [GRASS, DIRT], 'S': [GRASS, DIRT], - 'E': [GRASS, DIRT], 'W': [GRASS, DIRT]}, - WATER: {'N': [WATER, SAND], 'S': [WATER, SAND], - 'E': [WATER, SAND], 'W': [WATER, SAND]}, - SAND: {'N': [GRASS, WATER, SAND], 'S': [GRASS, WATER, SAND], - 'E': [GRASS, WATER, SAND], 'W': [GRASS, WATER, SAND]} - } - - tiles = set(rules.keys()) - possibilities = {(x, y): set(tiles) for y in range(height) for x in range(width)} - result = {} - - # Seed water lake - for x in range(22, 32): - for y in range(8, 18): - possibilities[(x, y)] = {WATER} - result[(x, y)] = WATER - - # Seed dirt path - for y in range(10, 18): - possibilities[(3, y)] = {DIRT} - result[(3, y)] = DIRT - - directions = {'N': (0, -1), 'S': (0, 1), 'E': (1, 0), 'W': (-1, 0)} - - def propagate(sx, sy): - stack = [(sx, sy)] - while stack: - x, y = stack.pop() - current = possibilities[(x, y)] - for dir_name, (dx, dy) in directions.items(): - nx, ny = x + dx, y + dy - if not (0 <= nx < width and 0 <= ny < height): - continue - neighbor = possibilities[(nx, ny)] - if len(neighbor) == 1: - continue - allowed = set() - for tile in current: - if dir_name in rules[tile]: - allowed.update(rules[tile][dir_name]) - new_opts = neighbor & allowed - if new_opts and new_opts != neighbor: - possibilities[(nx, ny)] = new_opts - stack.append((nx, ny)) - - # Propagate from seeds - for x in range(22, 32): - for y in range(8, 18): - propagate(x, y) - for y in range(10, 18): - propagate(3, y) - - # Collapse - for _ in range(width * height): - best, best_e = None, 1000.0 - for pos, opts in possibilities.items(): - if len(opts) > 1: - e = len(opts) + random() * 0.1 - if e < best_e: - best_e, best = e, pos - - if best is None: - break - - x, y = best - opts = list(possibilities[(x, y)]) - if not opts: - break - - weights = {GRASS: 5, DIRT: 2, WATER: 1, SAND: 2} - weighted = [] - for t in opts: - weighted.extend([t] * weights.get(t, 1)) - chosen = choice(weighted) if weighted else GRASS - - possibilities[(x, y)] = {chosen} - result[(x, y)] = chosen - propagate(x, y) - - # Fill remaining - for y in range(height): - for x in range(width): - if (x, y) not in result: - opts = list(possibilities[(x, y)]) - result[(x, y)] = choice(opts) if opts else GRASS - - # Draw - for y in range(height): - for x in range(width): - px = 20 + x * cell_size - py = 20 + y * cell_size - cell = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1)) - cell.fill_color = colors[result[(x, y)]] - scene.children.append(cell) - - # Title - title = mcrfpy.Caption(text="Wave Function Collapse", pos=(20, 445)) - title.fill_color = mcrfpy.Color(200, 200, 200) - title.font_size = 18 - scene.children.append(title) - - subtitle = mcrfpy.Caption(text="Constraint-based terrain (seeded lake + path)", pos=(20, 468)) - subtitle.fill_color = mcrfpy.Color(130, 140, 130) - subtitle.font_size = 12 - scene.children.append(subtitle) - - # Legend - for i, (name, tid) in enumerate([("Grass", GRASS), ("Dirt", DIRT), ("Sand", SAND), ("Water", WATER)]): - lx, ly = 480, 445 + i * 14 - swatch = mcrfpy.Frame(pos=(lx, ly), size=(12, 12)) - swatch.fill_color = colors[tid] - scene.children.append(swatch) - label = mcrfpy.Caption(text=name, pos=(lx + 16, ly)) - label.fill_color = mcrfpy.Color(150, 150, 150) - label.font_size = 11 - scene.children.append(label) - - mcrfpy.step(0.1) - automation.screenshot(OUTPUT_DIR + "/procgen_wfc.png") - print("Saved: procgen_wfc.png") - - -if __name__ == "__main__": - screenshot_cellular_caves() - screenshot_wfc() - print("\nDone!") - sys.exit(0) diff --git a/tests/demo/simple_showcase.py b/tests/demo/simple_showcase.py deleted file mode 100644 index eeb63a1..0000000 --- a/tests/demo/simple_showcase.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple Tutorial Screenshot Generator - -This creates ONE screenshot - the part01 tutorial showcase. -Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/simple_showcase.py - -NOTE: In headless mode, automation.screenshot() is SYNCHRONOUS - it renders -and captures immediately. No timer dance needed! -""" -import mcrfpy -from mcrfpy import automation -import sys -import os - -# Output -OUTPUT_PATH = "/opt/goblincorps/repos/mcrogueface.github.io/images/tutorials/part_01_grid_movement.png" - -# Tile sprites from the labeled tileset -PLAYER_KNIGHT = 84 -FLOOR_STONE = 42 -WALL_STONE = 30 -TORCH = 72 -BARREL = 73 -SKULL = 74 - -def main(): - """Create the part01 showcase screenshot.""" - # Ensure output dir exists - os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True) - - # Create scene - scene = mcrfpy.Scene("showcase") - - # Load texture - texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - - # Create grid - bigger zoom for visibility - grid = mcrfpy.Grid( - pos=(50, 80), - size=(700, 480), - grid_size=(12, 9), - texture=texture, - zoom=3.5 - ) - grid.fill_color = mcrfpy.Color(20, 20, 30) - scene.children.append(grid) - - # Fill with floor - for y in range(9): - for x in range(12): - grid.at(x, y).tilesprite = FLOOR_STONE - - # Add wall border - for x in range(12): - grid.at(x, 0).tilesprite = WALL_STONE - grid.at(x, 0).walkable = False - grid.at(x, 8).tilesprite = WALL_STONE - grid.at(x, 8).walkable = False - for y in range(9): - grid.at(0, y).tilesprite = WALL_STONE - grid.at(0, y).walkable = False - grid.at(11, y).tilesprite = WALL_STONE - grid.at(11, y).walkable = False - - # Add player entity - a knight! - player = mcrfpy.Entity( - grid_pos=(6, 4), - texture=texture, - sprite_index=PLAYER_KNIGHT - ) - grid.entities.append(player) - - # Add decorations - for pos, sprite in [((2, 2), TORCH), ((9, 2), TORCH), ((2, 6), BARREL), ((9, 6), SKULL)]: - entity = mcrfpy.Entity(grid_pos=pos, texture=texture, sprite_index=sprite) - grid.entities.append(entity) - - # Center camera on player - grid.center = (6 * 16 + 8, 4 * 16 + 8) - - # Add title - title = mcrfpy.Caption(text="Part 1: The '@' and the Dungeon Grid", pos=(50, 20)) - title.fill_color = mcrfpy.Color(255, 255, 255) - title.font_size = 28 - scene.children.append(title) - - subtitle = mcrfpy.Caption(text="Creating a grid, placing entities, handling input", pos=(50, 50)) - subtitle.fill_color = mcrfpy.Color(180, 180, 200) - subtitle.font_size = 16 - scene.children.append(subtitle) - - # Activate scene - scene.activate() - - # In headless mode, screenshot() is synchronous - renders then captures! - result = automation.screenshot(OUTPUT_PATH) - print(f"Screenshot saved: {OUTPUT_PATH} (result: {result})") - sys.exit(0) - - -# Run it -main() diff --git a/tests/demo/tutorial_screenshots.py b/tests/demo/tutorial_screenshots.py deleted file mode 100644 index ee14e03..0000000 --- a/tests/demo/tutorial_screenshots.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env python3 -""" -Tutorial Screenshot Generator - -Usage: - ./mcrogueface --headless --exec tests/demo/tutorial_screenshots.py - -Extracts code from tutorial markdown files and generates screenshots. -""" -import mcrfpy -from mcrfpy import automation -import sys -import os -import re - -# Paths -DOCS_REPO = "/opt/goblincorps/repos/mcrogueface.github.io" -TUTORIAL_DIR = os.path.join(DOCS_REPO, "tutorial") -OUTPUT_DIR = os.path.join(DOCS_REPO, "images", "tutorials") - -# Tutorials to process (in order) -TUTORIALS = [ - "part_01_grid_movement.md", - "part_02_tiles_collision.md", - "part_03_dungeon_generation.md", - "part_04_fov.md", - "part_05_enemies.md", - "part_06_combat.md", - "part_07_ui.md", -] - - -def extract_code_from_markdown(filepath): - """Extract the main Python code block from a tutorial markdown file.""" - with open(filepath, 'r') as f: - content = f.read() - - # Find code blocks after "## The Complete Code" header - # Look for the first python code block after that header - complete_code_match = re.search( - r'##\s+The Complete Code.*?```python\s*\n(.*?)```', - content, - re.DOTALL | re.IGNORECASE - ) - - if complete_code_match: - return complete_code_match.group(1) - - # Fallback: just get the first large python code block - code_blocks = re.findall(r'```python\s*\n(.*?)```', content, re.DOTALL) - if code_blocks: - # Return the largest code block (likely the main example) - return max(code_blocks, key=len) - - return None - - -def add_screenshot_hook(code, screenshot_path): - """Add screenshot capture code to the end of the script.""" - # Add code to take screenshot after a brief delay - hook_code = f''' - -# === Screenshot capture hook (added by tutorial_screenshots.py) === -import mcrfpy -from mcrfpy import automation -import sys - -_screenshot_taken = [False] - -def _take_screenshot(timer, runtime): - if not _screenshot_taken[0]: - _screenshot_taken[0] = True - automation.screenshot("{screenshot_path}") - print(f"Screenshot saved: {screenshot_path}") - sys.exit(0) - -# Wait a moment for scene to render, then capture -mcrfpy.Timer("_screenshot_hook", _take_screenshot, 200) -''' - return code + hook_code - - -class TutorialScreenshotter: - """Manages tutorial screenshot generation.""" - - def __init__(self): - self.tutorials = [] - self.current_index = 0 - - def load_tutorials(self): - """Load and parse all tutorial files.""" - for filename in TUTORIALS: - filepath = os.path.join(TUTORIAL_DIR, filename) - if not os.path.exists(filepath): - print(f"Warning: {filepath} not found, skipping") - continue - - code = extract_code_from_markdown(filepath) - if code: - # Generate output filename - base = os.path.splitext(filename)[0] - screenshot_name = f"{base}.png" - self.tutorials.append({ - 'name': filename, - 'code': code, - 'screenshot': screenshot_name, - 'filepath': filepath, - }) - print(f"Loaded: {filename}") - else: - print(f"Warning: No code found in {filename}") - - def run(self): - """Generate all screenshots.""" - # Ensure output directory exists - os.makedirs(OUTPUT_DIR, exist_ok=True) - - print(f"\nGenerating {len(self.tutorials)} tutorial screenshots...") - print(f"Output directory: {OUTPUT_DIR}\n") - - self.process_next() - - def process_next(self): - """Process the next tutorial.""" - if self.current_index >= len(self.tutorials): - print("\nAll screenshots generated!") - sys.exit(0) - return - - tutorial = self.tutorials[self.current_index] - print(f"[{self.current_index + 1}/{len(self.tutorials)}] Processing {tutorial['name']}...") - - # Add screenshot hook to the code - screenshot_path = os.path.join(OUTPUT_DIR, tutorial['screenshot']) - modified_code = add_screenshot_hook(tutorial['code'], screenshot_path) - - # Write to temp file and execute - temp_path = f"/tmp/tutorial_screenshot_{self.current_index}.py" - with open(temp_path, 'w') as f: - f.write(modified_code) - - try: - # Execute the code - exec(compile(modified_code, temp_path, 'exec'), {'__name__': '__main__'}) - except Exception as e: - print(f"Error processing {tutorial['name']}: {e}") - self.current_index += 1 - self.process_next() - finally: - try: - os.unlink(temp_path) - except: - pass - - -def main(): - """Main entry point.""" - screenshotter = TutorialScreenshotter() - screenshotter.load_tutorials() - - if not screenshotter.tutorials: - print("No tutorials found to process!") - sys.exit(1) - - screenshotter.run() - - -# Run when executed -main() diff --git a/tests/demo/tutorial_showcase.py b/tests/demo/tutorial_showcase.py deleted file mode 100644 index 6f22445..0000000 --- a/tests/demo/tutorial_showcase.py +++ /dev/null @@ -1,426 +0,0 @@ -#!/usr/bin/env python3 -""" -Tutorial Screenshot Showcase - ALL THE SCREENSHOTS! - -Generates beautiful screenshots for all tutorial parts. -Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/tutorial_showcase.py - -In headless mode, automation.screenshot() is SYNCHRONOUS - no timer dance needed! -""" -import mcrfpy -from mcrfpy import automation -import sys -import os - -# Output directory -OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/tutorials" - -# Tile meanings from the labeled tileset - the FUN sprites! -TILES = { - # Players - knights and heroes! - 'player_knight': 84, - 'player_mage': 85, - 'player_rogue': 86, - 'player_warrior': 87, - 'player_archer': 88, - 'player_alt1': 96, - 'player_alt2': 97, - 'player_alt3': 98, - - # Enemies - scary! - 'enemy_slime': 108, - 'enemy_bat': 109, - 'enemy_spider': 110, - 'enemy_rat': 111, - 'enemy_orc': 120, - 'enemy_troll': 121, - 'enemy_ghost': 122, - 'enemy_skeleton': 123, - 'enemy_demon': 124, - 'enemy_boss': 92, - - # Terrain - 'floor_stone': 42, - 'floor_wood': 49, - 'floor_grass': 48, - 'floor_dirt': 50, - 'wall_stone': 30, - 'wall_brick': 14, - 'wall_mossy': 28, - - # Items - 'item_potion': 113, - 'item_scroll': 114, - 'item_key': 115, - 'item_coin': 116, - - # Equipment - 'equip_sword': 101, - 'equip_shield': 102, - 'equip_helm': 103, - 'equip_armor': 104, - - # Chests and doors - 'chest_closed': 89, - 'chest_open': 90, - 'door_closed': 33, - 'door_open': 35, - - # Decorations - 'torch': 72, - 'barrel': 73, - 'skull': 74, - 'bones': 75, -} - - -class TutorialShowcase: - """Creates beautiful showcase screenshots for tutorials.""" - - def __init__(self, scene_name, output_name): - self.scene = mcrfpy.Scene(scene_name) - self.output_path = os.path.join(OUTPUT_DIR, output_name) - self.grid = None - - def setup_grid(self, width, height, zoom=3.0): - """Create a grid with nice defaults.""" - texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - self.grid = mcrfpy.Grid( - pos=(50, 80), - size=(700, 500), - grid_size=(width, height), - texture=texture, - zoom=zoom - ) - self.grid.fill_color = mcrfpy.Color(20, 20, 30) - self.scene.children.append(self.grid) - self.texture = texture - return self.grid - - def add_title(self, text, subtitle=None): - """Add a title to the scene.""" - title = mcrfpy.Caption(text=text, pos=(50, 20)) - title.fill_color = mcrfpy.Color(255, 255, 255) - title.font_size = 28 - self.scene.children.append(title) - - if subtitle: - sub = mcrfpy.Caption(text=subtitle, pos=(50, 50)) - sub.fill_color = mcrfpy.Color(180, 180, 200) - sub.font_size = 16 - self.scene.children.append(sub) - - def fill_floor(self, tile=None): - """Fill grid with floor tiles.""" - if tile is None: - tile = TILES['floor_stone'] - w, h = int(self.grid.grid_size[0]), int(self.grid.grid_size[1]) - for y in range(h): - for x in range(w): - self.grid.at(x, y).tilesprite = tile - - def add_walls(self, tile=None): - """Add wall border.""" - if tile is None: - tile = TILES['wall_stone'] - w, h = int(self.grid.grid_size[0]), int(self.grid.grid_size[1]) - for x in range(w): - self.grid.at(x, 0).tilesprite = tile - self.grid.at(x, 0).walkable = False - self.grid.at(x, h-1).tilesprite = tile - self.grid.at(x, h-1).walkable = False - for y in range(h): - self.grid.at(0, y).tilesprite = tile - self.grid.at(0, y).walkable = False - self.grid.at(w-1, y).tilesprite = tile - self.grid.at(w-1, y).walkable = False - - def add_entity(self, x, y, sprite): - """Add an entity to the grid.""" - entity = mcrfpy.Entity( - grid_pos=(x, y), - texture=self.texture, - sprite_index=sprite - ) - self.grid.entities.append(entity) - return entity - - def center_on(self, x, y): - """Center camera on a position.""" - self.grid.center = (x * 16 + 8, y * 16 + 8) - - def screenshot(self): - """Take the screenshot - synchronous in headless mode!""" - self.scene.activate() - result = automation.screenshot(self.output_path) - print(f" -> {self.output_path} (result: {result})") - return result - - -def part01_grid_movement(): - """Part 1: Grid Movement - Knight in a dungeon room.""" - showcase = TutorialShowcase("part01", "part_01_grid_movement.png") - showcase.setup_grid(12, 9, zoom=3.5) - showcase.add_title("Part 1: The '@' and the Dungeon Grid", - "Creating a grid, placing entities, handling input") - - showcase.fill_floor(TILES['floor_stone']) - showcase.add_walls(TILES['wall_stone']) - - # Add the player (a cool knight, not boring @) - showcase.add_entity(6, 4, TILES['player_knight']) - - # Add some decorations to make it interesting - showcase.add_entity(2, 2, TILES['torch']) - showcase.add_entity(9, 2, TILES['torch']) - showcase.add_entity(2, 6, TILES['barrel']) - showcase.add_entity(9, 6, TILES['skull']) - - showcase.center_on(6, 4) - showcase.screenshot() - - -def part02_tiles_collision(): - """Part 2: Tiles and Collision - Walls and walkability.""" - showcase = TutorialShowcase("part02", "part_02_tiles_collision.png") - showcase.setup_grid(14, 10, zoom=3.0) - showcase.add_title("Part 2: Tiles, Collision, and Walkability", - "Different tile types and blocking movement") - - showcase.fill_floor(TILES['floor_stone']) - showcase.add_walls(TILES['wall_brick']) - - # Create some interior walls to show collision - for y in range(2, 5): - showcase.grid.at(5, y).tilesprite = TILES['wall_stone'] - showcase.grid.at(5, y).walkable = False - for y in range(5, 8): - showcase.grid.at(9, y).tilesprite = TILES['wall_stone'] - showcase.grid.at(9, y).walkable = False - - # Add a door - showcase.grid.at(5, 5).tilesprite = TILES['door_closed'] - showcase.grid.at(5, 5).walkable = False - - # Player navigating the maze - showcase.add_entity(3, 4, TILES['player_warrior']) - - # Chest as goal - showcase.add_entity(11, 5, TILES['chest_closed']) - - showcase.center_on(7, 5) - showcase.screenshot() - - -def part03_dungeon_generation(): - """Part 3: Dungeon Generation - Procedural rooms and corridors.""" - showcase = TutorialShowcase("part03", "part_03_dungeon_generation.png") - showcase.setup_grid(20, 14, zoom=2.5) - showcase.add_title("Part 3: Procedural Dungeon Generation", - "Random rooms connected by corridors") - - # Fill with walls first - for y in range(14): - for x in range(20): - showcase.grid.at(x, y).tilesprite = TILES['wall_stone'] - showcase.grid.at(x, y).walkable = False - - # Carve out two rooms - # Room 1 (left) - for y in range(3, 8): - for x in range(2, 8): - showcase.grid.at(x, y).tilesprite = TILES['floor_stone'] - showcase.grid.at(x, y).walkable = True - - # Room 2 (right) - for y in range(6, 12): - for x in range(12, 18): - showcase.grid.at(x, y).tilesprite = TILES['floor_stone'] - showcase.grid.at(x, y).walkable = True - - # Corridor connecting them - for x in range(7, 13): - showcase.grid.at(x, 6).tilesprite = TILES['floor_dirt'] - showcase.grid.at(x, 6).walkable = True - for y in range(6, 9): - showcase.grid.at(12, y).tilesprite = TILES['floor_dirt'] - showcase.grid.at(12, y).walkable = True - - # Player in first room - showcase.add_entity(4, 5, TILES['player_knight']) - - # Some loot in second room - showcase.add_entity(14, 9, TILES['chest_closed']) - showcase.add_entity(16, 8, TILES['item_potion']) - - # Torches - showcase.add_entity(3, 3, TILES['torch']) - showcase.add_entity(6, 3, TILES['torch']) - showcase.add_entity(13, 7, TILES['torch']) - - showcase.center_on(10, 7) - showcase.screenshot() - - -def part04_fov(): - """Part 4: Field of View - Showing explored vs visible areas.""" - showcase = TutorialShowcase("part04", "part_04_fov.png") - showcase.setup_grid(16, 12, zoom=2.8) - showcase.add_title("Part 4: Field of View and Fog of War", - "What the player can see vs. the unknown") - - showcase.fill_floor(TILES['floor_stone']) - showcase.add_walls(TILES['wall_brick']) - - # Some interior pillars to block sight - for pos in [(5, 4), (5, 7), (10, 5), (10, 8)]: - showcase.grid.at(pos[0], pos[1]).tilesprite = TILES['wall_mossy'] - showcase.grid.at(pos[0], pos[1]).walkable = False - - # Player with "light" - showcase.add_entity(8, 6, TILES['player_mage']) - - # Hidden enemy (player wouldn't see this!) - showcase.add_entity(12, 3, TILES['enemy_ghost']) - - # Visible enemies - showcase.add_entity(3, 5, TILES['enemy_bat']) - showcase.add_entity(6, 8, TILES['enemy_spider']) - - showcase.center_on(8, 6) - showcase.screenshot() - - -def part05_enemies(): - """Part 5: Enemies - A dungeon full of monsters.""" - showcase = TutorialShowcase("part05", "part_05_enemies.png") - showcase.setup_grid(18, 12, zoom=2.5) - showcase.add_title("Part 5: Adding Enemies", - "Different monster types with AI behavior") - - showcase.fill_floor(TILES['floor_stone']) - showcase.add_walls(TILES['wall_stone']) - - # The hero - showcase.add_entity(3, 5, TILES['player_warrior']) - - # A variety of enemies - showcase.add_entity(7, 3, TILES['enemy_slime']) - showcase.add_entity(10, 6, TILES['enemy_bat']) - showcase.add_entity(8, 8, TILES['enemy_spider']) - showcase.add_entity(14, 4, TILES['enemy_orc']) - showcase.add_entity(15, 8, TILES['enemy_skeleton']) - showcase.add_entity(12, 5, TILES['enemy_rat']) - - # Boss at the end - showcase.add_entity(15, 6, TILES['enemy_boss']) - - # Some decorations - showcase.add_entity(5, 2, TILES['bones']) - showcase.add_entity(13, 9, TILES['skull']) - showcase.add_entity(2, 8, TILES['torch']) - showcase.add_entity(16, 2, TILES['torch']) - - showcase.center_on(9, 5) - showcase.screenshot() - - -def part06_combat(): - """Part 6: Combat - Battle in progress!""" - showcase = TutorialShowcase("part06", "part_06_combat.png") - showcase.setup_grid(14, 10, zoom=3.0) - showcase.add_title("Part 6: Combat System", - "HP, attack, defense, and turn-based fighting") - - showcase.fill_floor(TILES['floor_dirt']) - showcase.add_walls(TILES['wall_brick']) - - # Battle scene - player vs enemy - showcase.add_entity(5, 5, TILES['player_knight']) - showcase.add_entity(8, 5, TILES['enemy_orc']) - - # Fallen enemies (show combat has happened) - showcase.add_entity(4, 3, TILES['bones']) - showcase.add_entity(9, 7, TILES['skull']) - - # Equipment the player has - showcase.add_entity(3, 6, TILES['equip_shield']) - showcase.add_entity(10, 4, TILES['item_potion']) - - showcase.center_on(6, 5) - showcase.screenshot() - - -def part07_ui(): - """Part 7: User Interface - Health bars and menus.""" - showcase = TutorialShowcase("part07", "part_07_ui.png") - showcase.setup_grid(12, 8, zoom=3.0) - showcase.add_title("Part 7: User Interface", - "Health bars, message logs, and menus") - - showcase.fill_floor(TILES['floor_wood']) - showcase.add_walls(TILES['wall_brick']) - - # Player - showcase.add_entity(6, 4, TILES['player_rogue']) - - # Some items to interact with - showcase.add_entity(4, 3, TILES['chest_open']) - showcase.add_entity(8, 5, TILES['item_coin']) - - # Add UI overlay example - health bar frame - ui_frame = mcrfpy.Frame(pos=(50, 520), size=(200, 40)) - ui_frame.fill_color = mcrfpy.Color(40, 40, 50, 200) - ui_frame.outline = 2 - ui_frame.outline_color = mcrfpy.Color(80, 80, 100) - showcase.scene.children.append(ui_frame) - - # Health label - hp_label = mcrfpy.Caption(text="HP: 45/50", pos=(10, 10)) - hp_label.fill_color = mcrfpy.Color(255, 100, 100) - hp_label.font_size = 18 - ui_frame.children.append(hp_label) - - # Health bar background - hp_bg = mcrfpy.Frame(pos=(90, 12), size=(100, 16)) - hp_bg.fill_color = mcrfpy.Color(60, 20, 20) - ui_frame.children.append(hp_bg) - - # Health bar fill - hp_fill = mcrfpy.Frame(pos=(90, 12), size=(90, 16)) # 90% health - hp_fill.fill_color = mcrfpy.Color(200, 50, 50) - ui_frame.children.append(hp_fill) - - showcase.center_on(6, 4) - showcase.screenshot() - - -def main(): - """Generate all showcase screenshots!""" - os.makedirs(OUTPUT_DIR, exist_ok=True) - - print("=== Tutorial Screenshot Showcase ===") - print(f"Output: {OUTPUT_DIR}\n") - - showcases = [ - ('Part 1: Grid Movement', part01_grid_movement), - ('Part 2: Tiles & Collision', part02_tiles_collision), - ('Part 3: Dungeon Generation', part03_dungeon_generation), - ('Part 4: Field of View', part04_fov), - ('Part 5: Enemies', part05_enemies), - ('Part 6: Combat', part06_combat), - ('Part 7: UI', part07_ui), - ] - - for name, func in showcases: - print(f"Generating {name}...") - try: - func() - except Exception as e: - print(f" ERROR: {e}") - - print("\n=== All screenshots generated! ===") - sys.exit(0) - - -main() diff --git a/tests/docs/API_FINDINGS.md b/tests/docs/API_FINDINGS.md deleted file mode 100644 index b4cce5f..0000000 --- a/tests/docs/API_FINDINGS.md +++ /dev/null @@ -1,129 +0,0 @@ -# McRogueFace API Test Findings -*Generated by Frack, January 14, 2026* - -## Summary - -Tested code snippets from docs site against actual runtime API. -Tests created in `/tests/docs/` with screenshots in `/tests/docs/screenshots/`. - ---- - -## Entity Constructor - -**Correct:** -```python -entity = mcrfpy.Entity(grid_pos=(10, 7), texture=texture, sprite_index=84) -``` - -**Wrong:** -```python -entity = mcrfpy.Entity(pos=(10, 7), ...) # FAILS - no 'pos' kwarg -entity = mcrfpy.Entity(grid_x=10, grid_y=7, ...) # FAILS - no separate kwargs -``` - ---- - -## Scene API - -**Modern (WORKS):** -```python -scene = mcrfpy.Scene("name") -scene.children.append(frame) -scene.on_key = handler -scene.activate() -``` - -**Deprecated → Modern:** -```python -# OLD → NEW -mcrfpy.createScene("name") → scene = mcrfpy.Scene("name") -mcrfpy.sceneUI("name") → scene.children -mcrfpy.setScene("name") → scene.activate() -mcrfpy.keypressScene(fn) → scene.on_key = fn -mcrfpy.currentScene() → mcrfpy.current_scene # property, not function! -``` - ---- - -## Grid.at() Signature - -Both work: -```python -point = grid.at((x, y)) # Tuple - documented -point = grid.at(x, y) # Separate args - also works! -``` - ---- - -## Animation System - -**Works:** -- `x`, `y`, `w`, `h` properties animate correctly -- Callbacks fire as expected -- Multiple simultaneous animations work -- Easing functions work - -**Does NOT work:** -- `opacity` - property exists, can set directly, but animation ignores it - ---- - -## Timer API - -**Modern:** -```python -timer = mcrfpy.Timer("name", callback, seconds) -# Callback signature: def callback(timer, runtime): -``` - -**Deprecated:** -```python -mcrfpy.setTimer("name", callback, milliseconds) # Wrong signature too -``` - ---- - -## Color API - -Always use `mcrfpy.Color()`: -```python -frame.fill_color = mcrfpy.Color(255, 0, 0) # CORRECT -frame.fill_color = mcrfpy.Color(255, 0, 0, 128) # With alpha -``` - -Tuples no longer work: -```python -frame.fill_color = (255, 0, 0) # FAILS -``` - ---- - -## Available Globals - -**All exist:** -- `mcrfpy.default_texture` - Texture for kenney_tinydungeon.png -- `mcrfpy.default_font` - Font for JetBrains Mono -- `mcrfpy.default_fov` - (default FOV settings) - ---- - -## Files Requiring Updates - -1. `quickstart.md` - All 4 code blocks use deprecated API -2. `features/scenes.md` - "Procedural API" section entirely deprecated -3. `features/animation.md` - opacity examples don't work -4. Any file using `setTimer`, `createScene`, `sceneUI`, `setScene`, `keypressScene` - ---- - -## Test Files Created - -| Test | Status | Notes | -|------|--------|-------| -| test_quickstart_simple_scene.py | PASS | | -| test_quickstart_main_menu.py | PASS | | -| test_quickstart_entities.py | PASS | Uses grid_pos= | -| test_quickstart_sprites.py | PASS | | -| test_features_animation.py | PASS | opacity test skipped | -| test_features_scenes.py | PASS | Documents deprecated API | -| test_entity_api.py | INFO | Verifies grid_pos= works | diff --git a/tests/docs/screenshots/features_animation.png b/tests/docs/screenshots/features_animation.png deleted file mode 100644 index 4280a2c..0000000 Binary files a/tests/docs/screenshots/features_animation.png and /dev/null differ diff --git a/tests/docs/screenshots/features_scenes.png b/tests/docs/screenshots/features_scenes.png deleted file mode 100644 index 1c8ee92..0000000 Binary files a/tests/docs/screenshots/features_scenes.png and /dev/null differ diff --git a/tests/docs/screenshots/quickstart_entities.png b/tests/docs/screenshots/quickstart_entities.png deleted file mode 100644 index da85c34..0000000 Binary files a/tests/docs/screenshots/quickstart_entities.png and /dev/null differ diff --git a/tests/docs/screenshots/quickstart_main_menu.png b/tests/docs/screenshots/quickstart_main_menu.png deleted file mode 100644 index 6bb58d6..0000000 Binary files a/tests/docs/screenshots/quickstart_main_menu.png and /dev/null differ diff --git a/tests/docs/screenshots/quickstart_simple_scene.png b/tests/docs/screenshots/quickstart_simple_scene.png deleted file mode 100644 index e90b37d..0000000 Binary files a/tests/docs/screenshots/quickstart_simple_scene.png and /dev/null differ diff --git a/tests/docs/screenshots/quickstart_sprites.png b/tests/docs/screenshots/quickstart_sprites.png deleted file mode 100644 index a5d420e..0000000 Binary files a/tests/docs/screenshots/quickstart_sprites.png and /dev/null differ diff --git a/tests/docs/test_current_scene.py b/tests/docs/test_current_scene.py deleted file mode 100644 index 80eb18f..0000000 --- a/tests/docs/test_current_scene.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -"""Verify mcrfpy.current_scene property.""" -import mcrfpy -import sys - -scene = mcrfpy.Scene("test") -scene.activate() -mcrfpy.step(0.1) - -try: - current = mcrfpy.current_scene - print(f"mcrfpy.current_scene = {current}") - print(f"type: {type(current)}") - print("VERIFIED: mcrfpy.current_scene WORKS") -except AttributeError as e: - print(f"FAILED: {e}") - -sys.exit(0) diff --git a/tests/docs/test_defaults.py b/tests/docs/test_defaults.py deleted file mode 100644 index 6105cfc..0000000 --- a/tests/docs/test_defaults.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -"""Test mcrfpy default resources.""" -import mcrfpy -import sys - -scene = mcrfpy.Scene("test") -scene.activate() -mcrfpy.step(0.01) - -print("Checking mcrfpy defaults:") - -try: - dt = mcrfpy.default_texture - print(f" default_texture = {dt}") -except AttributeError as e: - print(f" default_texture: NOT FOUND") - -try: - df = mcrfpy.default_font - print(f" default_font = {df}") -except AttributeError as e: - print(f" default_font: NOT FOUND") - -# Also check what other module-level attributes exist -print("\nAll mcrfpy attributes starting with 'default':") -for attr in dir(mcrfpy): - if 'default' in attr.lower(): - print(f" {attr}") - -sys.exit(0) diff --git a/tests/docs/test_entity_api.py b/tests/docs/test_entity_api.py deleted file mode 100644 index 6cc8601..0000000 --- a/tests/docs/test_entity_api.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -"""Quick test to verify Entity constructor signature.""" -import mcrfpy -import sys - -scene = mcrfpy.Scene("test") -scene.activate() -mcrfpy.step(0.01) - -texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) -grid = mcrfpy.Grid(grid_size=(20, 15), texture=texture, pos=(10, 10), size=(640, 480)) -scene.children.append(grid) - -# Test grid_pos vs grid_x/grid_y -try: - e1 = mcrfpy.Entity(grid_pos=(5, 5), texture=texture, sprite_index=85) - grid.entities.append(e1) - print("grid_pos= WORKS") -except TypeError as e: - print(f"grid_pos= FAILS: {e}") - -try: - e2 = mcrfpy.Entity(grid_x=7, grid_y=7, texture=texture, sprite_index=85) - grid.entities.append(e2) - print("grid_x=/grid_y= WORKS") -except TypeError as e: - print(f"grid_x=/grid_y= FAILS: {e}") - -print("Entity API test complete") -sys.exit(0) diff --git a/tests/docs/test_features_animation.py b/tests/docs/test_features_animation.py deleted file mode 100644 index 2a40238..0000000 --- a/tests/docs/test_features_animation.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python3 -"""Test for features/animation.md examples. - -Tests the modern API equivalents of animation examples. -""" -import mcrfpy -from mcrfpy import automation -import sys - -# Setup scene using modern API -scene = mcrfpy.Scene("animation_demo") -scene.activate() -mcrfpy.step(0.01) - -# Test 1: Basic Animation (lines 9-19) -frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) -frame.fill_color = mcrfpy.Color(255, 0, 0) -scene.children.append(frame) - -# Animate x position -anim = mcrfpy.Animation("x", 500.0, 2.0, "easeInOut") -anim.start(frame) -print("Test 1: Basic animation started") - -# Step forward to run animation -mcrfpy.step(2.5) - -# Verify animation completed -if abs(frame.x - 500.0) < 1.0: - print("Test 1: PASS - frame moved to x=500") -else: - print(f"Test 1: FAIL - frame at x={frame.x}, expected 500") - sys.exit(1) - -# Test 2: Multiple simultaneous animations (lines 134-144) -frame2 = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) -frame2.fill_color = mcrfpy.Color(0, 255, 0) -scene.children.append(frame2) - -mcrfpy.Animation("x", 200.0, 1.0, "easeInOut").start(frame2) -mcrfpy.Animation("y", 150.0, 1.0, "easeInOut").start(frame2) -mcrfpy.Animation("w", 300.0, 1.0, "easeInOut").start(frame2) -mcrfpy.Animation("h", 200.0, 1.0, "easeInOut").start(frame2) - -mcrfpy.step(1.5) - -if abs(frame2.x - 200.0) < 1.0 and abs(frame2.y - 150.0) < 1.0: - print("Test 2: PASS - multiple animations completed") -else: - print(f"Test 2: FAIL - frame2 at ({frame2.x}, {frame2.y})") - sys.exit(1) - -# Test 3: Callback (lines 105-112) -callback_fired = False - -def on_complete(animation, target): - global callback_fired - callback_fired = True - print("Test 3: Callback fired!") - -frame3 = mcrfpy.Frame(pos=(0, 300), size=(50, 50)) -frame3.fill_color = mcrfpy.Color(0, 0, 255) -scene.children.append(frame3) - -anim3 = mcrfpy.Animation("x", 300.0, 0.5, "easeInOut", callback=on_complete) -anim3.start(frame3) - -mcrfpy.step(1.0) - -if callback_fired: - print("Test 3: PASS - callback executed") -else: - print("Test 3: FAIL - callback not executed") - sys.exit(1) - -# Test 4: NOTE - Opacity animation documented in features/animation.md -# but DOES NOT WORK on Frame. The property exists but animation -# system doesn't animate it. This is a DOCS BUG to report. -# Skipping test 4 - opacity animation not supported. -print("Test 4: SKIPPED - opacity animation not supported on Frame (docs bug)") - -# Take screenshot showing animation results -automation.screenshot("/opt/goblincorps/repos/McRogueFace/tests/docs/screenshots/features_animation.png") - -print("\nAll animation tests PASS") -sys.exit(0) diff --git a/tests/docs/test_features_scenes.py b/tests/docs/test_features_scenes.py deleted file mode 100644 index 89f6a6d..0000000 --- a/tests/docs/test_features_scenes.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python3 -"""Test for features/scenes.md examples. - -Tests both modern and procedural APIs from the docs. -""" -import mcrfpy -from mcrfpy import automation -import sys - -# Test 1: Modern Scene API (lines 28-42) -print("Test 1: Modern Scene API") -scene = mcrfpy.Scene("test_modern") -scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(800, 600))) - -def my_handler(key, action): - if action == "start": - print(f" Key handler received: {key}") - -scene.on_key = my_handler -scene.activate() -mcrfpy.step(0.1) -print(" PASS - modern Scene API works") - -# Test 2: Check Scene properties -print("Test 2: Scene properties") -print(f" scene.name = {scene.name}") -print(f" scene.active = {scene.active}") -print(f" len(scene.children) = {len(scene.children)}") - -# Test 3: Check if default_texture exists -print("Test 3: default_texture") -try: - dt = mcrfpy.default_texture - print(f" mcrfpy.default_texture = {dt}") -except AttributeError: - print(" default_texture NOT FOUND - docs bug!") - -# Test 4: Check if currentScene exists -print("Test 4: currentScene()") -try: - current = mcrfpy.currentScene() - print(f" mcrfpy.currentScene() = {current}") -except AttributeError: - print(" currentScene() NOT FOUND - docs bug!") - -# Test 5: Check Grid.at() signature -print("Test 5: Grid.at() signature") -texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) -grid = mcrfpy.Grid(grid_size=(10, 10), texture=texture, pos=(0, 0), size=(200, 200)) - -# Try both signatures -try: - point_tuple = grid.at((5, 5)) - print(" grid.at((x, y)) - tuple WORKS") -except Exception as e: - print(f" grid.at((x, y)) FAILS: {e}") - -try: - point_sep = grid.at(5, 5) - print(" grid.at(x, y) - separate args WORKS") -except Exception as e: - print(f" grid.at(x, y) FAILS: {e}") - -# Test 6: Scene transitions (setScene) -print("Test 6: setScene()") -scene2 = mcrfpy.Scene("test_transitions") -scene2.activate() -mcrfpy.step(0.1) - -# Check if setScene exists -try: - mcrfpy.setScene("test_modern") - print(" mcrfpy.setScene() WORKS") -except AttributeError: - print(" mcrfpy.setScene() NOT FOUND - use scene.activate() instead") -except Exception as e: - print(f" mcrfpy.setScene() error: {e}") - -# Take screenshot -mcrfpy.step(0.1) -automation.screenshot("/opt/goblincorps/repos/McRogueFace/tests/docs/screenshots/features_scenes.png") - -print("\nAll scene tests complete") -sys.exit(0) diff --git a/tests/docs/test_quickstart_entities.py b/tests/docs/test_quickstart_entities.py deleted file mode 100644 index dfc668f..0000000 --- a/tests/docs/test_quickstart_entities.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -"""Test for quickstart.md 'Game Entity' example. - -Original (DEPRECATED - lines 133-168): - mcrfpy.createScene("game") - grid = mcrfpy.Grid(20, 15, texture, (10, 10), (640, 480)) - ui = mcrfpy.sceneUI("game") - player = mcrfpy.Entity((10, 7), texture, 85) - mcrfpy.keypressScene(handle_keys) - mcrfpy.setScene("game") - -Modern equivalent below. -""" -import mcrfpy -from mcrfpy import automation -import sys - -# Create scene using modern API -scene = mcrfpy.Scene("game") -scene.activate() -mcrfpy.step(0.01) - -# Load texture -texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - -# Create grid using modern keyword API -grid = mcrfpy.Grid( - grid_size=(20, 15), - texture=texture, - pos=(10, 10), - size=(640, 480) -) -scene.children.append(grid) - -# Add player entity -player = mcrfpy.Entity(grid_pos=(10, 7), texture=texture, sprite_index=85) -grid.entities.append(player) - -# Add NPC entity -npc = mcrfpy.Entity(grid_pos=(5, 5), texture=texture, sprite_index=109) -grid.entities.append(npc) - -# Add treasure chest -treasure = mcrfpy.Entity(grid_pos=(15, 10), texture=texture, sprite_index=89) -grid.entities.append(treasure) - -# Movement handler using modern API -def handle_keys(key, state): - if state == "start": - x, y = player.pos[0], player.pos[1] - if key == "W": - player.pos = (x, y - 1) - elif key == "S": - player.pos = (x, y + 1) - elif key == "A": - player.pos = (x - 1, y) - elif key == "D": - player.pos = (x + 1, y) - -scene.on_key = handle_keys - -# Center grid on player -grid.center = (10, 7) - -# Render and screenshot -mcrfpy.step(0.1) -automation.screenshot("/opt/goblincorps/repos/McRogueFace/tests/docs/screenshots/quickstart_entities.png") - -print("PASS - quickstart entities") -sys.exit(0) diff --git a/tests/docs/test_quickstart_main_menu.py b/tests/docs/test_quickstart_main_menu.py deleted file mode 100644 index 30a9a0d..0000000 --- a/tests/docs/test_quickstart_main_menu.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python3 -"""Test for quickstart.md 'Main Menu' example. - -Original (DEPRECATED - lines 82-125): - mcrfpy.createScene("main_menu") - ui = mcrfpy.sceneUI("main_menu") - bg = mcrfpy.Frame(0, 0, 1024, 768, fill_color=(20, 20, 40)) - title = mcrfpy.Caption((312, 100), "My Awesome Game", font, fill_color=(255, 255, 100)) - button_frame.click = start_game - mcrfpy.setScene("main_menu") - -Modern equivalent below. -""" -import mcrfpy -from mcrfpy import automation -import sys - -# Create scene using modern API -scene = mcrfpy.Scene("main_menu") -scene.activate() -mcrfpy.step(0.01) - -# Load font -font = mcrfpy.Font("assets/JetbrainsMono.ttf") - -# Add background using modern Frame API (keyword args) -bg = mcrfpy.Frame( - pos=(0, 0), - size=(1024, 768), - fill_color=mcrfpy.Color(20, 20, 40) -) -scene.children.append(bg) - -# Add title using modern Caption API -title = mcrfpy.Caption( - pos=(312, 100), - text="My Awesome Game", - font=font, - fill_color=mcrfpy.Color(255, 255, 100) -) -title.font_size = 48 -title.outline = 2 -title.outline_color = mcrfpy.Color(0, 0, 0) -scene.children.append(title) - -# Create button frame -button_frame = mcrfpy.Frame( - pos=(362, 300), - size=(300, 80), - fill_color=mcrfpy.Color(50, 150, 50) -) - -# Button caption -button_caption = mcrfpy.Caption( - pos=(90, 25), # Centered in button - text="Start Game", - fill_color=mcrfpy.Color(255, 255, 255) -) -button_caption.font_size = 24 -button_frame.children.append(button_caption) - -# Click handler using modern on_click (3 args: x, y, button) -def start_game(x, y, button): - print("Starting the game!") - -button_frame.on_click = start_game -scene.children.append(button_frame) - -# Render and screenshot -mcrfpy.step(0.1) -automation.screenshot("/opt/goblincorps/repos/McRogueFace/tests/docs/screenshots/quickstart_main_menu.png") - -print("PASS - quickstart main menu") -sys.exit(0) diff --git a/tests/docs/test_quickstart_simple_scene.py b/tests/docs/test_quickstart_simple_scene.py deleted file mode 100644 index fdcb4f3..0000000 --- a/tests/docs/test_quickstart_simple_scene.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -"""Test for quickstart.md 'Simple Test Scene' example. - -Original (DEPRECATED - lines 48-74): - mcrfpy.createScene("test") - grid = mcrfpy.Grid(20, 15, texture, (10, 10), (800, 600)) - ui = mcrfpy.sceneUI("test") - mcrfpy.setScene("test") - mcrfpy.keypressScene(move_around) - -Modern equivalent below. -""" -import mcrfpy -from mcrfpy import automation -import sys - -# Create scene using modern API -scene = mcrfpy.Scene("test") -scene.activate() -mcrfpy.step(0.01) # Initialize - -# Load texture -texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - -# Create grid using modern keyword API -grid = mcrfpy.Grid( - grid_size=(20, 15), - texture=texture, - pos=(10, 10), - size=(800, 600) -) - -# Add to scene's children (not sceneUI) -scene.children.append(grid) - -# Add keyboard controls using modern API -def move_around(key, state): - if state == "start": - print(f"You pressed {key}") - -scene.on_key = move_around - -# Render and screenshot -mcrfpy.step(0.1) -automation.screenshot("/opt/goblincorps/repos/McRogueFace/tests/docs/screenshots/quickstart_simple_scene.png") - -print("PASS - quickstart simple scene") -sys.exit(0) diff --git a/tests/docs/test_quickstart_sprites.py b/tests/docs/test_quickstart_sprites.py deleted file mode 100644 index 2975a48..0000000 --- a/tests/docs/test_quickstart_sprites.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 -"""Test for quickstart.md 'Custom Sprite Sheet' example. - -Original (DEPRECATED - lines 176-201): - mcrfpy.createScene("game") - grid = mcrfpy.Grid(20, 15, my_texture, (10, 10), (640, 480)) - grid.at(5, 5).sprite = 10 - ui = mcrfpy.sceneUI("game") - mcrfpy.setScene("game") - -Modern equivalent below. -""" -import mcrfpy -from mcrfpy import automation -import sys - -# Create scene using modern API -scene = mcrfpy.Scene("game") -scene.activate() -mcrfpy.step(0.01) - -# Load sprite sheet (using existing texture for test) -texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) - -# Create grid using modern keyword API -grid = mcrfpy.Grid( - grid_size=(20, 15), - texture=texture, - pos=(10, 10), - size=(640, 480) -) - -# Set specific tiles using modern API -# grid.at() returns a GridPoint with tilesprite property -grid.at((5, 5)).tilesprite = 10 # Note: tuple for position -grid.at((6, 5)).tilesprite = 11 - -# Set walkability -grid.at((6, 5)).walkable = False - -# Add grid to scene -scene.children.append(grid) - -# Render and screenshot -mcrfpy.step(0.1) -automation.screenshot("/opt/goblincorps/repos/McRogueFace/tests/docs/screenshots/quickstart_sprites.png") - -print("PASS - quickstart sprites") -sys.exit(0) diff --git a/tests/pytest.ini b/tests/pytest.ini deleted file mode 100644 index 75a0668..0000000 --- a/tests/pytest.ini +++ /dev/null @@ -1,14 +0,0 @@ -[pytest] -# McRogueFace test scripts run via subprocess, not as Python modules -# They contain `import mcrfpy` which is only available inside McRogueFace - -# Don't try to import test scripts from these directories -norecursedirs = unit integration regression benchmarks demo geometry_demo notes vllm_demo - -# Run test_*.py files in tests/ root that are pytest-native wrappers -python_files = test_*.py -python_classes = Test* -python_functions = test_* - -# Custom option for timeout -addopts = -v diff --git a/tests/run_tests.py b/tests/run_tests.py index e9e6035..f51f3a9 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -7,8 +7,6 @@ Usage: python3 tests/run_tests.py # Run all tests python3 tests/run_tests.py unit # Run only unit tests python3 tests/run_tests.py -v # Verbose output - python3 tests/run_tests.py -q # Quiet (no checksums) - python3 tests/run_tests.py --timeout=30 # Custom timeout """ import os import subprocess @@ -20,9 +18,8 @@ from pathlib import Path # Configuration TESTS_DIR = Path(__file__).parent BUILD_DIR = TESTS_DIR.parent / "build" -LIB_DIR = TESTS_DIR.parent / "__lib" MCROGUEFACE = BUILD_DIR / "mcrogueface" -DEFAULT_TIMEOUT = 10 # seconds per test +TIMEOUT = 10 # seconds per test # Test directories to run (in order) TEST_DIRS = ['unit', 'integration', 'regression'] @@ -42,7 +39,7 @@ def get_screenshot_checksum(test_dir): checksums[png.name] = hashlib.md5(f.read()).hexdigest()[:8] return checksums -def run_test(test_path, verbose=False, timeout=DEFAULT_TIMEOUT): +def run_test(test_path, verbose=False): """Run a single test and return (passed, duration, output).""" start = time.time() @@ -50,19 +47,13 @@ def run_test(test_path, verbose=False, timeout=DEFAULT_TIMEOUT): for png in BUILD_DIR.glob("test_*.png"): png.unlink() - # Set up environment with library path - env = os.environ.copy() - existing_ld = env.get('LD_LIBRARY_PATH', '') - env['LD_LIBRARY_PATH'] = f"{LIB_DIR}:{existing_ld}" if existing_ld else str(LIB_DIR) - try: result = subprocess.run( [str(MCROGUEFACE), '--headless', '--exec', str(test_path)], capture_output=True, text=True, - timeout=timeout, - cwd=str(BUILD_DIR), - env=env + timeout=TIMEOUT, + cwd=str(BUILD_DIR) ) duration = time.time() - start passed = result.returncode == 0 @@ -75,7 +66,7 @@ def run_test(test_path, verbose=False, timeout=DEFAULT_TIMEOUT): return passed, duration, output except subprocess.TimeoutExpired: - return False, timeout, "TIMEOUT" + return False, TIMEOUT, "TIMEOUT" except Exception as e: return False, 0, str(e) @@ -88,16 +79,6 @@ def find_tests(directory): def main(): verbose = '-v' in sys.argv or '--verbose' in sys.argv - quiet = '-q' in sys.argv or '--quiet' in sys.argv - - # Parse --timeout=N - timeout = DEFAULT_TIMEOUT - for arg in sys.argv[1:]: - if arg.startswith('--timeout='): - try: - timeout = int(arg.split('=')[1]) - except ValueError: - pass # Determine which directories to test dirs_to_test = [] @@ -108,7 +89,7 @@ def main(): dirs_to_test = TEST_DIRS print(f"{BOLD}McRogueFace Test Runner{RESET}") - print(f"Testing: {', '.join(dirs_to_test)} (timeout: {timeout}s)") + print(f"Testing: {', '.join(dirs_to_test)}") print("=" * 60) results = {'pass': 0, 'fail': 0, 'total_time': 0} @@ -123,7 +104,7 @@ def main(): for test_path in tests: test_name = test_path.name - passed, duration, output = run_test(test_path, verbose, timeout) + passed, duration, output = run_test(test_path, verbose) results['total_time'] += duration if passed: @@ -134,12 +115,11 @@ def main(): status = f"{RED}FAIL{RESET}" failures.append((test_dir, test_name, output)) - # Get screenshot checksums if any were generated (skip in quiet mode) + # Get screenshot checksums if any were generated + checksums = get_screenshot_checksum(BUILD_DIR) checksum_str = "" - if not quiet: - checksums = get_screenshot_checksum(BUILD_DIR) - if checksums: - checksum_str = f" [{', '.join(f'{k}:{v}' for k,v in checksums.items())}]" + if checksums: + checksum_str = f" [{', '.join(f'{k}:{v}' for k,v in checksums.items())}]" print(f" {status} {test_name} ({duration:.2f}s){checksum_str}") diff --git a/tests/test_mcrogueface.py b/tests/test_mcrogueface.py deleted file mode 100644 index c165b5b..0000000 --- a/tests/test_mcrogueface.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -Pytest wrapper for McRogueFace test scripts. - -This file discovers and runs all McRogueFace test scripts in unit/, integration/, -and regression/ directories via subprocess. - -Usage: - pytest tests/test_mcrogueface.py -q # Quiet output - pytest tests/test_mcrogueface.py -k "bsp" # Filter by name - pytest tests/test_mcrogueface.py --mcrf-timeout=30 # Custom timeout - pytest tests/test_mcrogueface.py -x # Stop on first failure -""" -import os -import subprocess -import pytest -from pathlib import Path - -# Paths -TESTS_DIR = Path(__file__).parent -BUILD_DIR = TESTS_DIR.parent / "build" -LIB_DIR = TESTS_DIR.parent / "__lib" -MCROGUEFACE = BUILD_DIR / "mcrogueface" - -# Test directories -TEST_DIRS = ['unit', 'integration', 'regression'] - -# Default timeout -DEFAULT_TIMEOUT = 10 - - -def discover_tests(): - """Find all test scripts in test directories.""" - tests = [] - for test_dir in TEST_DIRS: - dir_path = TESTS_DIR / test_dir - if dir_path.exists(): - for test_file in sorted(dir_path.glob("*.py")): - if test_file.name != '__init__.py': - rel_path = f"{test_dir}/{test_file.name}" - tests.append(rel_path) - return tests - - -def get_env(): - """Get environment with LD_LIBRARY_PATH set.""" - env = os.environ.copy() - existing_ld = env.get('LD_LIBRARY_PATH', '') - env['LD_LIBRARY_PATH'] = f"{LIB_DIR}:{existing_ld}" if existing_ld else str(LIB_DIR) - return env - - -def run_mcrf_test(script_path, timeout=DEFAULT_TIMEOUT): - """Run a McRogueFace test script and return (passed, output).""" - full_path = TESTS_DIR / script_path - env = get_env() - - try: - result = subprocess.run( - [str(MCROGUEFACE), '--headless', '--exec', str(full_path)], - capture_output=True, - text=True, - timeout=timeout, - cwd=str(BUILD_DIR), - env=env - ) - - output = result.stdout + result.stderr - passed = result.returncode == 0 - - # Check for PASS/FAIL in output - if 'FAIL' in output and 'PASS' not in output.split('FAIL')[-1]: - passed = False - - return passed, output - - except subprocess.TimeoutExpired: - return False, "TIMEOUT" - except Exception as e: - return False, str(e) - - -# Discover tests at module load time -ALL_TESTS = discover_tests() - - -@pytest.fixture -def mcrf_timeout(request): - """Get timeout from command line or default.""" - return request.config.getoption("--mcrf-timeout", default=DEFAULT_TIMEOUT) - - -def pytest_addoption(parser): - """Add --mcrf-timeout option.""" - try: - parser.addoption( - "--mcrf-timeout", - action="store", - default=DEFAULT_TIMEOUT, - type=int, - help="Timeout in seconds for McRogueFace tests" - ) - except ValueError: - # Option already added - pass - - -@pytest.mark.parametrize("script_path", ALL_TESTS, ids=lambda x: x.replace('/', '::')) -def test_mcrogueface_script(script_path, request): - """Run a McRogueFace test script.""" - timeout = request.config.getoption("--mcrf-timeout", default=DEFAULT_TIMEOUT) - passed, output = run_mcrf_test(script_path, timeout=timeout) - - if not passed: - # Show last 15 lines of output on failure - lines = output.strip().split('\n')[-15:] - pytest.fail('\n'.join(lines)) diff --git a/tests/unit/WORKING_automation_test_example.py b/tests/unit/WORKING_automation_test_example.py index e7307d2..bfa8b82 100644 --- a/tests/unit/WORKING_automation_test_example.py +++ b/tests/unit/WORKING_automation_test_example.py @@ -1,11 +1,47 @@ #!/usr/bin/env python3 -"""Example of CORRECT test pattern using mcrfpy.step() for automation -Refactored from timer-based approach to synchronous step() pattern. -""" +"""Example of CORRECT test pattern using timer callbacks for automation""" import mcrfpy from mcrfpy import automation from datetime import datetime -import sys + +def run_automation_tests(timer, runtime): + """This runs AFTER the game loop has started and rendered frames""" + print("\n=== Automation Test Running (1 second after start) ===") + + # NOW we can take screenshots that will show content! + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"WORKING_screenshot_{timestamp}.png" + + # Take screenshot - this should now show our red frame + result = automation.screenshot(filename) + print(f"Screenshot taken: {filename} - Result: {result}") + + # Test clicking on the frame + automation.click(200, 200) # Click in center of red frame + + # Test keyboard input + automation.typewrite("Hello from timer callback!") + + # Take another screenshot to show any changes + filename2 = f"WORKING_screenshot_after_click_{timestamp}.png" + automation.screenshot(filename2) + print(f"Second screenshot: {filename2}") + + print("Test completed successfully!") + print("\nThis works because:") + print("1. The game loop has been running for 1 second") + print("2. The scene has been rendered multiple times") + print("3. The RenderTexture now contains actual rendered content") + + # Cancel this timer so it doesn't repeat + timer.stop() + + # Optional: exit after a moment + def exit_game(t, r): + print("Exiting...") + mcrfpy.exit() + global exit_timer + exit_timer = mcrfpy.Timer("exit", exit_game, 500, once=True) # This code runs during --exec script execution print("=== Setting Up Test Scene ===") @@ -13,8 +49,6 @@ print("=== Setting Up Test Scene ===") # Create scene with visible content timer_test_scene = mcrfpy.Scene("timer_test_scene") timer_test_scene.activate() -mcrfpy.step(0.01) # Initialize scene - ui = timer_test_scene.children # Add a bright red frame that should be visible @@ -26,57 +60,23 @@ ui.append(frame) # Add text caption = mcrfpy.Caption(pos=(150, 150), - text="STEP TEST - SHOULD BE VISIBLE", + text="TIMER TEST - SHOULD BE VISIBLE", fill_color=mcrfpy.Color(255, 255, 255)) caption.font_size = 24 frame.children.append(caption) # Add click handler to demonstrate interaction -click_received = False def frame_clicked(x, y, button): - global click_received - click_received = True print(f"Frame clicked at ({x}, {y}) with button {button}") frame.on_click = frame_clicked -print("Scene setup complete.") +print("Scene setup complete. Setting timer for automation tests...") -# Step to render the scene -mcrfpy.step(0.1) +# THIS IS THE KEY: Set timer to run AFTER the game loop starts +automation_test_timer = mcrfpy.Timer("automation_test", run_automation_tests, 1000, once=True) -print("\n=== Automation Test Running ===") +print("Timer set. Game loop will start after this script completes.") +print("Automation tests will run 1 second later when content is visible.") -# NOW we can take screenshots that will show content! -timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") -filename = f"WORKING_screenshot_{timestamp}.png" - -# Take screenshot - this should now show our red frame -result = automation.screenshot(filename) -print(f"Screenshot taken: {filename} - Result: {result}") - -# Test clicking on the frame -automation.click(200, 200) # Click in center of red frame - -# Step to process the click -mcrfpy.step(0.1) - -# Test keyboard input -automation.typewrite("Hello from step-based test!") - -# Step to process keyboard input -mcrfpy.step(0.1) - -# Take another screenshot to show any changes -filename2 = f"WORKING_screenshot_after_click_{timestamp}.png" -automation.screenshot(filename2) -print(f"Second screenshot: {filename2}") - -print("Test completed successfully!") -print("\nThis works because:") -print("1. mcrfpy.step() advances simulation synchronously") -print("2. The scene renders during step() calls") -print("3. The RenderTexture contains actual rendered content") - -print("PASS") -sys.exit(0) +# Script ends here - game loop starts next \ No newline at end of file diff --git a/tests/unit/alignment_test.py b/tests/unit/alignment_test.py index 8a3d2d6..35e0f00 100644 --- a/tests/unit/alignment_test.py +++ b/tests/unit/alignment_test.py @@ -46,12 +46,10 @@ print("Test 3: Checking margin properties...") try: frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) - # Check default margins: - # - margin returns 0 when both horiz/vert are unset (effective default) - # - horiz_margin/vert_margin return -1 (sentinel for "not set") - assert frame.margin == 0.0, f"Expected margin=0 (effective default), got {frame.margin}" - assert frame.horiz_margin == -1.0, f"Expected horiz_margin=-1 (unset), got {frame.horiz_margin}" - assert frame.vert_margin == -1.0, f"Expected vert_margin=-1 (unset), got {frame.vert_margin}" + # Check default margins are 0 + assert frame.margin == 0, f"Expected margin=0, got {frame.margin}" + assert frame.horiz_margin == 0, f"Expected horiz_margin=0, got {frame.horiz_margin}" + assert frame.vert_margin == 0, f"Expected vert_margin=0, got {frame.vert_margin}" # Set margins when no alignment frame.margin = 10.0 diff --git a/tests/unit/simple_timer_screenshot_test.py b/tests/unit/simple_timer_screenshot_test.py index b12fda3..b4f0d63 100644 --- a/tests/unit/simple_timer_screenshot_test.py +++ b/tests/unit/simple_timer_screenshot_test.py @@ -1,23 +1,28 @@ #!/usr/bin/env python3 -"""Test to verify timer-based screenshots work using mcrfpy.step() for synchronous execution""" +"""Simplified test to verify timer-based screenshots work""" import mcrfpy from mcrfpy import automation -import sys # Counter to track timer calls call_count = 0 -def take_screenshot(timer, runtime): - """Timer callback that takes screenshot""" +def take_screenshot_and_exit(timer, runtime): + """Timer callback that takes screenshot then exits""" global call_count call_count += 1 - print(f"Timer callback fired! (call #{call_count}, runtime={runtime})") + + print(f"\nTimer callback fired! (call #{call_count})") # Take screenshot filename = f"timer_screenshot_test_{call_count}.png" result = automation.screenshot(filename) print(f"Screenshot result: {result} -> {filename}") + # Exit after first call + if call_count >= 1: + print("Exiting game...") + mcrfpy.exit() + # Set up a simple scene print("Creating test scene...") test = mcrfpy.Scene("test") @@ -30,17 +35,6 @@ frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200), ui.append(frame) print("Setting timer to fire in 100ms...") -timer = mcrfpy.Timer("screenshot_timer", take_screenshot, 100, once=True) -print(f"Timer created: {timer}") +mcrfpy.Timer("screenshot_timer", take_screenshot_and_exit, 100, once=True) -# Use mcrfpy.step() to advance simulation synchronously instead of waiting -print("Advancing simulation by 200ms using step()...") -mcrfpy.step(0.2) # Advance 200ms - timer at 100ms should fire - -# Verify timer fired -if call_count >= 1: - print("SUCCESS: Timer fired and screenshot taken!") - sys.exit(0) -else: - print(f"FAIL: Expected call_count >= 1, got {call_count}") - sys.exit(1) +print("Setup complete. Game loop starting...") diff --git a/tests/unit/test_animation_callback_simple.py b/tests/unit/test_animation_callback_simple.py index f75d33d..48b5163 100644 --- a/tests/unit/test_animation_callback_simple.py +++ b/tests/unit/test_animation_callback_simple.py @@ -1,55 +1,73 @@ #!/usr/bin/env python3 -"""Simple test for animation callbacks using mcrfpy.step() for synchronous execution""" +"""Simple test for animation callbacks - demonstrates basic usage""" import mcrfpy import sys -print("Animation Callback Demo") -print("=" * 30) - # Global state to track callback callback_count = 0 +callback_demo = None # Will be set in setup_and_run def my_callback(anim, target): """Simple callback that prints when animation completes""" global callback_count callback_count += 1 print(f"Animation completed! Callback #{callback_count}") + # For now, anim and target are None - future enhancement -# Create scene -callback_demo = mcrfpy.Scene("callback_demo") -callback_demo.activate() +def setup_and_run(): + """Set up scene and run animation with callback""" + global callback_demo + # Create scene + callback_demo = mcrfpy.Scene("callback_demo") + callback_demo.activate() -# Create a frame to animate -frame = mcrfpy.Frame((100, 100), (200, 200), fill_color=(255, 0, 0)) -ui = callback_demo.children -ui.append(frame) + # Create a frame to animate + frame = mcrfpy.Frame((100, 100), (200, 200), fill_color=(255, 0, 0)) + ui = callback_demo.children + ui.append(frame) -# Test 1: Animation with callback -print("Starting animation with callback (1.0s duration)...") -anim = mcrfpy.Animation("x", 400.0, 1.0, "easeInOutQuad", callback=my_callback) -anim.start(frame) + # Create animation with callback + print("Starting animation with callback...") + anim = mcrfpy.Animation("x", 400.0, 1.0, "easeInOutQuad", callback=my_callback) + anim.start(frame) -# Use mcrfpy.step() to advance past animation completion -mcrfpy.step(1.5) # Advance 1.5 seconds - animation completes at 1.0s + # Schedule check after animation should complete + mcrfpy.Timer("check", check_result, 1500, once=True) -if callback_count != 1: - print(f"FAIL: Expected 1 callback, got {callback_count}") - sys.exit(1) -print("SUCCESS: Callback fired exactly once!") +def check_result(timer, runtime): + """Check if callback fired correctly""" + global callback_count, callback_demo -# Test 2: Animation without callback -print("\nTesting animation without callback (0.5s duration)...") -anim2 = mcrfpy.Animation("y", 300.0, 0.5, "linear") -anim2.start(frame) + if callback_count == 1: + print("SUCCESS: Callback fired exactly once!") -# Advance past second animation -mcrfpy.step(0.7) + # Test 2: Animation without callback + print("\nTesting animation without callback...") + ui = callback_demo.children + frame = ui[0] -if callback_count != 1: - print(f"FAIL: Callback count changed to {callback_count}") - sys.exit(1) + anim2 = mcrfpy.Animation("y", 300.0, 0.5, "linear") + anim2.start(frame) -print("SUCCESS: No unexpected callbacks fired!") -print("\nAnimation callback feature working correctly!") -sys.exit(0) + mcrfpy.Timer("final", final_check, 700, once=True) + else: + print(f"FAIL: Expected 1 callback, got {callback_count}") + sys.exit(1) + +def final_check(timer, runtime): + """Final check - callback count should still be 1""" + global callback_count + + if callback_count == 1: + print("SUCCESS: No unexpected callbacks fired!") + print("\nAnimation callback feature working correctly!") + sys.exit(0) + else: + print(f"FAIL: Callback count changed to {callback_count}") + sys.exit(1) + +# Start the demo +print("Animation Callback Demo") +print("=" * 30) +setup_and_run() diff --git a/tests/unit/test_animation_property_locking.py b/tests/unit/test_animation_property_locking.py index ea9e000..165fde7 100644 --- a/tests/unit/test_animation_property_locking.py +++ b/tests/unit/test_animation_property_locking.py @@ -210,7 +210,7 @@ def test_8_replace_completes_old(): test_result("Replace completes old animation", False, str(e)) -def run_all_tests(): +def run_all_tests(timer, runtime): """Run all property locking tests""" print("\nRunning Animation Property Locking Tests...") print("-" * 50) @@ -245,8 +245,5 @@ def run_all_tests(): test = mcrfpy.Scene("test") test.activate() -# Use mcrfpy.step() to advance simulation for scene initialization -mcrfpy.step(0.1) # Brief step to initialize scene - -# Run tests directly (no timer needed with step-based approach) -run_all_tests() +# Start tests after a brief delay to allow scene to initialize +mcrfpy.Timer("start", run_all_tests, 100, once=True) diff --git a/tests/unit/test_animation_raii.py b/tests/unit/test_animation_raii.py index 03eb37f..438e323 100644 --- a/tests/unit/test_animation_raii.py +++ b/tests/unit/test_animation_raii.py @@ -2,7 +2,6 @@ """ Test the RAII AnimationManager implementation. This verifies that weak_ptr properly handles all crash scenarios. -Uses mcrfpy.step() for synchronous test execution. """ import mcrfpy @@ -20,14 +19,189 @@ def test_result(name, passed, details=""): global tests_passed, tests_failed if passed: tests_passed += 1 - result = f"PASS: {name}" + result = f"✓ {name}" else: tests_failed += 1 - result = f"FAIL: {name}: {details}" + result = f"✗ {name}: {details}" print(result) test_results.append((name, passed, details)) -# Setup scene +def test_1_basic_animation(): + """Test that basic animations still work""" + try: + ui = test.children + frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100)) + ui.append(frame) + + anim = mcrfpy.Animation("x", 200.0, 1000, "linear") + anim.start(frame) + + # Check if animation has valid target + if hasattr(anim, 'hasValidTarget'): + valid = anim.hasValidTarget() + test_result("Basic animation with hasValidTarget", valid) + else: + test_result("Basic animation", True) + except Exception as e: + test_result("Basic animation", False, str(e)) + +def test_2_remove_animated_object(): + """Test removing object with active animation""" + try: + ui = test.children + frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100)) + ui.append(frame) + + # Start animation + anim = mcrfpy.Animation("x", 500.0, 2000, "easeInOut") + anim.start(frame) + + # Remove the frame + ui.remove(0) + + # Check if animation knows target is gone + if hasattr(anim, 'hasValidTarget'): + valid = anim.hasValidTarget() + test_result("Animation detects removed target", not valid) + else: + # If method doesn't exist, just check we didn't crash + test_result("Remove animated object", True) + except Exception as e: + test_result("Remove animated object", False, str(e)) + +def test_3_complete_animation(): + """Test completing animation immediately""" + try: + ui = test.children + frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100)) + ui.append(frame) + + # Start animation + anim = mcrfpy.Animation("x", 500.0, 2000, "linear") + anim.start(frame) + + # Complete it + if hasattr(anim, 'complete'): + anim.complete() + # Frame should now be at x=500 + test_result("Animation complete method", True) + else: + test_result("Animation complete method", True, "Method not available") + except Exception as e: + test_result("Animation complete method", False, str(e)) + +def test_4_multiple_animations_timer(): + """Test creating multiple animations in timer callback""" + success = False + + def create_animations(timer, runtime): + nonlocal success + try: + ui = test.children + frame = mcrfpy.Frame(pos=(200, 200), size=(100, 100)) + ui.append(frame) + + # Create multiple animations rapidly (this used to crash) + for i in range(10): + anim = mcrfpy.Animation("x", 300.0 + i * 10, 1000, "linear") + anim.start(frame) + + success = True + except Exception as e: + print(f"Timer animation error: {e}") + finally: + mcrfpy.Timer("exit", lambda t, r: None, 100, once=True) + + # Clear scene + ui = test.children + while len(ui) > 0: + ui.remove(len(ui) - 1) + + mcrfpy.Timer("test", create_animations, 50, once=True) + mcrfpy.Timer("check", lambda t, r: test_result("Multiple animations in timer", success), 200, once=True) + +def test_5_scene_cleanup(): + """Test that changing scenes cleans up animations""" + try: + # Create a second scene + test2 = mcrfpy.Scene("test2") + + # Add animated objects to first scene + ui = test.children + for i in range(5): + frame = mcrfpy.Frame(pos=(50 * i, 100), size=(40, 40)) + ui.append(frame) + anim = mcrfpy.Animation("y", 300.0, 2000, "easeOutBounce") + anim.start(frame) + + # Switch scenes (animations should become invalid) + test2.activate() + + # Switch back + test.activate() + + test_result("Scene change cleanup", True) + except Exception as e: + test_result("Scene change cleanup", False, str(e)) + +def test_6_animation_after_clear(): + """Test animations after clearing UI""" + try: + ui = test.children + + # Create and animate + frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100)) + ui.append(frame) + anim = mcrfpy.Animation("w", 200.0, 1500, "easeInOutCubic") + anim.start(frame) + + # Clear all UI + while len(ui) > 0: + ui.remove(len(ui) - 1) + + # Animation should handle this gracefully + if hasattr(anim, 'hasValidTarget'): + valid = anim.hasValidTarget() + test_result("Animation after UI clear", not valid) + else: + test_result("Animation after UI clear", True) + except Exception as e: + test_result("Animation after UI clear", False, str(e)) + +def run_all_tests(timer, runtime): + """Run all RAII tests""" + print("\nRunning RAII Animation Tests...") + print("-" * 40) + + test_1_basic_animation() + test_2_remove_animated_object() + test_3_complete_animation() + test_4_multiple_animations_timer() + test_5_scene_cleanup() + test_6_animation_after_clear() + + # Schedule result summary + mcrfpy.Timer("results", print_results, 500, once=True) + +def print_results(timer, runtime): + """Print test results""" + print("\n" + "=" * 40) + print(f"Tests passed: {tests_passed}") + print(f"Tests failed: {tests_failed}") + + if tests_failed == 0: + print("\n+ All tests passed! RAII implementation is working correctly.") + else: + print(f"\nx {tests_failed} tests failed.") + print("\nFailed tests:") + for name, passed, details in test_results: + if not passed: + print(f" - {name}: {details}") + + # Exit + mcrfpy.Timer("exit", lambda t, r: sys.exit(0 if tests_failed == 0 else 1), 500, once=True) + +# Setup and run test = mcrfpy.Scene("test") test.activate() @@ -37,125 +211,5 @@ bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768)) bg.fill_color = mcrfpy.Color(20, 20, 30) ui.append(bg) -# Initialize scene -mcrfpy.step(0.1) - -print("\nRunning RAII Animation Tests...") -print("-" * 40) - -# Test 1: Basic animation -try: - frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100)) - ui.append(frame) - - anim = mcrfpy.Animation("x", 200.0, 1000, "linear") - anim.start(frame) - - if hasattr(anim, 'hasValidTarget'): - valid = anim.hasValidTarget() - test_result("Basic animation with hasValidTarget", valid) - else: - test_result("Basic animation", True) -except Exception as e: - test_result("Basic animation", False, str(e)) - -# Test 2: Remove animated object -try: - frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100)) - ui.append(frame) - - anim = mcrfpy.Animation("x", 500.0, 2000, "easeInOut") - anim.start(frame) - - ui.remove(frame) - - if hasattr(anim, 'hasValidTarget'): - valid = anim.hasValidTarget() - test_result("Animation detects removed target", not valid) - else: - test_result("Remove animated object", True) -except Exception as e: - test_result("Remove animated object", False, str(e)) - -# Test 3: Complete animation immediately -try: - frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100)) - ui.append(frame) - - anim = mcrfpy.Animation("x", 500.0, 2000, "linear") - anim.start(frame) - - if hasattr(anim, 'complete'): - anim.complete() - test_result("Animation complete method", True) - else: - test_result("Animation complete method", True, "Method not available") -except Exception as e: - test_result("Animation complete method", False, str(e)) - -# Test 4: Multiple animations rapidly -try: - frame = mcrfpy.Frame(pos=(200, 200), size=(100, 100)) - ui.append(frame) - - for i in range(10): - anim = mcrfpy.Animation("x", 300.0 + i * 10, 1000, "linear") - anim.start(frame) - - test_result("Multiple animations rapidly", True) -except Exception as e: - test_result("Multiple animations rapidly", False, str(e)) - -# Test 5: Scene cleanup -try: - test2 = mcrfpy.Scene("test2") - - for i in range(5): - frame = mcrfpy.Frame(pos=(50 * i, 100), size=(40, 40)) - ui.append(frame) - anim = mcrfpy.Animation("y", 300.0, 2000, "easeOutBounce") - anim.start(frame) - - test2.activate() - mcrfpy.step(0.1) - test.activate() - mcrfpy.step(0.1) - - test_result("Scene change cleanup", True) -except Exception as e: - test_result("Scene change cleanup", False, str(e)) - -# Test 6: Animation after clearing UI -try: - frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100)) - ui.append(frame) - anim = mcrfpy.Animation("w", 200.0, 1500, "easeInOutCubic") - anim.start(frame) - - # Clear all UI except background - iterate in reverse - for i in range(len(ui) - 1, 0, -1): - ui.remove(ui[i]) - - if hasattr(anim, 'hasValidTarget'): - valid = anim.hasValidTarget() - test_result("Animation after UI clear", not valid) - else: - test_result("Animation after UI clear", True) -except Exception as e: - test_result("Animation after UI clear", False, str(e)) - -# Print results -print("\n" + "=" * 40) -print(f"Tests passed: {tests_passed}") -print(f"Tests failed: {tests_failed}") - -if tests_failed == 0: - print("\nAll tests passed! RAII implementation is working correctly.") -else: - print(f"\n{tests_failed} tests failed.") - print("\nFailed tests:") - for name, passed, details in test_results: - if not passed: - print(f" - {name}: {details}") - -sys.exit(0 if tests_failed == 0 else 1) +# Start tests +start_timer = mcrfpy.Timer("start", run_all_tests, 100, once=True) \ No newline at end of file diff --git a/tests/unit/test_animation_removal.py b/tests/unit/test_animation_removal.py index 126dbdb..f5baaea 100644 --- a/tests/unit/test_animation_removal.py +++ b/tests/unit/test_animation_removal.py @@ -1,14 +1,40 @@ #!/usr/bin/env python3 """ -Test if the crash is related to removing animated objects. -Uses mcrfpy.step() for synchronous test execution. +Test if the crash is related to removing animated objects """ import mcrfpy import sys -print("Animation Removal Test") -print("=" * 40) +def clear_and_recreate(timer, runtime): + """Clear UI and recreate - mimics demo switching""" + print(f"\nTimer called at {runtime}") + + ui = test.children + + # Remove all but first 2 items (like clear_demo_objects) + print(f"Scene has {len(ui)} elements before clearing") + while len(ui) > 2: + ui.remove(len(ui)-1) + print(f"Scene has {len(ui)} elements after clearing") + + # Create new animated objects + print("Creating new animated objects...") + for i in range(5): + f = mcrfpy.Frame(100 + i*50, 200, 40, 40) + f.fill_color = mcrfpy.Color(100 + i*30, 50, 200) + ui.append(f) + + # Start animation on the new frame + target_x = 300 + i * 50 + anim = mcrfpy.Animation("x", float(target_x), 1.0, "easeInOut") + anim.start(f) + + print("New objects created and animated") + + # Schedule exit + global exit_timer + exit_timer = mcrfpy.Timer("exit", lambda t, r: sys.exit(0), 2000, once=True) # Create initial scene print("Creating scene...") @@ -21,61 +47,20 @@ title = mcrfpy.Caption(pos=(400, 20), text="Test Title") subtitle = mcrfpy.Caption(pos=(400, 50), text="Test Subtitle") ui.extend([title, subtitle]) -# Initialize scene -mcrfpy.step(0.1) - # Create initial animated objects print("Creating initial animated objects...") -initial_frames = [] for i in range(10): f = mcrfpy.Frame(pos=(50 + i*30, 100), size=(25, 25)) f.fill_color = mcrfpy.Color(255, 100, 100) ui.append(f) - initial_frames.append(f) - + # Animate them anim = mcrfpy.Animation("y", 300.0, 2.0, "easeOutBounce") anim.start(f) print(f"Initial scene has {len(ui)} elements") -# Let animations run a bit -mcrfpy.step(0.5) +# Schedule the clear and recreate +switch_timer = mcrfpy.Timer("switch", clear_and_recreate, 1000, once=True) -# Clear and recreate - mimics demo switching -print("\nClearing and recreating...") -print(f"Scene has {len(ui)} elements before clearing") - -# Remove all but first 2 items (like clear_demo_objects) -# Use reverse iteration to remove by element -while len(ui) > 2: - ui.remove(ui[-1]) - -print(f"Scene has {len(ui)} elements after clearing") - -# Create new animated objects -print("Creating new animated objects...") -for i in range(5): - f = mcrfpy.Frame(pos=(100 + i*50, 200), size=(40, 40)) - f.fill_color = mcrfpy.Color(100 + i*30, 50, 200) - ui.append(f) - - # Start animation on the new frame - target_x = 300 + i * 50 - anim = mcrfpy.Animation("x", float(target_x), 1.0, "easeInOut") - anim.start(f) - -print("New objects created and animated") -print(f"Scene now has {len(ui)} elements") - -# Let new animations run -mcrfpy.step(1.5) - -# Final check -print(f"\nFinal scene has {len(ui)} elements") -if len(ui) == 7: # 2 captions + 5 new frames - print("SUCCESS: Animation removal test passed!") - sys.exit(0) -else: - print(f"FAIL: Expected 7 elements, got {len(ui)}") - sys.exit(1) +print("\nEntering game loop...") \ No newline at end of file diff --git a/tests/unit/test_color_helpers.py b/tests/unit/test_color_helpers.py index 7cc1512..795ee31 100644 --- a/tests/unit/test_color_helpers.py +++ b/tests/unit/test_color_helpers.py @@ -1,181 +1,182 @@ #!/usr/bin/env python3 """ Test #94: Color helper methods - from_hex, to_hex, lerp -Refactored to use mcrfpy.step() for synchronous execution. """ import mcrfpy import sys -# Initialize scene +def test_color_helpers(timer, runtime): + """Test Color helper methods""" + + all_pass = True + + # Test 1: from_hex with # prefix + try: + c1 = mcrfpy.Color.from_hex("#FF0000") + assert c1.r == 255 and c1.g == 0 and c1.b == 0 and c1.a == 255, f"from_hex('#FF0000') failed: {c1}" + print("+ Color.from_hex('#FF0000') works") + except Exception as e: + print(f"x Color.from_hex('#FF0000') failed: {e}") + all_pass = False + + # Test 2: from_hex without # prefix + try: + c2 = mcrfpy.Color.from_hex("00FF00") + assert c2.r == 0 and c2.g == 255 and c2.b == 0 and c2.a == 255, f"from_hex('00FF00') failed: {c2}" + print("+ Color.from_hex('00FF00') works") + except Exception as e: + print(f"x Color.from_hex('00FF00') failed: {e}") + all_pass = False + + # Test 3: from_hex with alpha + try: + c3 = mcrfpy.Color.from_hex("#0000FF80") + assert c3.r == 0 and c3.g == 0 and c3.b == 255 and c3.a == 128, f"from_hex('#0000FF80') failed: {c3}" + print("+ Color.from_hex('#0000FF80') with alpha works") + except Exception as e: + print(f"x Color.from_hex('#0000FF80') failed: {e}") + all_pass = False + + # Test 4: from_hex error handling + try: + c4 = mcrfpy.Color.from_hex("GGGGGG") + print("x from_hex should fail on invalid hex") + all_pass = False + except ValueError as e: + print("+ Color.from_hex() correctly rejects invalid hex") + + # Test 5: from_hex wrong length + try: + c5 = mcrfpy.Color.from_hex("FF00") + print("x from_hex should fail on wrong length") + all_pass = False + except ValueError as e: + print("+ Color.from_hex() correctly rejects wrong length") + + # Test 6: to_hex without alpha + try: + c6 = mcrfpy.Color(255, 128, 64) + hex_str = c6.to_hex() + assert hex_str == "#FF8040", f"to_hex() failed: {hex_str}" + print("+ Color.to_hex() works") + except Exception as e: + print(f"x Color.to_hex() failed: {e}") + all_pass = False + + # Test 7: to_hex with alpha + try: + c7 = mcrfpy.Color(255, 128, 64, 127) + hex_str = c7.to_hex() + assert hex_str == "#FF80407F", f"to_hex() with alpha failed: {hex_str}" + print("+ Color.to_hex() with alpha works") + except Exception as e: + print(f"x Color.to_hex() with alpha failed: {e}") + all_pass = False + + # Test 8: Round-trip hex conversion + try: + original_hex = "#ABCDEF" + c8 = mcrfpy.Color.from_hex(original_hex) + result_hex = c8.to_hex() + assert result_hex == original_hex, f"Round-trip failed: {original_hex} -> {result_hex}" + print("+ Hex round-trip conversion works") + except Exception as e: + print(f"x Hex round-trip failed: {e}") + all_pass = False + + # Test 9: lerp at t=0 + try: + red = mcrfpy.Color(255, 0, 0) + blue = mcrfpy.Color(0, 0, 255) + result = red.lerp(blue, 0.0) + assert result.r == 255 and result.g == 0 and result.b == 0, f"lerp(t=0) failed: {result}" + print("+ Color.lerp(t=0) returns start color") + except Exception as e: + print(f"x Color.lerp(t=0) failed: {e}") + all_pass = False + + # Test 10: lerp at t=1 + try: + red = mcrfpy.Color(255, 0, 0) + blue = mcrfpy.Color(0, 0, 255) + result = red.lerp(blue, 1.0) + assert result.r == 0 and result.g == 0 and result.b == 255, f"lerp(t=1) failed: {result}" + print("+ Color.lerp(t=1) returns end color") + except Exception as e: + print(f"x Color.lerp(t=1) failed: {e}") + all_pass = False + + # Test 11: lerp at t=0.5 + try: + red = mcrfpy.Color(255, 0, 0) + blue = mcrfpy.Color(0, 0, 255) + result = red.lerp(blue, 0.5) + # Expect roughly (127, 0, 127) + assert 126 <= result.r <= 128 and result.g == 0 and 126 <= result.b <= 128, f"lerp(t=0.5) failed: {result}" + print("+ Color.lerp(t=0.5) returns midpoint") + except Exception as e: + print(f"x Color.lerp(t=0.5) failed: {e}") + all_pass = False + + # Test 12: lerp with alpha + try: + c1 = mcrfpy.Color(255, 0, 0, 255) + c2 = mcrfpy.Color(0, 255, 0, 0) + result = c1.lerp(c2, 0.5) + assert 126 <= result.r <= 128 and 126 <= result.g <= 128 and result.b == 0, f"lerp color components failed" + assert 126 <= result.a <= 128, f"lerp alpha failed: {result.a}" + print("+ Color.lerp() with alpha works") + except Exception as e: + print(f"x Color.lerp() with alpha failed: {e}") + all_pass = False + + # Test 13: lerp clamps t < 0 + try: + red = mcrfpy.Color(255, 0, 0) + blue = mcrfpy.Color(0, 0, 255) + result = red.lerp(blue, -0.5) + assert result.r == 255 and result.g == 0 and result.b == 0, f"lerp(t<0) should clamp to 0" + print("+ Color.lerp() clamps t < 0") + except Exception as e: + print(f"x Color.lerp(t<0) failed: {e}") + all_pass = False + + # Test 14: lerp clamps t > 1 + try: + red = mcrfpy.Color(255, 0, 0) + blue = mcrfpy.Color(0, 0, 255) + result = red.lerp(blue, 1.5) + assert result.r == 0 and result.g == 0 and result.b == 255, f"lerp(t>1) should clamp to 1" + print("+ Color.lerp() clamps t > 1") + except Exception as e: + print(f"x Color.lerp(t>1) failed: {e}") + all_pass = False + + # Test 15: Practical use case - gradient + try: + start = mcrfpy.Color.from_hex("#FF0000") # Red + end = mcrfpy.Color.from_hex("#0000FF") # Blue + + # Create 5-step gradient + steps = [] + for i in range(5): + t = i / 4.0 + color = start.lerp(end, t) + steps.append(color.to_hex()) + + assert steps[0] == "#FF0000", "Gradient start should be red" + assert steps[4] == "#0000FF", "Gradient end should be blue" + assert len(set(steps)) == 5, "All gradient steps should be unique" + + print("+ Gradient generation works correctly") + except Exception as e: + print(f"x Gradient generation failed: {e}") + all_pass = False + + print(f"\n{'PASS' if all_pass else 'FAIL'}") + sys.exit(0 if all_pass else 1) + +# Run test test = mcrfpy.Scene("test") -test.activate() -mcrfpy.step(0.01) - -all_pass = True - -# Test 1: from_hex with # prefix -try: - c1 = mcrfpy.Color.from_hex("#FF0000") - assert c1.r == 255 and c1.g == 0 and c1.b == 0 and c1.a == 255, f"from_hex('#FF0000') failed: {c1}" - print("+ Color.from_hex('#FF0000') works") -except Exception as e: - print(f"x Color.from_hex('#FF0000') failed: {e}") - all_pass = False - -# Test 2: from_hex without # prefix -try: - c2 = mcrfpy.Color.from_hex("00FF00") - assert c2.r == 0 and c2.g == 255 and c2.b == 0 and c2.a == 255, f"from_hex('00FF00') failed: {c2}" - print("+ Color.from_hex('00FF00') works") -except Exception as e: - print(f"x Color.from_hex('00FF00') failed: {e}") - all_pass = False - -# Test 3: from_hex with alpha -try: - c3 = mcrfpy.Color.from_hex("#0000FF80") - assert c3.r == 0 and c3.g == 0 and c3.b == 255 and c3.a == 128, f"from_hex('#0000FF80') failed: {c3}" - print("+ Color.from_hex('#0000FF80') with alpha works") -except Exception as e: - print(f"x Color.from_hex('#0000FF80') failed: {e}") - all_pass = False - -# Test 4: from_hex error handling -try: - c4 = mcrfpy.Color.from_hex("GGGGGG") - print("x from_hex should fail on invalid hex") - all_pass = False -except ValueError as e: - print("+ Color.from_hex() correctly rejects invalid hex") - -# Test 5: from_hex wrong length -try: - c5 = mcrfpy.Color.from_hex("FF00") - print("x from_hex should fail on wrong length") - all_pass = False -except ValueError as e: - print("+ Color.from_hex() correctly rejects wrong length") - -# Test 6: to_hex without alpha -try: - c6 = mcrfpy.Color(255, 128, 64) - hex_str = c6.to_hex() - assert hex_str == "#FF8040", f"to_hex() failed: {hex_str}" - print("+ Color.to_hex() works") -except Exception as e: - print(f"x Color.to_hex() failed: {e}") - all_pass = False - -# Test 7: to_hex with alpha -try: - c7 = mcrfpy.Color(255, 128, 64, 127) - hex_str = c7.to_hex() - assert hex_str == "#FF80407F", f"to_hex() with alpha failed: {hex_str}" - print("+ Color.to_hex() with alpha works") -except Exception as e: - print(f"x Color.to_hex() with alpha failed: {e}") - all_pass = False - -# Test 8: Round-trip hex conversion -try: - original_hex = "#ABCDEF" - c8 = mcrfpy.Color.from_hex(original_hex) - result_hex = c8.to_hex() - assert result_hex == original_hex, f"Round-trip failed: {original_hex} -> {result_hex}" - print("+ Hex round-trip conversion works") -except Exception as e: - print(f"x Hex round-trip failed: {e}") - all_pass = False - -# Test 9: lerp at t=0 -try: - red = mcrfpy.Color(255, 0, 0) - blue = mcrfpy.Color(0, 0, 255) - result = red.lerp(blue, 0.0) - assert result.r == 255 and result.g == 0 and result.b == 0, f"lerp(t=0) failed: {result}" - print("+ Color.lerp(t=0) returns start color") -except Exception as e: - print(f"x Color.lerp(t=0) failed: {e}") - all_pass = False - -# Test 10: lerp at t=1 -try: - red = mcrfpy.Color(255, 0, 0) - blue = mcrfpy.Color(0, 0, 255) - result = red.lerp(blue, 1.0) - assert result.r == 0 and result.g == 0 and result.b == 255, f"lerp(t=1) failed: {result}" - print("+ Color.lerp(t=1) returns end color") -except Exception as e: - print(f"x Color.lerp(t=1) failed: {e}") - all_pass = False - -# Test 11: lerp at t=0.5 -try: - red = mcrfpy.Color(255, 0, 0) - blue = mcrfpy.Color(0, 0, 255) - result = red.lerp(blue, 0.5) - # Expect roughly (127, 0, 127) - assert 126 <= result.r <= 128 and result.g == 0 and 126 <= result.b <= 128, f"lerp(t=0.5) failed: {result}" - print("+ Color.lerp(t=0.5) returns midpoint") -except Exception as e: - print(f"x Color.lerp(t=0.5) failed: {e}") - all_pass = False - -# Test 12: lerp with alpha -try: - c1 = mcrfpy.Color(255, 0, 0, 255) - c2 = mcrfpy.Color(0, 255, 0, 0) - result = c1.lerp(c2, 0.5) - assert 126 <= result.r <= 128 and 126 <= result.g <= 128 and result.b == 0, f"lerp color components failed" - assert 126 <= result.a <= 128, f"lerp alpha failed: {result.a}" - print("+ Color.lerp() with alpha works") -except Exception as e: - print(f"x Color.lerp() with alpha failed: {e}") - all_pass = False - -# Test 13: lerp clamps t < 0 -try: - red = mcrfpy.Color(255, 0, 0) - blue = mcrfpy.Color(0, 0, 255) - result = red.lerp(blue, -0.5) - assert result.r == 255 and result.g == 0 and result.b == 0, f"lerp(t<0) should clamp to 0" - print("+ Color.lerp() clamps t < 0") -except Exception as e: - print(f"x Color.lerp(t<0) failed: {e}") - all_pass = False - -# Test 14: lerp clamps t > 1 -try: - red = mcrfpy.Color(255, 0, 0) - blue = mcrfpy.Color(0, 0, 255) - result = red.lerp(blue, 1.5) - assert result.r == 0 and result.g == 0 and result.b == 255, f"lerp(t>1) should clamp to 1" - print("+ Color.lerp() clamps t > 1") -except Exception as e: - print(f"x Color.lerp(t>1) failed: {e}") - all_pass = False - -# Test 15: Practical use case - gradient -try: - start = mcrfpy.Color.from_hex("#FF0000") # Red - end = mcrfpy.Color.from_hex("#0000FF") # Blue - - # Create 5-step gradient - steps = [] - for i in range(5): - t = i / 4.0 - color = start.lerp(end, t) - steps.append(color.to_hex()) - - assert steps[0] == "#FF0000", "Gradient start should be red" - assert steps[4] == "#0000FF", "Gradient end should be blue" - assert len(set(steps)) == 5, "All gradient steps should be unique" - - print("+ Gradient generation works correctly") -except Exception as e: - print(f"x Gradient generation failed: {e}") - all_pass = False - -print(f"\n{'PASS' if all_pass else 'FAIL'}") -sys.exit(0 if all_pass else 1) +test_timer = mcrfpy.Timer("test", test_color_helpers, 100, once=True) \ No newline at end of file diff --git a/tests/unit/test_empty_animation_manager.py b/tests/unit/test_empty_animation_manager.py index 46ef106..225bbde 100644 --- a/tests/unit/test_empty_animation_manager.py +++ b/tests/unit/test_empty_animation_manager.py @@ -1,28 +1,20 @@ #!/usr/bin/env python3 """ Test if AnimationManager crashes with no animations -Refactored to use mcrfpy.step() for synchronous execution. """ import mcrfpy -import sys print("Creating empty scene...") test = mcrfpy.Scene("test") test.activate() print("Scene created, no animations added") -print("Advancing simulation with step()...") +print("Starting game loop in 100ms...") -# Step multiple times to simulate game loop running -# If AnimationManager crashes with empty state, this will fail -try: - for i in range(10): - mcrfpy.step(0.1) # 10 steps of 0.1s = 1 second simulated +def check_alive(timer, runtime): + print(f"Timer fired at {runtime}ms - AnimationManager survived!") + mcrfpy.Timer("exit", lambda t, r: mcrfpy.exit(), 100, once=True) - print("AnimationManager survived 10 steps with no animations!") - print("PASS") - sys.exit(0) -except Exception as e: - print(f"FAIL: AnimationManager crashed: {e}") - sys.exit(1) +mcrfpy.Timer("check", check_alive, 1000, once=True) +print("If this crashes immediately, AnimationManager has an issue with empty state") diff --git a/tests/unit/test_frame_clipping.py b/tests/unit/test_frame_clipping.py index 1a93b75..0ec5c09 100644 --- a/tests/unit/test_frame_clipping.py +++ b/tests/unit/test_frame_clipping.py @@ -1,118 +1,135 @@ #!/usr/bin/env python3 -"""Test UIFrame clipping functionality -Refactored to use mcrfpy.step() for synchronous execution. -""" +"""Test UIFrame clipping functionality""" import mcrfpy -from mcrfpy import Color, Frame, Caption, automation +from mcrfpy import Color, Frame, Caption import sys +# Module-level state to avoid closures +_test_state = {} + +def take_second_screenshot(timer, runtime): + """Take final screenshot and exit""" + timer.stop() + from mcrfpy import automation + automation.screenshot("frame_clipping_animated.png") + print("\nTest completed successfully!") + print("Screenshots saved:") + print(" - frame_clipping_test.png (initial state)") + print(" - frame_clipping_animated.png (with animation)") + sys.exit(0) + +def animate_frames(timer, runtime): + """Animate frames to demonstrate clipping""" + timer.stop() + scene = test.children + # Move child frames + parent1 = scene[0] + parent2 = scene[1] + parent1.children[1].x = 50 + parent2.children[1].x = 50 + global screenshot2_timer + screenshot2_timer = mcrfpy.Timer("screenshot2", take_second_screenshot, 500, once=True) + +def test_clipping(timer, runtime): + """Test that clip_children property works correctly""" + timer.stop() + + print("Testing UIFrame clipping functionality...") + + scene = test.children + + # Create parent frame with clipping disabled (default) + parent1 = Frame(pos=(50, 50), size=(200, 150), + fill_color=Color(100, 100, 200), + outline_color=Color(255, 255, 255), + outline=2) + parent1.name = "parent1" + scene.append(parent1) + + # Create parent frame with clipping enabled + parent2 = Frame(pos=(300, 50), size=(200, 150), + fill_color=Color(200, 100, 100), + outline_color=Color(255, 255, 255), + outline=2) + parent2.name = "parent2" + parent2.clip_children = True + scene.append(parent2) + + # Add captions to both frames + caption1 = Caption(text="This text should overflow the frame bounds", pos=(10, 10)) + caption1.font_size = 16 + caption1.fill_color = Color(255, 255, 255) + parent1.children.append(caption1) + + caption2 = Caption(text="This text should be clipped to frame bounds", pos=(10, 10)) + caption2.font_size = 16 + caption2.fill_color = Color(255, 255, 255) + parent2.children.append(caption2) + + # Add child frames that extend beyond parent bounds + child1 = Frame(pos=(150, 100), size=(100, 100), + fill_color=Color(50, 255, 50), + outline_color=Color(0, 0, 0), + outline=1) + parent1.children.append(child1) + + child2 = Frame(pos=(150, 100), size=(100, 100), + fill_color=Color(50, 255, 50), + outline_color=Color(0, 0, 0), + outline=1) + parent2.children.append(child2) + + # Add caption to show clip state + status = Caption(text=f"Left frame: clip_children={parent1.clip_children}\n" + f"Right frame: clip_children={parent2.clip_children}", + pos=(50, 250)) + status.font_size = 14 + status.fill_color = Color(255, 255, 255) + scene.append(status) + + # Add instructions + instructions = Caption(text="Left: Children should overflow (no clipping)\n" + "Right: Children should be clipped to frame bounds\n" + "Press 'c' to toggle clipping on left frame", + pos=(50, 300)) + instructions.font_size = 12 + instructions.fill_color = Color(200, 200, 200) + scene.append(instructions) + + # Take screenshot + from mcrfpy import automation + automation.screenshot("frame_clipping_test.png") + + print(f"Parent1 clip_children: {parent1.clip_children}") + print(f"Parent2 clip_children: {parent2.clip_children}") + + # Test toggling clip_children + parent1.clip_children = True + print(f"After toggle - Parent1 clip_children: {parent1.clip_children}") + + # Verify the property setter works + try: + parent1.clip_children = "not a bool" + print("ERROR: clip_children accepted non-boolean value") + except TypeError as e: + print(f"PASS: clip_children correctly rejected non-boolean: {e}") + + # Start animation after a short delay + global animate_timer + animate_timer = mcrfpy.Timer("animate", animate_frames, 100, once=True) + +def handle_keypress(key, modifiers): + if key == "c": + scene = test.children + parent1 = scene[0] + parent1.clip_children = not parent1.clip_children + print(f"Toggled parent1 clip_children to: {parent1.clip_children}") + +# Main execution print("Creating test scene...") test = mcrfpy.Scene("test") test.activate() -mcrfpy.step(0.01) # Initialize - -print("Testing UIFrame clipping functionality...") - -scene = test.children - -# Create parent frame with clipping disabled (default) -parent1 = Frame(pos=(50, 50), size=(200, 150), - fill_color=Color(100, 100, 200), - outline_color=Color(255, 255, 255), - outline=2) -parent1.name = "parent1" -scene.append(parent1) - -# Create parent frame with clipping enabled -parent2 = Frame(pos=(300, 50), size=(200, 150), - fill_color=Color(200, 100, 100), - outline_color=Color(255, 255, 255), - outline=2) -parent2.name = "parent2" -parent2.clip_children = True -scene.append(parent2) - -# Add captions to both frames -caption1 = Caption(text="This text should overflow the frame bounds", pos=(10, 10)) -caption1.font_size = 16 -caption1.fill_color = Color(255, 255, 255) -parent1.children.append(caption1) - -caption2 = Caption(text="This text should be clipped to frame bounds", pos=(10, 10)) -caption2.font_size = 16 -caption2.fill_color = Color(255, 255, 255) -parent2.children.append(caption2) - -# Add child frames that extend beyond parent bounds -child1 = Frame(pos=(150, 100), size=(100, 100), - fill_color=Color(50, 255, 50), - outline_color=Color(0, 0, 0), - outline=1) -parent1.children.append(child1) - -child2 = Frame(pos=(150, 100), size=(100, 100), - fill_color=Color(50, 255, 50), - outline_color=Color(0, 0, 0), - outline=1) -parent2.children.append(child2) - -# Add caption to show clip state -status = Caption(text=f"Left frame: clip_children={parent1.clip_children}\n" - f"Right frame: clip_children={parent2.clip_children}", - pos=(50, 250)) -status.font_size = 14 -status.fill_color = Color(255, 255, 255) -scene.append(status) - -# Add instructions -instructions = Caption(text="Left: Children should overflow (no clipping)\n" - "Right: Children should be clipped to frame bounds", - pos=(50, 300)) -instructions.font_size = 12 -instructions.fill_color = Color(200, 200, 200) -scene.append(instructions) - -# Step to render -mcrfpy.step(0.1) - -# Take screenshot -automation.screenshot("frame_clipping_test.png") - -print(f"Parent1 clip_children: {parent1.clip_children}") -print(f"Parent2 clip_children: {parent2.clip_children}") - -# Test toggling clip_children -parent1.clip_children = True -print(f"After toggle - Parent1 clip_children: {parent1.clip_children}") - -# Verify the property setter works -test_passed = True -try: - parent1.clip_children = "not a bool" - print("ERROR: clip_children accepted non-boolean value") - test_passed = False -except TypeError as e: - print(f"PASS: clip_children correctly rejected non-boolean: {e}") - -# Animate frames (move children) -parent1.children[1].x = 50 -parent2.children[1].x = 50 - -# Step to render animation -mcrfpy.step(0.1) - -# Take second screenshot -automation.screenshot("frame_clipping_animated.png") - -print("\nTest completed successfully!") -print("Screenshots saved:") -print(" - frame_clipping_test.png (initial state)") -print(" - frame_clipping_animated.png (with animation)") - -if test_passed: - print("PASS") - sys.exit(0) -else: - print("FAIL") - sys.exit(1) +test.on_key = handle_keypress +test_clipping_timer = mcrfpy.Timer("test_clipping", test_clipping, 100, once=True) +print("Test scheduled, running...") diff --git a/tests/unit/test_frame_clipping_advanced.py b/tests/unit/test_frame_clipping_advanced.py index 769986b..5c18331 100644 --- a/tests/unit/test_frame_clipping_advanced.py +++ b/tests/unit/test_frame_clipping_advanced.py @@ -1,95 +1,105 @@ #!/usr/bin/env python3 -"""Advanced test for UIFrame clipping with nested frames -Refactored to use mcrfpy.step() for synchronous execution. -""" +"""Advanced test for UIFrame clipping with nested frames""" import mcrfpy -from mcrfpy import Color, Frame, Caption, Vector, automation +from mcrfpy import Color, Frame, Caption, Vector import sys +def test_nested_clipping(timer, runtime): + """Test nested frames with clipping""" + timer.stop() + + print("Testing advanced UIFrame clipping with nested frames...") + + # Create test scene + scene = test.children + + # Create outer frame with clipping enabled + outer = Frame(pos=(50, 50), size=(400, 300), + fill_color=Color(50, 50, 150), + outline_color=Color(255, 255, 255), + outline=3) + outer.name = "outer" + outer.clip_children = True + scene.append(outer) + + # Create inner frame that extends beyond outer bounds + inner = Frame(pos=(200, 150), size=(300, 200), + fill_color=Color(150, 50, 50), + outline_color=Color(255, 255, 0), + outline=2) + inner.name = "inner" + inner.clip_children = True # Also enable clipping on inner frame + outer.children.append(inner) + + # Add content to inner frame that extends beyond its bounds + for i in range(5): + caption = Caption(text=f"Line {i+1}: This text should be double-clipped", pos=(10, 30 * i)) + caption.font_size = 14 + caption.fill_color = Color(255, 255, 255) + inner.children.append(caption) + + # Add a child frame to inner that extends way out + deeply_nested = Frame(pos=(250, 100), size=(200, 150), + fill_color=Color(50, 150, 50), + outline_color=Color(255, 0, 255), + outline=2) + deeply_nested.name = "deeply_nested" + inner.children.append(deeply_nested) + + # Add status text + status = Caption(text="Nested clipping test:\n" + "- Blue outer frame clips red inner frame\n" + "- Red inner frame clips green deeply nested frame\n" + "- All text should be clipped to frame bounds", + pos=(50, 380)) + status.font_size = 12 + status.fill_color = Color(200, 200, 200) + scene.append(status) + + # Test render texture size handling + print(f"Outer frame size: {outer.w}x{outer.h}") + print(f"Inner frame size: {inner.w}x{inner.h}") + + # Dynamically resize frames to test RenderTexture recreation + def resize_test(timer, runtime): + timer.stop() + print("Resizing frames to test RenderTexture recreation...") + outer.w = 450 + outer.h = 350 + inner.w = 350 + inner.h = 250 + print(f"New outer frame size: {outer.w}x{outer.h}") + print(f"New inner frame size: {inner.w}x{inner.h}") + + # Take screenshot after resize + global screenshot_resize_timer + screenshot_resize_timer = mcrfpy.Timer("screenshot_resize", take_resize_screenshot, 500, once=True) + + def take_resize_screenshot(timer, runtime): + timer.stop() + from mcrfpy import automation + automation.screenshot("frame_clipping_resized.png") + print("\nAdvanced test completed!") + print("Screenshots saved:") + print(" - frame_clipping_resized.png (after resize)") + sys.exit(0) + + # Take initial screenshot + from mcrfpy import automation + automation.screenshot("frame_clipping_nested.png") + print("Initial screenshot saved: frame_clipping_nested.png") + + # Schedule resize test + global resize_test_timer + resize_test_timer = mcrfpy.Timer("resize_test", resize_test, 1000, once=True) + +# Main execution print("Creating advanced test scene...") test = mcrfpy.Scene("test") test.activate() -mcrfpy.step(0.01) -print("Testing advanced UIFrame clipping with nested frames...") +# Schedule the test +test_nested_clipping_timer = mcrfpy.Timer("test_nested_clipping", test_nested_clipping, 100, once=True) -# Create test scene -scene = test.children - -# Create outer frame with clipping enabled -outer = Frame(pos=(50, 50), size=(400, 300), - fill_color=Color(50, 50, 150), - outline_color=Color(255, 255, 255), - outline=3) -outer.name = "outer" -outer.clip_children = True -scene.append(outer) - -# Create inner frame that extends beyond outer bounds -inner = Frame(pos=(200, 150), size=(300, 200), - fill_color=Color(150, 50, 50), - outline_color=Color(255, 255, 0), - outline=2) -inner.name = "inner" -inner.clip_children = True # Also enable clipping on inner frame -outer.children.append(inner) - -# Add content to inner frame that extends beyond its bounds -for i in range(5): - caption = Caption(text=f"Line {i+1}: This text should be double-clipped", pos=(10, 30 * i)) - caption.font_size = 14 - caption.fill_color = Color(255, 255, 255) - inner.children.append(caption) - -# Add a child frame to inner that extends way out -deeply_nested = Frame(pos=(250, 100), size=(200, 150), - fill_color=Color(50, 150, 50), - outline_color=Color(255, 0, 255), - outline=2) -deeply_nested.name = "deeply_nested" -inner.children.append(deeply_nested) - -# Add status text -status = Caption(text="Nested clipping test:\n" - "- Blue outer frame clips red inner frame\n" - "- Red inner frame clips green deeply nested frame\n" - "- All text should be clipped to frame bounds", - pos=(50, 380)) -status.font_size = 12 -status.fill_color = Color(200, 200, 200) -scene.append(status) - -# Test render texture size handling -print(f"Outer frame size: {outer.w}x{outer.h}") -print(f"Inner frame size: {inner.w}x{inner.h}") - -# Step to render -mcrfpy.step(0.1) - -# Take initial screenshot -automation.screenshot("frame_clipping_nested.png") -print("Initial screenshot saved: frame_clipping_nested.png") - -# Dynamically resize frames to test RenderTexture recreation -print("Resizing frames to test RenderTexture recreation...") -outer.w = 450 -outer.h = 350 -inner.w = 350 -inner.h = 250 -print(f"New outer frame size: {outer.w}x{outer.h}") -print(f"New inner frame size: {inner.w}x{inner.h}") - -# Step to render resize -mcrfpy.step(0.1) - -# Take screenshot after resize -automation.screenshot("frame_clipping_resized.png") - -print("\nAdvanced test completed!") -print("Screenshots saved:") -print(" - frame_clipping_nested.png (initial)") -print(" - frame_clipping_resized.png (after resize)") - -print("PASS") -sys.exit(0) +print("Advanced test scheduled, running...") \ No newline at end of file diff --git a/tests/unit/test_grid_children.py b/tests/unit/test_grid_children.py index 9ffd5e4..306f8d9 100644 --- a/tests/unit/test_grid_children.py +++ b/tests/unit/test_grid_children.py @@ -1,125 +1,129 @@ #!/usr/bin/env python3 -"""Test Grid.children collection - Issue #132 -Refactored to use mcrfpy.step() for synchronous execution. -""" +"""Test Grid.children collection - Issue #132""" import mcrfpy from mcrfpy import automation import sys -print("Creating test scene...") +def take_screenshot(timer, runtime): + """Take screenshot after render completes""" + timer.stop() + automation.screenshot("test_grid_children_result.png") + + print("Screenshot saved to test_grid_children_result.png") + print("PASS - Grid.children test completed") + sys.exit(0) + +def run_test(timer, runtime): + """Main test - runs after scene is set up""" + timer.stop() + + # Get the scene UI + ui = test.children + + # Create a grid without texture (uses default 16x16 cells) + print("Test 1: Creating Grid with children...") + grid = mcrfpy.Grid(grid_size=(20, 15), pos=(50, 50), size=(320, 240)) + grid.fill_color = mcrfpy.Color(30, 30, 60) + ui.append(grid) + + # Verify entities and children properties exist + print(f" grid.entities = {grid.entities}") + print(f" grid.children = {grid.children}") + + # Test 2: Add UIDrawable children to the grid + print("\nTest 2: Adding UIDrawable children...") + + # Speech bubble style caption - positioned in grid-world pixels + # At cell (5, 3) which is 5*16=80, 3*16=48 in pixels + caption = mcrfpy.Caption(text="Hello!", pos=(80, 48)) + caption.fill_color = mcrfpy.Color(255, 255, 200) + caption.outline = 1 + caption.outline_color = mcrfpy.Color(0, 0, 0) + grid.children.append(caption) + print(f" Added caption at (80, 48)") + + # A highlight circle around cell (10, 7) = (160, 112) + # Circle needs center, not pos + circle = mcrfpy.Circle(center=(168, 120), radius=20, + fill_color=mcrfpy.Color(255, 255, 0, 100), + outline_color=mcrfpy.Color(255, 255, 0), + outline=2) + grid.children.append(circle) + print(f" Added highlight circle at (168, 120)") + + # A line indicating a path from (2,2) to (8,6) + # In pixels: (32, 32) to (128, 96) + line = mcrfpy.Line(start=(32, 32), end=(128, 96), + color=mcrfpy.Color(0, 255, 0), thickness=3) + grid.children.append(line) + print(f" Added path line from (32,32) to (128,96)") + + # An arc for range indicator at (15, 10) = (240, 160) + arc = mcrfpy.Arc(center=(240, 160), radius=40, start_angle=0, end_angle=270, + color=mcrfpy.Color(255, 0, 255), thickness=4) + grid.children.append(arc) + print(f" Added range arc at (240, 160)") + + # Test 3: Verify children count + print(f"\nTest 3: Verifying children count...") + print(f" grid.children count = {len(grid.children)}") + assert len(grid.children) == 4, f"Expected 4 children, got {len(grid.children)}" + + # Test 4: Children should be accessible by index + print("\nTest 4: Accessing children by index...") + child0 = grid.children[0] + print(f" grid.children[0] = {child0}") + child1 = grid.children[1] + print(f" grid.children[1] = {child1}") + + # Test 5: Modify a child's position (should update in grid) + print("\nTest 5: Modifying child position...") + original_pos = (caption.pos.x, caption.pos.y) + caption.pos = mcrfpy.Vector(90, 58) + new_pos = (caption.pos.x, caption.pos.y) + print(f" Moved caption from {original_pos} to {new_pos}") + + # Test 6: Test z_index for children + print("\nTest 6: Testing z_index ordering...") + # Add overlapping elements with different z_index + frame1 = mcrfpy.Frame(pos=(150, 80), size=(40, 40)) + frame1.fill_color = mcrfpy.Color(255, 0, 0, 200) + frame1.z_index = 10 + grid.children.append(frame1) + + frame2 = mcrfpy.Frame(pos=(160, 90), size=(40, 40)) + frame2.fill_color = mcrfpy.Color(0, 255, 0, 200) + frame2.z_index = 5 # Lower z_index, rendered first (behind) + grid.children.append(frame2) + print(f" Added overlapping frames: red z=10, green z=5") + + # Test 7: Test visibility + print("\nTest 7: Testing child visibility...") + frame3 = mcrfpy.Frame(pos=(50, 150), size=(30, 30)) + frame3.fill_color = mcrfpy.Color(0, 0, 255) + frame3.visible = False + grid.children.append(frame3) + print(f" Added invisible blue frame (should not appear)") + + # Test 8: Pan the grid and verify children move with it + print("\nTest 8: Testing pan (children should follow grid camera)...") + # Center the view on cell (10, 7.5) - default was grid center + grid.center = (160, 120) # Center on pixel (160, 120) + print(f" Centered grid on (160, 120)") + + # Test 9: Test zoom + print("\nTest 9: Testing zoom...") + grid.zoom = 1.5 + print(f" Set zoom to 1.5") + + print(f"\nFinal children count: {len(grid.children)}") + + # Schedule screenshot for next frame + mcrfpy.Timer("screenshot", take_screenshot, 100, once=True) + +# Create a test scene test = mcrfpy.Scene("test") test.activate() -mcrfpy.step(0.01) # Initialize -# Get the scene UI -ui = test.children - -# Test 1: Creating Grid with children -print("Test 1: Creating Grid with children...") -grid = mcrfpy.Grid(grid_size=(20, 15), pos=(50, 50), size=(320, 240)) -grid.fill_color = mcrfpy.Color(30, 30, 60) -ui.append(grid) - -# Verify entities and children properties exist -print(f" grid.entities = {grid.entities}") -print(f" grid.children = {grid.children}") - -# Test 2: Add UIDrawable children to the grid -print("\nTest 2: Adding UIDrawable children...") - -# Speech bubble style caption - positioned in grid-world pixels -# At cell (5, 3) which is 5*16=80, 3*16=48 in pixels -caption = mcrfpy.Caption(text="Hello!", pos=(80, 48)) -caption.fill_color = mcrfpy.Color(255, 255, 200) -caption.outline = 1 -caption.outline_color = mcrfpy.Color(0, 0, 0) -grid.children.append(caption) -print(f" Added caption at (80, 48)") - -# A highlight circle around cell (10, 7) = (160, 112) -# Circle needs center, not pos -circle = mcrfpy.Circle(center=(168, 120), radius=20, - fill_color=mcrfpy.Color(255, 255, 0, 100), - outline_color=mcrfpy.Color(255, 255, 0), - outline=2) -grid.children.append(circle) -print(f" Added highlight circle at (168, 120)") - -# A line indicating a path from (2,2) to (8,6) -# In pixels: (32, 32) to (128, 96) -line = mcrfpy.Line(start=(32, 32), end=(128, 96), - color=mcrfpy.Color(0, 255, 0), thickness=3) -grid.children.append(line) -print(f" Added path line from (32,32) to (128,96)") - -# An arc for range indicator at (15, 10) = (240, 160) -arc = mcrfpy.Arc(center=(240, 160), radius=40, start_angle=0, end_angle=270, - color=mcrfpy.Color(255, 0, 255), thickness=4) -grid.children.append(arc) -print(f" Added range arc at (240, 160)") - -# Test 3: Verify children count -print(f"\nTest 3: Verifying children count...") -print(f" grid.children count = {len(grid.children)}") -if len(grid.children) != 4: - print(f"FAIL: Expected 4 children, got {len(grid.children)}") - sys.exit(1) - -# Test 4: Children should be accessible by index -print("\nTest 4: Accessing children by index...") -child0 = grid.children[0] -print(f" grid.children[0] = {child0}") -child1 = grid.children[1] -print(f" grid.children[1] = {child1}") - -# Test 5: Modify a child's position (should update in grid) -print("\nTest 5: Modifying child position...") -original_pos = (caption.pos.x, caption.pos.y) -caption.pos = mcrfpy.Vector(90, 58) -new_pos = (caption.pos.x, caption.pos.y) -print(f" Moved caption from {original_pos} to {new_pos}") - -# Test 6: Test z_index for children -print("\nTest 6: Testing z_index ordering...") -# Add overlapping elements with different z_index -frame1 = mcrfpy.Frame(pos=(150, 80), size=(40, 40)) -frame1.fill_color = mcrfpy.Color(255, 0, 0, 200) -frame1.z_index = 10 -grid.children.append(frame1) - -frame2 = mcrfpy.Frame(pos=(160, 90), size=(40, 40)) -frame2.fill_color = mcrfpy.Color(0, 255, 0, 200) -frame2.z_index = 5 # Lower z_index, rendered first (behind) -grid.children.append(frame2) -print(f" Added overlapping frames: red z=10, green z=5") - -# Test 7: Test visibility -print("\nTest 7: Testing child visibility...") -frame3 = mcrfpy.Frame(pos=(50, 150), size=(30, 30)) -frame3.fill_color = mcrfpy.Color(0, 0, 255) -frame3.visible = False -grid.children.append(frame3) -print(f" Added invisible blue frame (should not appear)") - -# Test 8: Pan the grid and verify children move with it -print("\nTest 8: Testing pan (children should follow grid camera)...") -# Center the view on cell (10, 7.5) - default was grid center -grid.center = (160, 120) # Center on pixel (160, 120) -print(f" Centered grid on (160, 120)") - -# Test 9: Test zoom -print("\nTest 9: Testing zoom...") -grid.zoom = 1.5 -print(f" Set zoom to 1.5") - -print(f"\nFinal children count: {len(grid.children)}") - -# Step to render everything -mcrfpy.step(0.1) - -# Take screenshot -automation.screenshot("test_grid_children_result.png") -print("Screenshot saved to test_grid_children_result.png") - -print("PASS - Grid.children test completed") -sys.exit(0) +# Schedule test to run after game loop starts +mcrfpy.Timer("test", run_test, 50, once=True) diff --git a/tests/unit/test_no_arg_constructors.py b/tests/unit/test_no_arg_constructors.py index c159030..1c884d3 100644 --- a/tests/unit/test_no_arg_constructors.py +++ b/tests/unit/test_no_arg_constructors.py @@ -2,94 +2,90 @@ """ Test that all UI classes can be instantiated without arguments. This verifies the fix for requiring arguments even with safe default constructors. -Refactored to use mcrfpy.step() for synchronous execution. """ import mcrfpy import sys -import traceback -# Initialize scene -test = mcrfpy.Scene("test") -test.activate() -mcrfpy.step(0.01) - -print("Testing UI class instantiation without arguments...") - -all_pass = True - -# Test UICaption with no arguments -try: - caption = mcrfpy.Caption() - print("PASS: Caption() - Success") - print(f" Position: ({caption.x}, {caption.y})") - print(f" Text: '{caption.text}'") - assert caption.x == 0.0 - assert caption.y == 0.0 - assert caption.text == "" -except Exception as e: - print(f"FAIL: Caption() - {e}") - traceback.print_exc() - all_pass = False - -# Test UIFrame with no arguments -try: - frame = mcrfpy.Frame() - print("PASS: Frame() - Success") - print(f" Position: ({frame.x}, {frame.y})") - print(f" Size: ({frame.w}, {frame.h})") - assert frame.x == 0.0 - assert frame.y == 0.0 - assert frame.w == 0.0 - assert frame.h == 0.0 -except Exception as e: - print(f"FAIL: Frame() - {e}") - traceback.print_exc() - all_pass = False - -# Test UIGrid with no arguments -try: - grid = mcrfpy.Grid() - print("PASS: Grid() - Success") - print(f" Grid size: {grid.grid_x} x {grid.grid_y}") - print(f" Position: ({grid.x}, {grid.y})") - assert grid.grid_x == 1 - assert grid.grid_y == 1 - assert grid.x == 0.0 - assert grid.y == 0.0 -except Exception as e: - print(f"FAIL: Grid() - {e}") - traceback.print_exc() - all_pass = False - -# Test UIEntity with no arguments -try: - entity = mcrfpy.Entity() - print("PASS: Entity() - Success") - print(f" Position: ({entity.x}, {entity.y})") - assert entity.x == 0.0 - assert entity.y == 0.0 -except Exception as e: - print(f"FAIL: Entity() - {e}") - traceback.print_exc() - all_pass = False - -# Test UISprite with no arguments (if it has a default constructor) -try: - sprite = mcrfpy.Sprite() - print("PASS: Sprite() - Success") - print(f" Position: ({sprite.x}, {sprite.y})") - assert sprite.x == 0.0 - assert sprite.y == 0.0 -except Exception as e: - print(f"FAIL: Sprite() - {e}") - # Sprite might still require arguments, which is okay - -print("\nAll tests complete!") - -if all_pass: - print("PASS") +def test_ui_constructors(timer, runtime): + """Test that UI classes can be instantiated without arguments""" + + print("Testing UI class instantiation without arguments...") + + # Test UICaption with no arguments + try: + caption = mcrfpy.Caption() + print("PASS: Caption() - Success") + print(f" Position: ({caption.x}, {caption.y})") + print(f" Text: '{caption.text}'") + assert caption.x == 0.0 + assert caption.y == 0.0 + assert caption.text == "" + except Exception as e: + print(f"FAIL: Caption() - {e}") + import traceback + traceback.print_exc() + + # Test UIFrame with no arguments + try: + frame = mcrfpy.Frame() + print("PASS: Frame() - Success") + print(f" Position: ({frame.x}, {frame.y})") + print(f" Size: ({frame.w}, {frame.h})") + assert frame.x == 0.0 + assert frame.y == 0.0 + assert frame.w == 0.0 + assert frame.h == 0.0 + except Exception as e: + print(f"FAIL: Frame() - {e}") + import traceback + traceback.print_exc() + + # Test UIGrid with no arguments + try: + grid = mcrfpy.Grid() + print("PASS: Grid() - Success") + print(f" Grid size: {grid.grid_x} x {grid.grid_y}") + print(f" Position: ({grid.x}, {grid.y})") + assert grid.grid_x == 1 + assert grid.grid_y == 1 + assert grid.x == 0.0 + assert grid.y == 0.0 + except Exception as e: + print(f"FAIL: Grid() - {e}") + import traceback + traceback.print_exc() + + # Test UIEntity with no arguments + try: + entity = mcrfpy.Entity() + print("PASS: Entity() - Success") + print(f" Position: ({entity.x}, {entity.y})") + assert entity.x == 0.0 + assert entity.y == 0.0 + except Exception as e: + print(f"FAIL: Entity() - {e}") + import traceback + traceback.print_exc() + + # Test UISprite with no arguments (if it has a default constructor) + try: + sprite = mcrfpy.Sprite() + print("PASS: Sprite() - Success") + print(f" Position: ({sprite.x}, {sprite.y})") + assert sprite.x == 0.0 + assert sprite.y == 0.0 + except Exception as e: + print(f"FAIL: Sprite() - {e}") + # Sprite might still require arguments, which is okay + + print("\nAll tests complete!") + + # Exit cleanly sys.exit(0) -else: - print("FAIL") - sys.exit(1) + +# Create a basic scene so the game can start +test = mcrfpy.Scene("test") + +# Schedule the test to run after game initialization +test_timer = mcrfpy.Timer("test", test_ui_constructors, 100, once=True) \ No newline at end of file diff --git a/tests/unit/test_properties_quick.py b/tests/unit/test_properties_quick.py index 5c1e696..0fd6ee3 100644 --- a/tests/unit/test_properties_quick.py +++ b/tests/unit/test_properties_quick.py @@ -1,67 +1,57 @@ #!/usr/bin/env python3 -"""Quick test of drawable properties -Refactored to use mcrfpy.step() for synchronous execution. -""" +"""Quick test of drawable properties""" import mcrfpy import sys -# Initialize scene -test = mcrfpy.Scene("test") -test.activate() -mcrfpy.step(0.01) - -print("\n=== Testing Properties ===") - -all_pass = True - -# Test Frame -try: - frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100)) - print(f"Frame visible: {frame.visible}") - frame.visible = False - print(f"Frame visible after setting to False: {frame.visible}") - - print(f"Frame opacity: {frame.opacity}") - frame.opacity = 0.5 - print(f"Frame opacity after setting to 0.5: {frame.opacity}") - - bounds = frame.get_bounds() - print(f"Frame bounds: {bounds}") - - frame.move(5, 5) - bounds2 = frame.get_bounds() - print(f"Frame bounds after move(5,5): {bounds2}") - - print("+ Frame properties work!") -except Exception as e: - print(f"x Frame failed: {e}") - all_pass = False - -# Test Entity -try: - entity = mcrfpy.Entity() - print(f"\nEntity visible: {entity.visible}") - entity.visible = False - print(f"Entity visible after setting to False: {entity.visible}") - - print(f"Entity opacity: {entity.opacity}") - entity.opacity = 0.7 - print(f"Entity opacity after setting to 0.7: {entity.opacity}") - - bounds = entity.get_bounds() - print(f"Entity bounds: {bounds}") - - entity.move(3, 3) - print(f"Entity position after move(3,3): ({entity.x}, {entity.y})") - - print("+ Entity properties work!") -except Exception as e: - print(f"x Entity failed: {e}") - all_pass = False - -if all_pass: - print("\nPASS") +def test_properties(timer, runtime): + timer.stop() + + print("\n=== Testing Properties ===") + + # Test Frame + try: + frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100)) + print(f"Frame visible: {frame.visible}") + frame.visible = False + print(f"Frame visible after setting to False: {frame.visible}") + + print(f"Frame opacity: {frame.opacity}") + frame.opacity = 0.5 + print(f"Frame opacity after setting to 0.5: {frame.opacity}") + + bounds = frame.get_bounds() + print(f"Frame bounds: {bounds}") + + frame.move(5, 5) + bounds2 = frame.get_bounds() + print(f"Frame bounds after move(5,5): {bounds2}") + + print("✓ Frame properties work!") + except Exception as e: + print(f"✗ Frame failed: {e}") + + # Test Entity + try: + entity = mcrfpy.Entity() + print(f"\nEntity visible: {entity.visible}") + entity.visible = False + print(f"Entity visible after setting to False: {entity.visible}") + + print(f"Entity opacity: {entity.opacity}") + entity.opacity = 0.7 + print(f"Entity opacity after setting to 0.7: {entity.opacity}") + + bounds = entity.get_bounds() + print(f"Entity bounds: {bounds}") + + entity.move(3, 3) + print(f"Entity position after move(3,3): ({entity.x}, {entity.y})") + + print("✓ Entity properties work!") + except Exception as e: + print(f"✗ Entity failed: {e}") + sys.exit(0) -else: - print("\nFAIL") - sys.exit(1) + +test = mcrfpy.Scene("test") +test_properties_timer = mcrfpy.Timer("test_properties", test_properties, 100, once=True) \ No newline at end of file diff --git a/tests/unit/test_python_object_cache.py b/tests/unit/test_python_object_cache.py index d426cd4..dbf83e3 100644 --- a/tests/unit/test_python_object_cache.py +++ b/tests/unit/test_python_object_cache.py @@ -4,7 +4,6 @@ Test for Python object cache - verifies that derived Python classes maintain their identity when stored in and retrieved from collections. Issue #112: Object Splitting - Preserve Python derived types in collections -Refactored to use mcrfpy.step() for synchronous execution. """ import mcrfpy @@ -17,128 +16,136 @@ test_results = [] def test(condition, message): global test_passed if condition: - test_results.append(f"+ {message}") + test_results.append(f"✓ {message}") else: - test_results.append(f"x {message}") + test_results.append(f"✗ {message}") test_passed = False +def run_tests(timer, runtime): + """Timer callback to run tests after game loop starts""" + global test_passed + + print("\n=== Testing Python Object Cache ===") + + # Test 1: Create derived Frame class + class MyFrame(mcrfpy.Frame): + def __init__(self, x=0, y=0): + super().__init__(pos=(x, y), size=(100, 100)) + self.custom_data = "I am a custom frame" + self.test_value = 42 + + # Test 2: Create instance and add to scene + frame = MyFrame(50, 50) + scene_ui = test_scene.children + scene_ui.append(frame) + + # Test 3: Retrieve from collection and check type + retrieved = scene_ui[0] + test(type(retrieved) == MyFrame, "Retrieved object maintains derived type") + test(isinstance(retrieved, MyFrame), "isinstance check passes") + test(hasattr(retrieved, 'custom_data'), "Custom attribute exists") + if hasattr(retrieved, 'custom_data'): + test(retrieved.custom_data == "I am a custom frame", "Custom attribute value preserved") + if hasattr(retrieved, 'test_value'): + test(retrieved.test_value == 42, "Numeric attribute value preserved") + + # Test 4: Check object identity (same Python object) + test(retrieved is frame, "Retrieved object is the same Python object") + test(id(retrieved) == id(frame), "Object IDs match") + + # Test 5: Multiple retrievals return same object + retrieved2 = scene_ui[0] + test(retrieved2 is retrieved, "Multiple retrievals return same object") + + # Test 6: Test with other UI types + class MySprite(mcrfpy.Sprite): + def __init__(self): + # Use default texture by passing None + super().__init__(texture=None, sprite_index=0) + self.sprite_data = "custom sprite" + + sprite = MySprite() + sprite.x = 200 + sprite.y = 200 + scene_ui.append(sprite) + + retrieved_sprite = scene_ui[1] + test(type(retrieved_sprite) == MySprite, "Sprite maintains derived type") + if hasattr(retrieved_sprite, 'sprite_data'): + test(retrieved_sprite.sprite_data == "custom sprite", "Sprite custom data preserved") + + # Test 7: Test with Caption + class MyCaption(mcrfpy.Caption): + def __init__(self, text): + # Use default font by passing None + super().__init__(text=text, font=None) + self.caption_id = "test_caption" + + caption = MyCaption("Test Caption") + caption.x = 10 + caption.y = 10 + scene_ui.append(caption) + + retrieved_caption = scene_ui[2] + test(type(retrieved_caption) == MyCaption, "Caption maintains derived type") + if hasattr(retrieved_caption, 'caption_id'): + test(retrieved_caption.caption_id == "test_caption", "Caption custom data preserved") + + # Test 8: Test removal and re-addition + # Use del to remove by index (Python standard), or .remove(element) to remove by value + print(f"before remove: {len(scene_ui)=}") + del scene_ui[-1] # Remove last element by index + print(f"after remove: {len(scene_ui)=}") + + scene_ui.append(frame) + retrieved3 = scene_ui[-1] # Get last element + test(retrieved3 is frame, "Object identity preserved after removal/re-addition") + + # Test 9: Test with Grid + class MyGrid(mcrfpy.Grid): + def __init__(self, w, h): + super().__init__(grid_size=(w, h)) + self.grid_name = "custom_grid" + + grid = MyGrid(10, 10) + grid.x = 300 + grid.y = 100 + scene_ui.append(grid) + + retrieved_grid = scene_ui[-1] + test(type(retrieved_grid) == MyGrid, "Grid maintains derived type") + if hasattr(retrieved_grid, 'grid_name'): + test(retrieved_grid.grid_name == "custom_grid", "Grid custom data preserved") + + # Test 10: Test with nested collections (Frame with children) + parent = MyFrame(400, 400) + child = MyFrame(10, 10) + child.custom_data = "I am a child" + parent.children.append(child) + scene_ui.append(parent) + + retrieved_parent = scene_ui[-1] + test(type(retrieved_parent) == MyFrame, "Parent frame maintains type") + if len(retrieved_parent.children) > 0: + retrieved_child = retrieved_parent.children[0] + test(type(retrieved_child) == MyFrame, "Child frame maintains type in nested collection") + if hasattr(retrieved_child, 'custom_data'): + test(retrieved_child.custom_data == "I am a child", "Child custom data preserved") + + # Print results + print("\n=== Test Results ===") + for result in test_results: + print(result) + + print(f"\n{'PASS' if test_passed else 'FAIL'}: {sum(1 for r in test_results if r.startswith('✓'))}/{len(test_results)} tests passed") + + sys.exit(0 if test_passed else 1) + # Create test scene test_scene = mcrfpy.Scene("test_scene") test_scene.activate() -mcrfpy.step(0.01) -print("\n=== Testing Python Object Cache ===") +# Schedule tests to run after game loop starts +test_timer = mcrfpy.Timer("test", run_tests, 100, once=True) -# Test 1: Create derived Frame class -class MyFrame(mcrfpy.Frame): - def __init__(self, x=0, y=0): - super().__init__(pos=(x, y), size=(100, 100)) - self.custom_data = "I am a custom frame" - self.test_value = 42 - -# Test 2: Create instance and add to scene -frame = MyFrame(50, 50) -scene_ui = test_scene.children -scene_ui.append(frame) - -# Test 3: Retrieve from collection and check type -retrieved = scene_ui[0] -test(type(retrieved) == MyFrame, "Retrieved object maintains derived type") -test(isinstance(retrieved, MyFrame), "isinstance check passes") -test(hasattr(retrieved, 'custom_data'), "Custom attribute exists") -if hasattr(retrieved, 'custom_data'): - test(retrieved.custom_data == "I am a custom frame", "Custom attribute value preserved") -if hasattr(retrieved, 'test_value'): - test(retrieved.test_value == 42, "Numeric attribute value preserved") - -# Test 4: Check object identity (same Python object) -test(retrieved is frame, "Retrieved object is the same Python object") -test(id(retrieved) == id(frame), "Object IDs match") - -# Test 5: Multiple retrievals return same object -retrieved2 = scene_ui[0] -test(retrieved2 is retrieved, "Multiple retrievals return same object") - -# Test 6: Test with other UI types -class MySprite(mcrfpy.Sprite): - def __init__(self): - # Use default texture by passing None - super().__init__(texture=None, sprite_index=0) - self.sprite_data = "custom sprite" - -sprite = MySprite() -sprite.x = 200 -sprite.y = 200 -scene_ui.append(sprite) - -retrieved_sprite = scene_ui[1] -test(type(retrieved_sprite) == MySprite, "Sprite maintains derived type") -if hasattr(retrieved_sprite, 'sprite_data'): - test(retrieved_sprite.sprite_data == "custom sprite", "Sprite custom data preserved") - -# Test 7: Test with Caption -class MyCaption(mcrfpy.Caption): - def __init__(self, text): - # Use default font by passing None - super().__init__(text=text, font=None) - self.caption_id = "test_caption" - -caption = MyCaption("Test Caption") -caption.x = 10 -caption.y = 10 -scene_ui.append(caption) - -retrieved_caption = scene_ui[2] -test(type(retrieved_caption) == MyCaption, "Caption maintains derived type") -if hasattr(retrieved_caption, 'caption_id'): - test(retrieved_caption.caption_id == "test_caption", "Caption custom data preserved") - -# Test 8: Test removal and re-addition -# Use del to remove by index (Python standard), or .remove(element) to remove by value -print(f"before remove: {len(scene_ui)=}") -del scene_ui[-1] # Remove last element by index -print(f"after remove: {len(scene_ui)=}") - -scene_ui.append(frame) -retrieved3 = scene_ui[-1] # Get last element -test(retrieved3 is frame, "Object identity preserved after removal/re-addition") - -# Test 9: Test with Grid -class MyGrid(mcrfpy.Grid): - def __init__(self, w, h): - super().__init__(grid_size=(w, h)) - self.grid_name = "custom_grid" - -grid = MyGrid(10, 10) -grid.x = 300 -grid.y = 100 -scene_ui.append(grid) - -retrieved_grid = scene_ui[-1] -test(type(retrieved_grid) == MyGrid, "Grid maintains derived type") -if hasattr(retrieved_grid, 'grid_name'): - test(retrieved_grid.grid_name == "custom_grid", "Grid custom data preserved") - -# Test 10: Test with nested collections (Frame with children) -parent = MyFrame(400, 400) -child = MyFrame(10, 10) -child.custom_data = "I am a child" -parent.children.append(child) -scene_ui.append(parent) - -retrieved_parent = scene_ui[-1] -test(type(retrieved_parent) == MyFrame, "Parent frame maintains type") -if len(retrieved_parent.children) > 0: - retrieved_child = retrieved_parent.children[0] - test(type(retrieved_child) == MyFrame, "Child frame maintains type in nested collection") - if hasattr(retrieved_child, 'custom_data'): - test(retrieved_child.custom_data == "I am a child", "Child custom data preserved") - -# Print results -print("\n=== Test Results ===") -for result in test_results: - print(result) - -print(f"\n{'PASS' if test_passed else 'FAIL'}: {sum(1 for r in test_results if r.startswith('+'))}/{len(test_results)} tests passed") - -sys.exit(0 if test_passed else 1) +print("Python object cache test initialized. Running tests...") diff --git a/tests/unit/test_simple_callback.py b/tests/unit/test_simple_callback.py index 18a403b..7e7cd6a 100644 --- a/tests/unit/test_simple_callback.py +++ b/tests/unit/test_simple_callback.py @@ -1,32 +1,14 @@ #!/usr/bin/env python3 -"""Very simple callback test - refactored to use mcrfpy.step()""" +"""Very simple callback test""" import mcrfpy import sys -callback_fired = False - def cb(a, t): - global callback_fired - callback_fired = True print("CB") -# Setup scene test = mcrfpy.Scene("test") test.activate() -mcrfpy.step(0.01) # Initialize - -# Create entity and animation e = mcrfpy.Entity((0, 0), texture=None, sprite_index=0) a = mcrfpy.Animation("x", 1.0, 0.1, "linear", callback=cb) a.start(e) - -# Advance past animation duration (0.1s) -mcrfpy.step(0.15) - -# Verify callback fired -if callback_fired: - print("PASS: Callback fired") - sys.exit(0) -else: - print("FAIL: Callback did not fire") - sys.exit(1) +mcrfpy.Timer("exit", lambda t, r: sys.exit(0), 200, once=True) diff --git a/tests/unit/test_simple_drawable.py b/tests/unit/test_simple_drawable.py index 36552b4..63d37c3 100644 --- a/tests/unit/test_simple_drawable.py +++ b/tests/unit/test_simple_drawable.py @@ -1,32 +1,30 @@ #!/usr/bin/env python3 -"""Simple test to isolate drawable issue -Refactored to use mcrfpy.step() for synchronous execution. -""" +"""Simple test to isolate drawable issue""" import mcrfpy import sys -# Initialize scene -test = mcrfpy.Scene("test") -test.activate() -mcrfpy.step(0.01) - -try: - # Test basic functionality - frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100)) - print(f"Frame created: visible={frame.visible}, opacity={frame.opacity}") - - bounds = frame.get_bounds() - print(f"Bounds: {bounds}") - - frame.move(5, 5) - print("Move completed") - - frame.resize(150, 150) - print("Resize completed") - - print("PASS - No crash!") +def simple_test(timer, runtime): + timer.stop() + + try: + # Test basic functionality + frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100)) + print(f"Frame created: visible={frame.visible}, opacity={frame.opacity}") + + bounds = frame.get_bounds() + print(f"Bounds: {bounds}") + + frame.move(5, 5) + print("Move completed") + + frame.resize(150, 150) + print("Resize completed") + + print("PASS - No crash!") + except Exception as e: + print(f"ERROR: {e}") + sys.exit(0) -except Exception as e: - print(f"ERROR: {e}") - print("FAIL") - sys.exit(1) + +test = mcrfpy.Scene("test") +simple_test_timer = mcrfpy.Timer("simple_test", simple_test, 100, once=True) \ No newline at end of file diff --git a/tests/unit/test_timer_callback.py b/tests/unit/test_timer_callback.py index 81d2357..6f46efe 100644 --- a/tests/unit/test_timer_callback.py +++ b/tests/unit/test_timer_callback.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 """ Test timer callback arguments with new Timer API (#173) -Uses mcrfpy.step() for synchronous test execution. """ import mcrfpy import sys @@ -15,6 +14,9 @@ def new_style_callback(timer, runtime): print(f"Callback called with: timer={timer} (type: {type(timer)}), runtime={runtime} (type: {type(runtime)})") if hasattr(timer, 'once'): print(f"Got Timer object! once={timer.once}") + if call_count >= 2: + print("PASS") + sys.exit(0) # Set up the scene test_scene = mcrfpy.Scene("test_scene") @@ -23,14 +25,3 @@ test_scene.activate() print("Testing new Timer callback signature (timer, runtime)...") timer = mcrfpy.Timer("test_timer", new_style_callback, 100) print(f"Timer created: {timer}") - -# Advance time to let timer fire - each step() processes timers once -mcrfpy.step(0.15) # First fire -mcrfpy.step(0.15) # Second fire - -if call_count >= 2: - print("PASS: Timer callback received correct arguments") - sys.exit(0) -else: - print(f"FAIL: Expected at least 2 callbacks, got {call_count}") - sys.exit(1)