Compare commits
7 commits
65b5ecc5c7
...
a1b692bb1f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1b692bb1f | ||
|
|
23afae69ad | ||
|
|
be450286f8 | ||
|
|
bb86cece2b | ||
|
|
4528ece0a7 | ||
|
|
f063d0af0c | ||
|
|
4579be2791 |
46 changed files with 4183 additions and 1151 deletions
240
explanation/headless-automation.md
Normal file
240
explanation/headless-automation.md
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
# 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
|
||||
134
tests/KNOWN_ISSUES.md
Normal file
134
tests/KNOWN_ISSUES.md
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# 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
|
||||
190
tests/conftest.py
Normal file
190
tests/conftest.py
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
"""
|
||||
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
|
||||
495
tests/demo/cookbook_showcase.py
Normal file
495
tests/demo/cookbook_showcase.py
Normal file
|
|
@ -0,0 +1,495 @@
|
|||
#!/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()
|
||||
255
tests/demo/new_features_showcase.py
Normal file
255
tests/demo/new_features_showcase.py
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
#!/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()
|
||||
286
tests/demo/procgen_showcase.py
Normal file
286
tests/demo/procgen_showcase.py
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
#!/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)
|
||||
103
tests/demo/simple_showcase.py
Normal file
103
tests/demo/simple_showcase.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#!/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()
|
||||
169
tests/demo/tutorial_screenshots.py
Normal file
169
tests/demo/tutorial_screenshots.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
#!/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()
|
||||
426
tests/demo/tutorial_showcase.py
Normal file
426
tests/demo/tutorial_showcase.py
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
#!/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()
|
||||
129
tests/docs/API_FINDINGS.md
Normal file
129
tests/docs/API_FINDINGS.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# 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 |
|
||||
BIN
tests/docs/screenshots/features_animation.png
Normal file
BIN
tests/docs/screenshots/features_animation.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
tests/docs/screenshots/features_scenes.png
Normal file
BIN
tests/docs/screenshots/features_scenes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
tests/docs/screenshots/quickstart_entities.png
Normal file
BIN
tests/docs/screenshots/quickstart_entities.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
tests/docs/screenshots/quickstart_main_menu.png
Normal file
BIN
tests/docs/screenshots/quickstart_main_menu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
BIN
tests/docs/screenshots/quickstart_simple_scene.png
Normal file
BIN
tests/docs/screenshots/quickstart_simple_scene.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
tests/docs/screenshots/quickstart_sprites.png
Normal file
BIN
tests/docs/screenshots/quickstart_sprites.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
18
tests/docs/test_current_scene.py
Normal file
18
tests/docs/test_current_scene.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#!/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)
|
||||
30
tests/docs/test_defaults.py
Normal file
30
tests/docs/test_defaults.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#!/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)
|
||||
30
tests/docs/test_entity_api.py
Normal file
30
tests/docs/test_entity_api.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#!/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)
|
||||
86
tests/docs/test_features_animation.py
Normal file
86
tests/docs/test_features_animation.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
#!/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)
|
||||
84
tests/docs/test_features_scenes.py
Normal file
84
tests/docs/test_features_scenes.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
#!/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)
|
||||
70
tests/docs/test_quickstart_entities.py
Normal file
70
tests/docs/test_quickstart_entities.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
#!/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)
|
||||
74
tests/docs/test_quickstart_main_menu.py
Normal file
74
tests/docs/test_quickstart_main_menu.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
#!/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)
|
||||
48
tests/docs/test_quickstart_simple_scene.py
Normal file
48
tests/docs/test_quickstart_simple_scene.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#!/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)
|
||||
49
tests/docs/test_quickstart_sprites.py
Normal file
49
tests/docs/test_quickstart_sprites.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#!/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)
|
||||
14
tests/pytest.ini
Normal file
14
tests/pytest.ini
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[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
|
||||
|
|
@ -7,6 +7,8 @@ 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
|
||||
|
|
@ -18,8 +20,9 @@ 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"
|
||||
TIMEOUT = 10 # seconds per test
|
||||
DEFAULT_TIMEOUT = 10 # seconds per test
|
||||
|
||||
# Test directories to run (in order)
|
||||
TEST_DIRS = ['unit', 'integration', 'regression']
|
||||
|
|
@ -39,7 +42,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):
|
||||
def run_test(test_path, verbose=False, timeout=DEFAULT_TIMEOUT):
|
||||
"""Run a single test and return (passed, duration, output)."""
|
||||
start = time.time()
|
||||
|
||||
|
|
@ -47,13 +50,19 @@ def run_test(test_path, verbose=False):
|
|||
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)
|
||||
timeout=timeout,
|
||||
cwd=str(BUILD_DIR),
|
||||
env=env
|
||||
)
|
||||
duration = time.time() - start
|
||||
passed = result.returncode == 0
|
||||
|
|
@ -66,7 +75,7 @@ def run_test(test_path, verbose=False):
|
|||
return passed, duration, output
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, TIMEOUT, "TIMEOUT"
|
||||
return False, timeout, "TIMEOUT"
|
||||
except Exception as e:
|
||||
return False, 0, str(e)
|
||||
|
||||
|
|
@ -79,6 +88,16 @@ 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 = []
|
||||
|
|
@ -89,7 +108,7 @@ def main():
|
|||
dirs_to_test = TEST_DIRS
|
||||
|
||||
print(f"{BOLD}McRogueFace Test Runner{RESET}")
|
||||
print(f"Testing: {', '.join(dirs_to_test)}")
|
||||
print(f"Testing: {', '.join(dirs_to_test)} (timeout: {timeout}s)")
|
||||
print("=" * 60)
|
||||
|
||||
results = {'pass': 0, 'fail': 0, 'total_time': 0}
|
||||
|
|
@ -104,7 +123,7 @@ def main():
|
|||
|
||||
for test_path in tests:
|
||||
test_name = test_path.name
|
||||
passed, duration, output = run_test(test_path, verbose)
|
||||
passed, duration, output = run_test(test_path, verbose, timeout)
|
||||
results['total_time'] += duration
|
||||
|
||||
if passed:
|
||||
|
|
@ -115,11 +134,12 @@ def main():
|
|||
status = f"{RED}FAIL{RESET}"
|
||||
failures.append((test_dir, test_name, output))
|
||||
|
||||
# Get screenshot checksums if any were generated
|
||||
checksums = get_screenshot_checksum(BUILD_DIR)
|
||||
# Get screenshot checksums if any were generated (skip in quiet mode)
|
||||
checksum_str = ""
|
||||
if checksums:
|
||||
checksum_str = f" [{', '.join(f'{k}:{v}' for k,v in checksums.items())}]"
|
||||
if not quiet:
|
||||
checksums = get_screenshot_checksum(BUILD_DIR)
|
||||
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}")
|
||||
|
||||
|
|
|
|||
116
tests/test_mcrogueface.py
Normal file
116
tests/test_mcrogueface.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""
|
||||
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))
|
||||
|
|
@ -1,47 +1,11 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Example of CORRECT test pattern using timer callbacks for automation"""
|
||||
"""Example of CORRECT test pattern using mcrfpy.step() for automation
|
||||
Refactored from timer-based approach to synchronous step() pattern.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
from datetime import datetime
|
||||
|
||||
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)
|
||||
import sys
|
||||
|
||||
# This code runs during --exec script execution
|
||||
print("=== Setting Up Test Scene ===")
|
||||
|
|
@ -49,6 +13,8 @@ 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
|
||||
|
|
@ -60,23 +26,57 @@ ui.append(frame)
|
|||
|
||||
# Add text
|
||||
caption = mcrfpy.Caption(pos=(150, 150),
|
||||
text="TIMER TEST - SHOULD BE VISIBLE",
|
||||
text="STEP 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. Setting timer for automation tests...")
|
||||
print("Scene setup complete.")
|
||||
|
||||
# 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)
|
||||
# Step to render the scene
|
||||
mcrfpy.step(0.1)
|
||||
|
||||
print("Timer set. Game loop will start after this script completes.")
|
||||
print("Automation tests will run 1 second later when content is visible.")
|
||||
print("\n=== Automation Test Running ===")
|
||||
|
||||
# Script ends here - game loop starts next
|
||||
# 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)
|
||||
|
|
|
|||
|
|
@ -46,10 +46,12 @@ print("Test 3: Checking margin properties...")
|
|||
try:
|
||||
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
|
||||
|
||||
# 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}"
|
||||
# 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}"
|
||||
|
||||
# Set margins when no alignment
|
||||
frame.margin = 10.0
|
||||
|
|
|
|||
|
|
@ -1,28 +1,23 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Simplified test to verify timer-based screenshots work"""
|
||||
"""Test to verify timer-based screenshots work using mcrfpy.step() for synchronous execution"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
# Counter to track timer calls
|
||||
call_count = 0
|
||||
|
||||
def take_screenshot_and_exit(timer, runtime):
|
||||
"""Timer callback that takes screenshot then exits"""
|
||||
def take_screenshot(timer, runtime):
|
||||
"""Timer callback that takes screenshot"""
|
||||
global call_count
|
||||
call_count += 1
|
||||
|
||||
print(f"\nTimer callback fired! (call #{call_count})")
|
||||
print(f"Timer callback fired! (call #{call_count}, runtime={runtime})")
|
||||
|
||||
# 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")
|
||||
|
|
@ -35,6 +30,17 @@ frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200),
|
|||
ui.append(frame)
|
||||
|
||||
print("Setting timer to fire in 100ms...")
|
||||
mcrfpy.Timer("screenshot_timer", take_screenshot_and_exit, 100, once=True)
|
||||
timer = mcrfpy.Timer("screenshot_timer", take_screenshot, 100, once=True)
|
||||
print(f"Timer created: {timer}")
|
||||
|
||||
print("Setup complete. Game loop starting...")
|
||||
# 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)
|
||||
|
|
|
|||
|
|
@ -1,73 +1,55 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Simple test for animation callbacks - demonstrates basic usage"""
|
||||
"""Simple test for animation callbacks using mcrfpy.step() for synchronous execution"""
|
||||
|
||||
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
|
||||
|
||||
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 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)
|
||||
|
||||
# Create animation with callback
|
||||
print("Starting animation with callback...")
|
||||
anim = mcrfpy.Animation("x", 400.0, 1.0, "easeInOutQuad", callback=my_callback)
|
||||
anim.start(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)
|
||||
|
||||
# Schedule check after animation should complete
|
||||
mcrfpy.Timer("check", check_result, 1500, once=True)
|
||||
# Use mcrfpy.step() to advance past animation completion
|
||||
mcrfpy.step(1.5) # Advance 1.5 seconds - animation completes at 1.0s
|
||||
|
||||
def check_result(timer, runtime):
|
||||
"""Check if callback fired correctly"""
|
||||
global callback_count, callback_demo
|
||||
if callback_count != 1:
|
||||
print(f"FAIL: Expected 1 callback, got {callback_count}")
|
||||
sys.exit(1)
|
||||
print("SUCCESS: Callback fired exactly once!")
|
||||
|
||||
if callback_count == 1:
|
||||
print("SUCCESS: Callback fired exactly once!")
|
||||
# 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)
|
||||
|
||||
# Test 2: Animation without callback
|
||||
print("\nTesting animation without callback...")
|
||||
ui = callback_demo.children
|
||||
frame = ui[0]
|
||||
# Advance past second animation
|
||||
mcrfpy.step(0.7)
|
||||
|
||||
anim2 = mcrfpy.Animation("y", 300.0, 0.5, "linear")
|
||||
anim2.start(frame)
|
||||
if callback_count != 1:
|
||||
print(f"FAIL: Callback count changed to {callback_count}")
|
||||
sys.exit(1)
|
||||
|
||||
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()
|
||||
print("SUCCESS: No unexpected callbacks fired!")
|
||||
print("\nAnimation callback feature working correctly!")
|
||||
sys.exit(0)
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ def test_8_replace_completes_old():
|
|||
test_result("Replace completes old animation", False, str(e))
|
||||
|
||||
|
||||
def run_all_tests(timer, runtime):
|
||||
def run_all_tests():
|
||||
"""Run all property locking tests"""
|
||||
print("\nRunning Animation Property Locking Tests...")
|
||||
print("-" * 50)
|
||||
|
|
@ -245,5 +245,8 @@ def run_all_tests(timer, runtime):
|
|||
test = mcrfpy.Scene("test")
|
||||
test.activate()
|
||||
|
||||
# Start tests after a brief delay to allow scene to initialize
|
||||
mcrfpy.Timer("start", run_all_tests, 100, once=True)
|
||||
# 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()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"""
|
||||
Test the RAII AnimationManager implementation.
|
||||
This verifies that weak_ptr properly handles all crash scenarios.
|
||||
Uses mcrfpy.step() for synchronous test execution.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
|
@ -19,189 +20,14 @@ def test_result(name, passed, details=""):
|
|||
global tests_passed, tests_failed
|
||||
if passed:
|
||||
tests_passed += 1
|
||||
result = f"✓ {name}"
|
||||
result = f"PASS: {name}"
|
||||
else:
|
||||
tests_failed += 1
|
||||
result = f"✗ {name}: {details}"
|
||||
result = f"FAIL: {name}: {details}"
|
||||
print(result)
|
||||
test_results.append((name, passed, details))
|
||||
|
||||
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
|
||||
# Setup scene
|
||||
test = mcrfpy.Scene("test")
|
||||
test.activate()
|
||||
|
||||
|
|
@ -211,5 +37,125 @@ bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768))
|
|||
bg.fill_color = mcrfpy.Color(20, 20, 30)
|
||||
ui.append(bg)
|
||||
|
||||
# Start tests
|
||||
start_timer = mcrfpy.Timer("start", run_all_tests, 100, once=True)
|
||||
# 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)
|
||||
|
|
|
|||
|
|
@ -1,40 +1,14 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test if the crash is related to removing animated objects
|
||||
Test if the crash is related to removing animated objects.
|
||||
Uses mcrfpy.step() for synchronous test execution.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
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)
|
||||
print("Animation Removal Test")
|
||||
print("=" * 40)
|
||||
|
||||
# Create initial scene
|
||||
print("Creating scene...")
|
||||
|
|
@ -47,20 +21,61 @@ 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")
|
||||
|
||||
# Schedule the clear and recreate
|
||||
switch_timer = mcrfpy.Timer("switch", clear_and_recreate, 1000, once=True)
|
||||
# Let animations run a bit
|
||||
mcrfpy.step(0.5)
|
||||
|
||||
print("\nEntering game loop...")
|
||||
# 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)
|
||||
|
|
|
|||
|
|
@ -1,182 +1,181 @@
|
|||
#!/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
|
||||
|
||||
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
|
||||
# Initialize scene
|
||||
test = mcrfpy.Scene("test")
|
||||
test_timer = mcrfpy.Timer("test", test_color_helpers, 100, once=True)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,28 @@
|
|||
#!/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("Starting game loop in 100ms...")
|
||||
print("Advancing simulation with step()...")
|
||||
|
||||
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)
|
||||
# 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
|
||||
|
||||
mcrfpy.Timer("check", check_alive, 1000, once=True)
|
||||
print("If this crashes immediately, AnimationManager has an issue with empty state")
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,135 +1,118 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test UIFrame clipping functionality"""
|
||||
"""Test UIFrame clipping functionality
|
||||
Refactored to use mcrfpy.step() for synchronous execution.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
from mcrfpy import Color, Frame, Caption
|
||||
from mcrfpy import Color, Frame, Caption, automation
|
||||
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()
|
||||
test.on_key = handle_keypress
|
||||
test_clipping_timer = mcrfpy.Timer("test_clipping", test_clipping, 100, once=True)
|
||||
print("Test scheduled, running...")
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,105 +1,95 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Advanced test for UIFrame clipping with nested frames"""
|
||||
"""Advanced test for UIFrame clipping with nested frames
|
||||
Refactored to use mcrfpy.step() for synchronous execution.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
from mcrfpy import Color, Frame, Caption, Vector
|
||||
from mcrfpy import Color, Frame, Caption, Vector, automation
|
||||
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)
|
||||
|
||||
# Schedule the test
|
||||
test_nested_clipping_timer = mcrfpy.Timer("test_nested_clipping", test_nested_clipping, 100, once=True)
|
||||
print("Testing advanced UIFrame clipping with nested frames...")
|
||||
|
||||
print("Advanced test scheduled, running...")
|
||||
# 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)
|
||||
|
|
|
|||
|
|
@ -1,129 +1,125 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test Grid.children collection - Issue #132"""
|
||||
"""Test Grid.children collection - Issue #132
|
||||
Refactored to use mcrfpy.step() for synchronous execution.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
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
|
||||
print("Creating test scene...")
|
||||
test = mcrfpy.Scene("test")
|
||||
test.activate()
|
||||
mcrfpy.step(0.01) # Initialize
|
||||
|
||||
# Schedule test to run after game loop starts
|
||||
mcrfpy.Timer("test", run_test, 50, once=True)
|
||||
# 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)
|
||||
|
|
|
|||
|
|
@ -2,90 +2,94 @@
|
|||
"""
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
# Create a basic scene so the game can start
|
||||
# Initialize scene
|
||||
test = mcrfpy.Scene("test")
|
||||
test.activate()
|
||||
mcrfpy.step(0.01)
|
||||
|
||||
# Schedule the test to run after game initialization
|
||||
test_timer = mcrfpy.Timer("test", test_ui_constructors, 100, once=True)
|
||||
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")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("FAIL")
|
||||
sys.exit(1)
|
||||
|
|
|
|||
|
|
@ -1,57 +1,67 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Quick test of drawable properties"""
|
||||
"""Quick test of drawable properties
|
||||
Refactored to use mcrfpy.step() for synchronous execution.
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
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)
|
||||
|
||||
# Initialize scene
|
||||
test = mcrfpy.Scene("test")
|
||||
test_properties_timer = mcrfpy.Timer("test_properties", test_properties, 100, once=True)
|
||||
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")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\nFAIL")
|
||||
sys.exit(1)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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
|
||||
|
|
@ -16,136 +17,128 @@ 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"✗ {message}")
|
||||
test_results.append(f"x {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)
|
||||
|
||||
# Schedule tests to run after game loop starts
|
||||
test_timer = mcrfpy.Timer("test", run_tests, 100, once=True)
|
||||
print("\n=== Testing Python Object Cache ===")
|
||||
|
||||
print("Python object cache test initialized. Running tests...")
|
||||
# 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)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,32 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Very simple callback test"""
|
||||
"""Very simple callback test - refactored to use mcrfpy.step()"""
|
||||
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)
|
||||
mcrfpy.Timer("exit", lambda t, r: sys.exit(0), 200, once=True)
|
||||
|
||||
# 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)
|
||||
|
|
|
|||
|
|
@ -1,30 +1,32 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Simple test to isolate drawable issue"""
|
||||
"""Simple test to isolate drawable issue
|
||||
Refactored to use mcrfpy.step() for synchronous execution.
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
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)
|
||||
|
||||
# Initialize scene
|
||||
test = mcrfpy.Scene("test")
|
||||
simple_test_timer = mcrfpy.Timer("simple_test", simple_test, 100, once=True)
|
||||
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!")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
print("FAIL")
|
||||
sys.exit(1)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test timer callback arguments with new Timer API (#173)
|
||||
Uses mcrfpy.step() for synchronous test execution.
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
|
@ -14,9 +15,6 @@ 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")
|
||||
|
|
@ -25,3 +23,14 @@ 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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue