Compare commits

..

No commits in common. "a1b692bb1fb642d73da87c6fe58843a13590f65b" and "65b5ecc5c73bfa74de49eef9a42b61027f566a39" have entirely different histories.

46 changed files with 1152 additions and 4184 deletions

View file

@ -1,240 +0,0 @@
# Headless Mode and Automation
McRogueFace supports headless operation for automated testing, CI/CD pipelines, and programmatic control without a display.
## Running Headless
Launch with the `--headless` flag:
```bash
mcrogueface --headless --exec game.py
```
Or use xvfb for a virtual framebuffer (required for rendering):
```bash
xvfb-run -a -s "-screen 0 1024x768x24" mcrogueface --headless --exec game.py
```
## Time Control with mcrfpy.step()
In headless mode, you control time explicitly rather than waiting for real-time to pass. The `step()` function takes **seconds** as a float:
```python
import mcrfpy
# Advance simulation by 100ms
mcrfpy.step(0.1)
# Advance by 1 second
mcrfpy.step(1.0)
# Advance by 16ms (~60fps frame)
mcrfpy.step(0.016)
```
### Why This Matters
Traditional timer-based code waits for real time and the game loop:
```python
# OLD PATTERN - waits for game loop, subject to timeouts
def delayed_action(timer, runtime):
print("Action!")
sys.exit(0)
mcrfpy.Timer("delay", delayed_action, 500, once=True)
# Script ends, game loop runs, timer eventually fires
```
With `mcrfpy.step()`, you control the clock synchronously:
```python
# NEW PATTERN - instant, deterministic
mcrfpy.Timer("delay", delayed_action, 500, once=True)
mcrfpy.step(0.6) # Advance 600ms - timer fires during this call
```
### Timer Behavior
- Timers fire **once per `step()` call** if their interval has elapsed
- To fire a timer multiple times, call `step()` multiple times:
```python
count = 0
def tick(timer, runtime):
global count
count += 1
timer = mcrfpy.Timer("tick", tick, 100) # Fire every 100ms
# Each step() processes timers once
for i in range(5):
mcrfpy.step(0.1) # 100ms each
print(count) # 5
```
## Screenshots
The `automation.screenshot()` function captures the current frame:
```python
from mcrfpy import automation
# Capture screenshot (synchronous in headless mode)
result = automation.screenshot("output.png")
print(f"Screenshot saved: {result}")
```
**Key insight:** In headless mode, `screenshot()` is synchronous - no timer dance needed.
## Testing Patterns
### Synchronous Test Structure
```python
#!/usr/bin/env python3
import mcrfpy
import sys
# Setup scene
scene = mcrfpy.Scene("test")
scene.activate()
mcrfpy.step(0.1) # Initialize scene
# Create test objects
frame = mcrfpy.Frame(pos=(100, 100), size=(50, 50))
scene.children.append(frame)
# Verify state
if frame.x != 100:
print("FAIL: frame.x should be 100")
sys.exit(1)
print("PASS")
sys.exit(0)
```
### Testing Animations
```python
# Create frame and start animation
frame = mcrfpy.Frame(pos=(100, 100), size=(50, 50))
scene.children.append(frame)
anim = mcrfpy.Animation("x", 200.0, 1.0, "linear") # 1 second duration
anim.start(frame)
# Advance to midpoint (0.5 seconds)
mcrfpy.step(0.5)
# frame.x should be ~150 (halfway between 100 and 200)
# Advance to completion
mcrfpy.step(0.6) # Past the 1.0s duration
# frame.x should be 200
if frame.x == 200.0:
print("PASS: Animation completed")
sys.exit(0)
```
### Testing Timers
```python
callback_count = 0
def on_timer(timer, runtime):
"""Timer callbacks receive (timer_object, runtime_ms)"""
global callback_count
callback_count += 1
print(f"Timer fired! Count: {callback_count}, runtime: {runtime}ms")
# Create timer that fires every 100ms
timer = mcrfpy.Timer("test", on_timer, 100)
# Advance time - each step() can fire the timer once
mcrfpy.step(0.15) # First fire at ~100ms
mcrfpy.step(0.15) # Second fire at ~200ms
if callback_count >= 2:
print("PASS")
sys.exit(0)
```
### Testing with once=True Timers
```python
fired = False
def one_shot(timer, runtime):
global fired
fired = True
print(f"One-shot timer fired! once={timer.once}")
# Create one-shot timer
timer = mcrfpy.Timer("oneshot", one_shot, 100, once=True)
mcrfpy.step(0.15) # Should fire
mcrfpy.step(0.15) # Should NOT fire again
if fired:
print("PASS: One-shot timer worked")
```
## Pattern Comparison
| Aspect | Timer-based (old) | step()-based (new) |
|--------|-------------------|-------------------|
| Execution | Async (game loop) | Sync (immediate) |
| Timeout risk | High | None |
| Determinism | Variable | Consistent |
| Script flow | Callbacks | Linear |
## LLM Agent Integration
Headless mode enables AI agents to interact with McRogueFace programmatically:
1. **Observe**: Capture screenshots, read game state
2. **Decide**: Process with vision models or state analysis
3. **Act**: Send input commands, modify game state
4. **Verify**: Check results, capture new state
```python
from mcrfpy import automation
# Agent loop
while not game_over:
automation.screenshot("state.png")
action = agent.decide("state.png")
execute_action(action)
mcrfpy.step(0.1) # Let action take effect
```
## Best Practices
1. **Use `mcrfpy.step()`** instead of real-time waiting for all headless tests
2. **Initialize scenes** with a brief `step(0.1)` after `activate()`
3. **Be deterministic** - same inputs should produce same outputs
4. **Test incrementally** - advance time in small steps to catch intermediate states
5. **Use `sys.exit(0/1)`** for clear pass/fail signals to test runners
6. **Multiple `step()` calls** to fire repeating timers multiple times
## Running the Test Suite
```bash
# Quick run with pytest wrapper
pytest tests/ -q --mcrf-timeout=5
# Using the original runner with xvfb
xvfb-run -a python3 tests/run_tests.py -q
# Run specific test directly
xvfb-run -a mcrogueface --headless --exec tests/unit/test_animation.py
```
## Related Topics
- [Animation System](animation.md) - How animations work
- [Scene API](scene-api.md) - Managing scenes
- [Timer API](timer-api.md) - Timer details

View file

@ -1,134 +0,0 @@
# McRogueFace Test Suite - Known Issues
## Test Results Summary
As of 2026-01-14, with `--mcrf-timeout=5`:
- **120 passed** (67%)
- **59 failed** (33%)
- 40 timeout failures (tests requiring timers/animations)
- 19 actual failures (API changes, missing features, or bugs)
## Synchronous Testing with `mcrfpy.step()`
**RECOMMENDED:** Use `mcrfpy.step(t)` to advance simulation time synchronously instead of relying on Timer callbacks and the game loop. This eliminates timeout issues and makes tests deterministic.
### Old Pattern (Timer-based, async)
```python
# OLD: Requires game loop, subject to timeouts
def run_tests(timer, runtime):
# tests here
sys.exit(0)
mcrfpy.Timer("run", run_tests, 100, once=True)
# Script ends, game loop runs, timer eventually fires
```
### New Pattern (step-based, sync)
```python
# NEW: Synchronous, no timeouts
import mcrfpy
import sys
# Setup scene
scene = mcrfpy.Scene("test")
scene.activate()
mcrfpy.step(0.1) # Initialize scene
# Run tests directly
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
scene.children.append(frame)
# Start animation
anim = mcrfpy.Animation("x", 500.0, 1.0, "linear")
anim.start(frame)
# Advance simulation to complete animation
mcrfpy.step(1.5) # Advances 1.5 seconds synchronously
# Verify results
if frame.x == 500.0:
print("PASS")
sys.exit(0)
else:
print("FAIL")
sys.exit(1)
```
### Key Differences
| Aspect | Timer-based | step()-based |
|--------|-------------|--------------|
| Execution | Async (game loop) | Sync (immediate) |
| Timeout risk | High | None |
| Determinism | Variable | Consistent |
| Timer firing | Once per step() call | Per elapsed interval |
### Timer Behavior with `step()`
- Timers fire once per `step()` call if their interval has elapsed
- To fire a timer multiple times, call `step()` multiple times:
```python
# Timer fires every 100ms
timer = mcrfpy.Timer("tick", callback, 100)
# This fires the timer ~6 times
for i in range(6):
mcrfpy.step(0.1) # Each step processes timers once
```
## Refactored Tests
The following tests have been converted to use `mcrfpy.step()`:
- simple_timer_screenshot_test.py
- test_animation_callback_simple.py
- test_animation_property_locking.py
- test_animation_raii.py
- test_animation_removal.py
- test_timer_callback.py
- test_timer_once.py
- test_simple_callback.py
- test_empty_animation_manager.py
- test_frame_clipping.py
- test_frame_clipping_advanced.py
- test_grid_children.py
- test_color_helpers.py
- test_no_arg_constructors.py
- test_properties_quick.py
- test_simple_drawable.py
- test_python_object_cache.py
- WORKING_automation_test_example.py
## Remaining Timeout Failures
These tests still use Timer-based async patterns:
- benchmark_logging_test.py
- keypress_scene_validation_test.py
**Headless mode tests:**
- test_headless_detection.py
- test_headless_modes.py
## Running Tests
```bash
# Quick run (5s timeout, many timeouts expected)
pytest tests/ -q --mcrf-timeout=5
# Full run (30s timeout, should pass most timing tests)
pytest tests/ -q --mcrf-timeout=30
# Filter by pattern
pytest tests/ -k "bsp" -q
# Run original runner
python3 tests/run_tests.py -q
```
## Recommendations
1. **For CI:** Use `--mcrf-timeout=10` and accept ~30% timeout failures
2. **For local dev:** Use `--mcrf-timeout=30` for comprehensive testing
3. **For quick validation:** Use `-k "not animation and not timer"` to skip slow tests

View file

@ -1,190 +0,0 @@
"""
Pytest configuration for McRogueFace tests.
Provides fixtures for running McRogueFace scripts in headless mode.
Usage:
pytest tests/ -q # Run all tests quietly
pytest tests/ -k "bsp" # Run tests matching "bsp"
pytest tests/ -x # Stop on first failure
pytest tests/ --tb=short # Short tracebacks
"""
import os
import subprocess
import pytest
from pathlib import Path
# Paths
TESTS_DIR = Path(__file__).parent
BUILD_DIR = TESTS_DIR.parent / "build"
LIB_DIR = TESTS_DIR.parent / "__lib"
MCROGUEFACE = BUILD_DIR / "mcrogueface"
# Default timeout for tests (can be overridden with --timeout)
DEFAULT_TIMEOUT = 10
def pytest_addoption(parser):
"""Add custom command line options."""
parser.addoption(
"--mcrf-timeout",
action="store",
default=DEFAULT_TIMEOUT,
type=int,
help="Timeout in seconds for each McRogueFace test"
)
@pytest.fixture
def mcrf_timeout(request):
"""Get the configured timeout."""
return request.config.getoption("--mcrf-timeout")
@pytest.fixture
def mcrf_env():
"""Environment with LD_LIBRARY_PATH set for McRogueFace."""
env = os.environ.copy()
existing_ld = env.get('LD_LIBRARY_PATH', '')
env['LD_LIBRARY_PATH'] = f"{LIB_DIR}:{existing_ld}" if existing_ld else str(LIB_DIR)
return env
@pytest.fixture
def mcrf_exec(mcrf_env, mcrf_timeout):
"""
Fixture that returns a function to execute McRogueFace scripts.
Usage in tests:
def test_something(mcrf_exec):
passed, output = mcrf_exec("unit/my_test.py")
assert passed
"""
def _exec(script_path, timeout=None):
"""
Execute a McRogueFace script in headless mode.
Args:
script_path: Path relative to tests/ directory
timeout: Override default timeout
Returns:
(passed: bool, output: str)
"""
if timeout is None:
timeout = mcrf_timeout
full_path = TESTS_DIR / script_path
try:
result = subprocess.run(
[str(MCROGUEFACE), '--headless', '--exec', str(full_path)],
capture_output=True,
text=True,
timeout=timeout,
cwd=str(BUILD_DIR),
env=mcrf_env
)
output = result.stdout + result.stderr
passed = result.returncode == 0
# Check for PASS/FAIL in output
if 'FAIL' in output and 'PASS' not in output.split('FAIL')[-1]:
passed = False
return passed, output
except subprocess.TimeoutExpired:
return False, "TIMEOUT"
except Exception as e:
return False, str(e)
return _exec
def pytest_collect_file(parent, file_path):
"""Auto-discover McRogueFace test scripts."""
# Only collect from unit/, integration/, regression/ directories
try:
rel_path = file_path.relative_to(TESTS_DIR)
except ValueError:
return None
if rel_path.parts and rel_path.parts[0] in ('unit', 'integration', 'regression', 'docs', 'demo'):
if file_path.suffix == '.py' and file_path.name not in ('__init__.py', 'conftest.py'):
return McRFTestFile.from_parent(parent, path=file_path)
return None
def pytest_ignore_collect(collection_path, config):
"""Prevent pytest from trying to import test scripts as Python modules."""
try:
rel_path = collection_path.relative_to(TESTS_DIR)
if rel_path.parts and rel_path.parts[0] in ('unit', 'integration', 'regression', 'docs', 'demo'):
# Let our custom collector handle these, don't import them
return False # Don't ignore - we'll collect them our way
except ValueError:
pass
return None
class McRFTestFile(pytest.File):
"""Custom test file for McRogueFace scripts."""
def collect(self):
"""Yield a single test item for this script."""
yield McRFTestItem.from_parent(self, name=self.path.stem)
class McRFTestItem(pytest.Item):
"""Test item that runs a McRogueFace script."""
def runtest(self):
"""Run the McRogueFace script."""
timeout = self.config.getoption("--mcrf-timeout")
env = os.environ.copy()
existing_ld = env.get('LD_LIBRARY_PATH', '')
env['LD_LIBRARY_PATH'] = f"{LIB_DIR}:{existing_ld}" if existing_ld else str(LIB_DIR)
try:
result = subprocess.run(
[str(MCROGUEFACE), '--headless', '--exec', str(self.path)],
capture_output=True,
text=True,
timeout=timeout,
cwd=str(BUILD_DIR),
env=env
)
self.output = result.stdout + result.stderr
passed = result.returncode == 0
if 'FAIL' in self.output and 'PASS' not in self.output.split('FAIL')[-1]:
passed = False
if not passed:
raise McRFTestException(self.output)
except subprocess.TimeoutExpired:
self.output = "TIMEOUT"
raise McRFTestException("TIMEOUT")
def repr_failure(self, excinfo):
"""Format failure output."""
if isinstance(excinfo.value, McRFTestException):
output = str(excinfo.value)
# Show last 10 lines
lines = output.strip().split('\n')[-10:]
return '\n'.join(lines)
return super().repr_failure(excinfo)
def reportinfo(self):
"""Report test location."""
return self.path, None, f"mcrf:{self.path.relative_to(TESTS_DIR)}"
class McRFTestException(Exception):
"""Exception raised when a McRogueFace test fails."""
pass

View file

@ -1,495 +0,0 @@
#!/usr/bin/env python3
"""
Cookbook Screenshot Showcase - Visual examples for cookbook recipes!
Generates beautiful screenshots for cookbook pages.
Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/cookbook_showcase.py
In headless mode, automation.screenshot() is SYNCHRONOUS - no timer dance needed!
"""
import mcrfpy
from mcrfpy import automation
import sys
import os
# Output directory - in the docs site images folder
OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/cookbook"
# Tile sprites from the labeled tileset
TILES = {
'player_knight': 84,
'player_mage': 85,
'player_rogue': 86,
'player_warrior': 87,
'enemy_slime': 108,
'enemy_orc': 120,
'enemy_skeleton': 123,
'floor_stone': 42,
'wall_stone': 30,
'wall_brick': 14,
'torch': 72,
'chest_closed': 89,
'item_potion': 113,
}
def screenshot_health_bar():
"""Create a health bar showcase."""
scene = mcrfpy.Scene("health_bar")
# Dark background
bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600))
bg.fill_color = mcrfpy.Color(20, 20, 30)
scene.children.append(bg)
# Title
title = mcrfpy.Caption(text="Health Bar Recipe", pos=(50, 30))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 28
scene.children.append(title)
subtitle = mcrfpy.Caption(text="Nested frames for dynamic UI elements", pos=(50, 60))
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
subtitle.font_size = 16
scene.children.append(subtitle)
# Example health bars at different levels
y_start = 120
bar_configs = [
("Player - Full Health", 100, 100, mcrfpy.Color(50, 200, 50)),
("Player - Damaged", 65, 100, mcrfpy.Color(200, 200, 50)),
("Player - Critical", 20, 100, mcrfpy.Color(200, 50, 50)),
("Boss - 3/4 Health", 750, 1000, mcrfpy.Color(150, 50, 150)),
]
for i, (label, current, maximum, color) in enumerate(bar_configs):
y = y_start + i * 100
# Label
lbl = mcrfpy.Caption(text=label, pos=(50, y))
lbl.fill_color = mcrfpy.Color(220, 220, 220)
lbl.font_size = 18
scene.children.append(lbl)
# Background bar
bar_bg = mcrfpy.Frame(pos=(50, y + 30), size=(400, 30))
bar_bg.fill_color = mcrfpy.Color(40, 40, 50)
bar_bg.outline = 2
bar_bg.outline_color = mcrfpy.Color(80, 80, 100)
scene.children.append(bar_bg)
# Fill bar (scaled to current/maximum)
fill_width = int(400 * (current / maximum))
bar_fill = mcrfpy.Frame(pos=(50, y + 30), size=(fill_width, 30))
bar_fill.fill_color = color
scene.children.append(bar_fill)
# Text overlay
hp_text = mcrfpy.Caption(text=f"{current}/{maximum}", pos=(60, y + 35))
hp_text.fill_color = mcrfpy.Color(255, 255, 255)
hp_text.font_size = 16
scene.children.append(hp_text)
scene.activate()
output_path = os.path.join(OUTPUT_DIR, "ui_health_bar.png")
automation.screenshot(output_path)
print(f" -> {output_path}")
def screenshot_fog_of_war():
"""Create a fog of war showcase."""
scene = mcrfpy.Scene("fog_of_war")
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
grid = mcrfpy.Grid(
pos=(50, 80),
size=(700, 480),
grid_size=(16, 12),
texture=texture,
zoom=2.8
)
grid.fill_color = mcrfpy.Color(0, 0, 0) # Black for unknown areas
scene.children.append(grid)
# Title
title = mcrfpy.Caption(text="Fog of War Recipe", pos=(50, 20))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 28
scene.children.append(title)
subtitle = mcrfpy.Caption(text="Visible, discovered, and unknown areas", pos=(50, 50))
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
subtitle.font_size = 16
scene.children.append(subtitle)
# Fill floor
for y in range(12):
for x in range(16):
grid.at(x, y).tilesprite = TILES['floor_stone']
# Add walls
for x in range(16):
grid.at(x, 0).tilesprite = TILES['wall_stone']
grid.at(x, 11).tilesprite = TILES['wall_stone']
for y in range(12):
grid.at(0, y).tilesprite = TILES['wall_stone']
grid.at(15, y).tilesprite = TILES['wall_stone']
# Interior walls (to break LOS)
for y in range(3, 8):
grid.at(8, y).tilesprite = TILES['wall_brick']
# Player (mage with light)
player = mcrfpy.Entity(grid_pos=(4, 6), texture=texture, sprite_index=TILES['player_mage'])
grid.entities.append(player)
# Hidden enemies on the other side
enemy1 = mcrfpy.Entity(grid_pos=(12, 4), texture=texture, sprite_index=TILES['enemy_orc'])
grid.entities.append(enemy1)
enemy2 = mcrfpy.Entity(grid_pos=(13, 8), texture=texture, sprite_index=TILES['enemy_skeleton'])
grid.entities.append(enemy2)
# Torch in visible area
torch = mcrfpy.Entity(grid_pos=(2, 3), texture=texture, sprite_index=TILES['torch'])
grid.entities.append(torch)
grid.center = (4 * 16 + 8, 6 * 16 + 8)
scene.activate()
output_path = os.path.join(OUTPUT_DIR, "grid_fog_of_war.png")
automation.screenshot(output_path)
print(f" -> {output_path}")
def screenshot_combat_melee():
"""Create a melee combat showcase."""
scene = mcrfpy.Scene("combat_melee")
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
grid = mcrfpy.Grid(
pos=(50, 80),
size=(700, 480),
grid_size=(12, 9),
texture=texture,
zoom=3.5
)
grid.fill_color = mcrfpy.Color(20, 20, 30)
scene.children.append(grid)
# Title
title = mcrfpy.Caption(text="Melee Combat Recipe", pos=(50, 20))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 28
scene.children.append(title)
subtitle = mcrfpy.Caption(text="Bump-to-attack mechanics with damage calculation", pos=(50, 50))
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
subtitle.font_size = 16
scene.children.append(subtitle)
# Fill with dirt floor (battle arena feel)
for y in range(9):
for x in range(12):
grid.at(x, y).tilesprite = 50 # dirt
# Brick walls
for x in range(12):
grid.at(x, 0).tilesprite = TILES['wall_brick']
grid.at(x, 8).tilesprite = TILES['wall_brick']
for y in range(9):
grid.at(0, y).tilesprite = TILES['wall_brick']
grid.at(11, y).tilesprite = TILES['wall_brick']
# Player knight engaging orc!
player = mcrfpy.Entity(grid_pos=(4, 4), texture=texture, sprite_index=TILES['player_knight'])
grid.entities.append(player)
enemy = mcrfpy.Entity(grid_pos=(6, 4), texture=texture, sprite_index=TILES['enemy_orc'])
grid.entities.append(enemy)
# Fallen enemy (bones)
bones = mcrfpy.Entity(grid_pos=(8, 6), texture=texture, sprite_index=75) # bones
grid.entities.append(bones)
# Potion for healing
potion = mcrfpy.Entity(grid_pos=(3, 2), texture=texture, sprite_index=TILES['item_potion'])
grid.entities.append(potion)
grid.center = (5 * 16 + 8, 4 * 16 + 8)
# Combat log UI
log_frame = mcrfpy.Frame(pos=(50, 520), size=(700, 60))
log_frame.fill_color = mcrfpy.Color(30, 30, 40, 220)
log_frame.outline = 1
log_frame.outline_color = mcrfpy.Color(60, 60, 80)
scene.children.append(log_frame)
msg1 = mcrfpy.Caption(text="You hit the Orc for 8 damage!", pos=(10, 10))
msg1.fill_color = mcrfpy.Color(255, 200, 100)
msg1.font_size = 14
log_frame.children.append(msg1)
msg2 = mcrfpy.Caption(text="The Orc hits you for 4 damage!", pos=(10, 30))
msg2.fill_color = mcrfpy.Color(255, 100, 100)
msg2.font_size = 14
log_frame.children.append(msg2)
scene.activate()
output_path = os.path.join(OUTPUT_DIR, "combat_melee.png")
automation.screenshot(output_path)
print(f" -> {output_path}")
def screenshot_dungeon_generator():
"""Create a dungeon generator showcase."""
scene = mcrfpy.Scene("dungeon_gen")
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
grid = mcrfpy.Grid(
pos=(50, 80),
size=(700, 480),
grid_size=(24, 16),
texture=texture,
zoom=2.0
)
grid.fill_color = mcrfpy.Color(10, 10, 15)
scene.children.append(grid)
# Title
title = mcrfpy.Caption(text="Dungeon Generator Recipe", pos=(50, 20))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 28
scene.children.append(title)
subtitle = mcrfpy.Caption(text="Procedural rooms connected by corridors", pos=(50, 50))
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
subtitle.font_size = 16
scene.children.append(subtitle)
# Fill with walls
for y in range(16):
for x in range(24):
grid.at(x, y).tilesprite = TILES['wall_stone']
# Carve rooms
rooms = [
(2, 2, 6, 5), # Room 1
(10, 2, 7, 5), # Room 2
(18, 3, 5, 4), # Room 3
(2, 9, 5, 5), # Room 4
(10, 10, 6, 5), # Room 5
(18, 9, 5, 6), # Room 6
]
for rx, ry, rw, rh in rooms:
for y in range(ry, ry + rh):
for x in range(rx, rx + rw):
if x < 24 and y < 16:
grid.at(x, y).tilesprite = TILES['floor_stone']
# Carve corridors (horizontal and vertical)
# Room 1 to Room 2
for x in range(7, 11):
grid.at(x, 4).tilesprite = 50 # dirt corridor
# Room 2 to Room 3
for x in range(16, 19):
grid.at(x, 4).tilesprite = 50
# Room 1 to Room 4
for y in range(6, 10):
grid.at(4, y).tilesprite = 50
# Room 2 to Room 5
for y in range(6, 11):
grid.at(13, y).tilesprite = 50
# Room 3 to Room 6
for y in range(6, 10):
grid.at(20, y).tilesprite = 50
# Room 5 to Room 6
for x in range(15, 19):
grid.at(x, 12).tilesprite = 50
# Add player in first room
player = mcrfpy.Entity(grid_pos=(4, 4), texture=texture, sprite_index=TILES['player_knight'])
grid.entities.append(player)
# Add decorations
grid.entities.append(mcrfpy.Entity(grid_pos=(3, 3), texture=texture, sprite_index=TILES['torch']))
grid.entities.append(mcrfpy.Entity(grid_pos=(12, 4), texture=texture, sprite_index=TILES['torch']))
grid.entities.append(mcrfpy.Entity(grid_pos=(19, 11), texture=texture, sprite_index=TILES['chest_closed']))
grid.entities.append(mcrfpy.Entity(grid_pos=(13, 12), texture=texture, sprite_index=TILES['enemy_slime']))
grid.entities.append(mcrfpy.Entity(grid_pos=(20, 5), texture=texture, sprite_index=TILES['enemy_skeleton']))
grid.center = (12 * 16, 8 * 16)
scene.activate()
output_path = os.path.join(OUTPUT_DIR, "grid_dungeon_generator.png")
automation.screenshot(output_path)
print(f" -> {output_path}")
def screenshot_floating_text():
"""Create a floating text/damage numbers showcase."""
scene = mcrfpy.Scene("floating_text")
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
grid = mcrfpy.Grid(
pos=(50, 100),
size=(700, 420),
grid_size=(12, 8),
texture=texture,
zoom=3.5
)
grid.fill_color = mcrfpy.Color(20, 20, 30)
scene.children.append(grid)
# Title
title = mcrfpy.Caption(text="Floating Text Recipe", pos=(50, 20))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 28
scene.children.append(title)
subtitle = mcrfpy.Caption(text="Animated damage numbers and status messages", pos=(50, 50))
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
subtitle.font_size = 16
scene.children.append(subtitle)
# Fill floor
for y in range(8):
for x in range(12):
grid.at(x, y).tilesprite = TILES['floor_stone']
# Walls
for x in range(12):
grid.at(x, 0).tilesprite = TILES['wall_stone']
grid.at(x, 7).tilesprite = TILES['wall_stone']
for y in range(8):
grid.at(0, y).tilesprite = TILES['wall_stone']
grid.at(11, y).tilesprite = TILES['wall_stone']
# Player and enemy in combat
player = mcrfpy.Entity(grid_pos=(4, 4), texture=texture, sprite_index=TILES['player_warrior'])
grid.entities.append(player)
enemy = mcrfpy.Entity(grid_pos=(7, 4), texture=texture, sprite_index=TILES['enemy_orc'])
grid.entities.append(enemy)
grid.center = (5.5 * 16, 4 * 16)
# Floating damage numbers (as captions positioned over entities)
# These would normally animate upward
dmg1 = mcrfpy.Caption(text="-12", pos=(330, 240))
dmg1.fill_color = mcrfpy.Color(255, 80, 80)
dmg1.font_size = 24
scene.children.append(dmg1)
dmg2 = mcrfpy.Caption(text="-5", pos=(500, 260))
dmg2.fill_color = mcrfpy.Color(255, 100, 100)
dmg2.font_size = 20
scene.children.append(dmg2)
crit = mcrfpy.Caption(text="CRITICAL!", pos=(280, 200))
crit.fill_color = mcrfpy.Color(255, 200, 50)
crit.font_size = 18
scene.children.append(crit)
heal = mcrfpy.Caption(text="+8", pos=(320, 280))
heal.fill_color = mcrfpy.Color(100, 255, 100)
heal.font_size = 20
scene.children.append(heal)
scene.activate()
output_path = os.path.join(OUTPUT_DIR, "effects_floating_text.png")
automation.screenshot(output_path)
print(f" -> {output_path}")
def screenshot_message_log():
"""Create a message log showcase."""
scene = mcrfpy.Scene("message_log")
# Dark background
bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600))
bg.fill_color = mcrfpy.Color(20, 20, 30)
scene.children.append(bg)
# Title
title = mcrfpy.Caption(text="Message Log Recipe", pos=(50, 30))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 28
scene.children.append(title)
subtitle = mcrfpy.Caption(text="Scrollable combat and event messages", pos=(50, 60))
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
subtitle.font_size = 16
scene.children.append(subtitle)
# Message log frame
log_frame = mcrfpy.Frame(pos=(50, 100), size=(700, 400))
log_frame.fill_color = mcrfpy.Color(30, 30, 40)
log_frame.outline = 2
log_frame.outline_color = mcrfpy.Color(60, 60, 80)
scene.children.append(log_frame)
# Sample messages with colors
messages = [
("Welcome to the dungeon!", mcrfpy.Color(200, 200, 255)),
("You see a dark corridor ahead.", mcrfpy.Color(180, 180, 180)),
("A goblin appears!", mcrfpy.Color(255, 200, 100)),
("You hit the Goblin for 8 damage!", mcrfpy.Color(255, 255, 150)),
("The Goblin hits you for 3 damage!", mcrfpy.Color(255, 100, 100)),
("You hit the Goblin for 12 damage! Critical hit!", mcrfpy.Color(255, 200, 50)),
("The Goblin dies!", mcrfpy.Color(150, 255, 150)),
("You found a Healing Potion.", mcrfpy.Color(100, 200, 255)),
("An Orc blocks your path!", mcrfpy.Color(255, 150, 100)),
("You drink the Healing Potion. +15 HP", mcrfpy.Color(100, 255, 100)),
("You hit the Orc for 6 damage!", mcrfpy.Color(255, 255, 150)),
("The Orc hits you for 8 damage!", mcrfpy.Color(255, 100, 100)),
]
for i, (msg, color) in enumerate(messages):
caption = mcrfpy.Caption(text=msg, pos=(15, 15 + i * 30))
caption.fill_color = color
caption.font_size = 16
log_frame.children.append(caption)
# Scroll indicator
scroll = mcrfpy.Caption(text="▼ More messages below", pos=(580, 370))
scroll.fill_color = mcrfpy.Color(100, 100, 120)
scroll.font_size = 12
log_frame.children.append(scroll)
scene.activate()
output_path = os.path.join(OUTPUT_DIR, "ui_message_log.png")
automation.screenshot(output_path)
print(f" -> {output_path}")
def main():
"""Generate all cookbook screenshots!"""
os.makedirs(OUTPUT_DIR, exist_ok=True)
print("=== Cookbook Screenshot Showcase ===")
print(f"Output: {OUTPUT_DIR}\n")
showcases = [
('Health Bar UI', screenshot_health_bar),
('Fog of War', screenshot_fog_of_war),
('Melee Combat', screenshot_combat_melee),
('Dungeon Generator', screenshot_dungeon_generator),
('Floating Text', screenshot_floating_text),
('Message Log', screenshot_message_log),
]
for name, func in showcases:
print(f"Generating {name}...")
try:
func()
except Exception as e:
print(f" ERROR: {e}")
import traceback
traceback.print_exc()
print("\n=== All cookbook screenshots generated! ===")
sys.exit(0)
main()

View file

@ -1,255 +0,0 @@
#!/usr/bin/env python3
"""
New Features Screenshot Showcase - Alignment + Dijkstra-to-HeightMap
Generates screenshots for the new API cookbook recipes.
Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/new_features_showcase.py
"""
import mcrfpy
from mcrfpy import automation
import sys
import os
OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/cookbook"
def screenshot_alignment():
"""Create an alignment system showcase."""
scene = mcrfpy.Scene("alignment")
# Dark background
bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600))
bg.fill_color = mcrfpy.Color(20, 20, 30)
scene.children.append(bg)
# Title
title = mcrfpy.Caption(text="UI Alignment System", pos=(50, 20))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 28
scene.children.append(title)
subtitle = mcrfpy.Caption(text="Auto-positioning with reactive resize", pos=(50, 50))
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
subtitle.font_size = 16
scene.children.append(subtitle)
# Demo container
container = mcrfpy.Frame(pos=(100, 100), size=(600, 400))
container.fill_color = mcrfpy.Color(40, 40, 50)
container.outline = 2
container.outline_color = mcrfpy.Color(80, 80, 100)
scene.children.append(container)
# Container label
container_label = mcrfpy.Caption(text="Parent Container (600x400)", pos=(10, 10))
container_label.fill_color = mcrfpy.Color(100, 100, 120)
container_label.font_size = 12
container.children.append(container_label)
# 9 alignment positions demo
alignments = [
(mcrfpy.Alignment.TOP_LEFT, "TL", mcrfpy.Color(200, 80, 80)),
(mcrfpy.Alignment.TOP_CENTER, "TC", mcrfpy.Color(200, 150, 80)),
(mcrfpy.Alignment.TOP_RIGHT, "TR", mcrfpy.Color(200, 200, 80)),
(mcrfpy.Alignment.CENTER_LEFT, "CL", mcrfpy.Color(80, 200, 80)),
(mcrfpy.Alignment.CENTER, "C", mcrfpy.Color(80, 200, 200)),
(mcrfpy.Alignment.CENTER_RIGHT, "CR", mcrfpy.Color(80, 80, 200)),
(mcrfpy.Alignment.BOTTOM_LEFT, "BL", mcrfpy.Color(150, 80, 200)),
(mcrfpy.Alignment.BOTTOM_CENTER, "BC", mcrfpy.Color(200, 80, 200)),
(mcrfpy.Alignment.BOTTOM_RIGHT, "BR", mcrfpy.Color(200, 80, 150)),
]
for align, label, color in alignments:
box = mcrfpy.Frame(pos=(0, 0), size=(60, 40))
box.fill_color = color
box.outline = 1
box.outline_color = mcrfpy.Color(255, 255, 255)
box.align = align
if align != mcrfpy.Alignment.CENTER:
box.margin = 15.0
# Label inside box
text = mcrfpy.Caption(text=label, pos=(0, 0))
text.fill_color = mcrfpy.Color(255, 255, 255)
text.font_size = 16
text.align = mcrfpy.Alignment.CENTER
box.children.append(text)
container.children.append(box)
# Legend
legend = mcrfpy.Caption(text="TL=TOP_LEFT TC=TOP_CENTER TR=TOP_RIGHT etc.", pos=(100, 520))
legend.fill_color = mcrfpy.Color(150, 150, 170)
legend.font_size = 14
scene.children.append(legend)
legend2 = mcrfpy.Caption(text="All boxes have margin=15 except CENTER", pos=(100, 545))
legend2.fill_color = mcrfpy.Color(150, 150, 170)
legend2.font_size = 14
scene.children.append(legend2)
scene.activate()
output_path = os.path.join(OUTPUT_DIR, "ui_alignment.png")
automation.screenshot(output_path)
print(f" -> {output_path}")
def screenshot_dijkstra_heightmap():
"""Create a dijkstra-to-heightmap showcase."""
scene = mcrfpy.Scene("dijkstra_hmap")
# Title
title = mcrfpy.Caption(text="Dijkstra to HeightMap", pos=(50, 20))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 28
scene.children.append(title)
subtitle = mcrfpy.Caption(text="Distance-based gradients for fog, difficulty, and visualization", pos=(50, 50))
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
subtitle.font_size = 16
scene.children.append(subtitle)
# Create grid for dijkstra visualization
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
grid = mcrfpy.Grid(
pos=(50, 90),
size=(350, 350),
grid_size=(16, 16),
texture=texture,
zoom=1.3
)
grid.fill_color = mcrfpy.Color(20, 20, 30)
scene.children.append(grid)
# Initialize grid
for y in range(16):
for x in range(16):
grid.at((x, y)).walkable = True
grid.at((x, y)).tilesprite = 42 # floor
# Add some walls
for i in range(5, 11):
grid.at((i, 5)).walkable = False
grid.at((i, 5)).tilesprite = 30 # wall
grid.at((5, i)).walkable = False
grid.at((5, i)).tilesprite = 30
# Player at center
player = mcrfpy.Entity(grid_pos=(8, 8), texture=texture, sprite_index=84)
grid.entities.append(player)
# Get dijkstra and create color visualization
dijkstra = grid.get_dijkstra_map((8, 8))
hmap = dijkstra.to_heightmap(unreachable=-1.0)
# Find max for normalization
max_dist = 0
for y in range(16):
for x in range(16):
d = hmap[(x, y)]
if d > max_dist and d >= 0:
max_dist = d
# Second visualization panel - color gradient
viz_frame = mcrfpy.Frame(pos=(420, 90), size=(350, 350))
viz_frame.fill_color = mcrfpy.Color(30, 30, 40)
viz_frame.outline = 2
viz_frame.outline_color = mcrfpy.Color(60, 60, 80)
scene.children.append(viz_frame)
viz_label = mcrfpy.Caption(text="Distance Visualization", pos=(80, 10))
viz_label.fill_color = mcrfpy.Color(200, 200, 220)
viz_label.font_size = 16
viz_frame.children.append(viz_label)
# Draw colored squares for each cell
cell_size = 20
offset_x = 15
offset_y = 35
for y in range(16):
for x in range(16):
dist = hmap[(x, y)]
if dist < 0:
# Unreachable - dark red
color = mcrfpy.Color(60, 0, 0)
elif dist == 0:
# Source - bright yellow
color = mcrfpy.Color(255, 255, 0)
else:
# Gradient: green (near) to blue (far)
t = min(1.0, dist / max_dist)
r = 0
g = int(200 * (1 - t))
b = int(200 * t)
color = mcrfpy.Color(r, g, b)
cell = mcrfpy.Frame(
pos=(offset_x + x * cell_size, offset_y + y * cell_size),
size=(cell_size - 1, cell_size - 1)
)
cell.fill_color = color
viz_frame.children.append(cell)
# Legend
legend_frame = mcrfpy.Frame(pos=(50, 460), size=(720, 100))
legend_frame.fill_color = mcrfpy.Color(30, 30, 40)
legend_frame.outline = 1
legend_frame.outline_color = mcrfpy.Color(60, 60, 80)
scene.children.append(legend_frame)
leg1 = mcrfpy.Caption(text="Use Cases:", pos=(15, 10))
leg1.fill_color = mcrfpy.Color(255, 255, 255)
leg1.font_size = 16
legend_frame.children.append(leg1)
uses = [
"Distance-based enemy difficulty",
"Fog intensity gradients",
"Pathfinding visualization",
"Influence maps for AI",
]
for i, use in enumerate(uses):
txt = mcrfpy.Caption(text=f"- {use}", pos=(15 + (i // 2) * 350, 35 + (i % 2) * 25))
txt.fill_color = mcrfpy.Color(180, 180, 200)
txt.font_size = 14
legend_frame.children.append(txt)
# Color key
key_label = mcrfpy.Caption(text="Yellow=Source Green=Near Blue=Far Red=Blocked", pos=(420, 450))
key_label.fill_color = mcrfpy.Color(150, 150, 170)
key_label.font_size = 12
scene.children.append(key_label)
scene.activate()
output_path = os.path.join(OUTPUT_DIR, "grid_dijkstra_heightmap.png")
automation.screenshot(output_path)
print(f" -> {output_path}")
def main():
os.makedirs(OUTPUT_DIR, exist_ok=True)
print("=== New Features Screenshot Showcase ===")
print(f"Output: {OUTPUT_DIR}\n")
showcases = [
('Alignment System', screenshot_alignment),
('Dijkstra to HeightMap', screenshot_dijkstra_heightmap),
]
for name, func in showcases:
print(f"Generating {name}...")
try:
func()
except Exception as e:
print(f" ERROR: {e}")
import traceback
traceback.print_exc()
print("\n=== New feature screenshots generated! ===")
sys.exit(0)
main()

View file

@ -1,286 +0,0 @@
#!/usr/bin/env python3
"""Generate screenshots for procgen cookbook recipes.
Uses Frame-based visualization since Grid cell colors use ColorLayer API.
"""
import mcrfpy
from mcrfpy import automation
import sys
OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/cookbook"
# Simple PRNG
_seed = 42
def random():
global _seed
_seed = (_seed * 1103515245 + 12345) & 0x7fffffff
return (_seed >> 16) / 32768.0
def seed(n):
global _seed
_seed = n
def choice(lst):
return lst[int(random() * len(lst))]
def screenshot_cellular_caves():
"""Generate cellular automata caves visualization."""
print("Generating cellular automata caves...")
scene = mcrfpy.Scene("caves")
scene.activate()
mcrfpy.step(0.1)
# Background
bg = mcrfpy.Frame(pos=(0, 0), size=(640, 500))
bg.fill_color = mcrfpy.Color(15, 15, 25)
scene.children.append(bg)
width, height = 50, 35
cell_size = 12
seed(42)
# Store cell data
cells = [[False for _ in range(width)] for _ in range(height)]
# Step 1: Random noise (45% walls)
for y in range(height):
for x in range(width):
if x == 0 or x == width-1 or y == 0 or y == height-1:
cells[y][x] = True # Border walls
else:
cells[y][x] = random() < 0.45
# Step 2: Smooth with cellular automata (5 iterations)
for _ in range(5):
new_cells = [[cells[y][x] for x in range(width)] for y in range(height)]
for y in range(1, height - 1):
for x in range(1, width - 1):
wall_count = sum(
1 for dy in [-1, 0, 1] for dx in [-1, 0, 1]
if not (dx == 0 and dy == 0) and cells[y + dy][x + dx]
)
if wall_count >= 5:
new_cells[y][x] = True
elif wall_count <= 3:
new_cells[y][x] = False
cells = new_cells
# Find largest connected region
visited = set()
regions = []
def flood_fill(start_x, start_y):
result = []
stack = [(start_x, start_y)]
while stack:
x, y = stack.pop()
if (x, y) in visited or x < 0 or x >= width or y < 0 or y >= height:
continue
if cells[y][x]: # Wall
continue
visited.add((x, y))
result.append((x, y))
stack.extend([(x+1, y), (x-1, y), (x, y+1), (x, y-1)])
return result
for y in range(height):
for x in range(width):
if (x, y) not in visited and not cells[y][x]:
region = flood_fill(x, y)
if region:
regions.append(region)
largest = max(regions, key=len) if regions else []
largest_set = set(largest)
# Draw cells as colored frames
for y in range(height):
for x in range(width):
px = 20 + x * cell_size
py = 20 + y * cell_size
cell = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1))
if cells[y][x]:
cell.fill_color = mcrfpy.Color(60, 40, 30) # Wall
elif (x, y) in largest_set:
cell.fill_color = mcrfpy.Color(50, 90, 100) # Main cave
else:
cell.fill_color = mcrfpy.Color(45, 35, 30) # Filled region
scene.children.append(cell)
# Title
title = mcrfpy.Caption(text="Cellular Automata Caves", pos=(20, 445))
title.fill_color = mcrfpy.Color(200, 200, 200)
title.font_size = 18
scene.children.append(title)
subtitle = mcrfpy.Caption(text="45% fill, 5 iterations, largest region preserved", pos=(20, 468))
subtitle.fill_color = mcrfpy.Color(130, 130, 140)
subtitle.font_size = 12
scene.children.append(subtitle)
mcrfpy.step(0.1)
automation.screenshot(OUTPUT_DIR + "/procgen_cellular_caves.png")
print("Saved: procgen_cellular_caves.png")
def screenshot_wfc():
"""Generate WFC pattern visualization."""
print("Generating WFC patterns...")
scene = mcrfpy.Scene("wfc")
scene.activate()
mcrfpy.step(0.1)
# Background
bg = mcrfpy.Frame(pos=(0, 0), size=(640, 500))
bg.fill_color = mcrfpy.Color(15, 20, 15)
scene.children.append(bg)
width, height = 40, 28
cell_size = 15
seed(123)
GRASS, DIRT, WATER, SAND = 0, 1, 2, 3
colors = {
GRASS: mcrfpy.Color(60, 120, 50),
DIRT: mcrfpy.Color(100, 70, 40),
WATER: mcrfpy.Color(40, 80, 140),
SAND: mcrfpy.Color(180, 160, 90)
}
rules = {
GRASS: {'N': [GRASS, DIRT, SAND], 'S': [GRASS, DIRT, SAND],
'E': [GRASS, DIRT, SAND], 'W': [GRASS, DIRT, SAND]},
DIRT: {'N': [GRASS, DIRT], 'S': [GRASS, DIRT],
'E': [GRASS, DIRT], 'W': [GRASS, DIRT]},
WATER: {'N': [WATER, SAND], 'S': [WATER, SAND],
'E': [WATER, SAND], 'W': [WATER, SAND]},
SAND: {'N': [GRASS, WATER, SAND], 'S': [GRASS, WATER, SAND],
'E': [GRASS, WATER, SAND], 'W': [GRASS, WATER, SAND]}
}
tiles = set(rules.keys())
possibilities = {(x, y): set(tiles) for y in range(height) for x in range(width)}
result = {}
# Seed water lake
for x in range(22, 32):
for y in range(8, 18):
possibilities[(x, y)] = {WATER}
result[(x, y)] = WATER
# Seed dirt path
for y in range(10, 18):
possibilities[(3, y)] = {DIRT}
result[(3, y)] = DIRT
directions = {'N': (0, -1), 'S': (0, 1), 'E': (1, 0), 'W': (-1, 0)}
def propagate(sx, sy):
stack = [(sx, sy)]
while stack:
x, y = stack.pop()
current = possibilities[(x, y)]
for dir_name, (dx, dy) in directions.items():
nx, ny = x + dx, y + dy
if not (0 <= nx < width and 0 <= ny < height):
continue
neighbor = possibilities[(nx, ny)]
if len(neighbor) == 1:
continue
allowed = set()
for tile in current:
if dir_name in rules[tile]:
allowed.update(rules[tile][dir_name])
new_opts = neighbor & allowed
if new_opts and new_opts != neighbor:
possibilities[(nx, ny)] = new_opts
stack.append((nx, ny))
# Propagate from seeds
for x in range(22, 32):
for y in range(8, 18):
propagate(x, y)
for y in range(10, 18):
propagate(3, y)
# Collapse
for _ in range(width * height):
best, best_e = None, 1000.0
for pos, opts in possibilities.items():
if len(opts) > 1:
e = len(opts) + random() * 0.1
if e < best_e:
best_e, best = e, pos
if best is None:
break
x, y = best
opts = list(possibilities[(x, y)])
if not opts:
break
weights = {GRASS: 5, DIRT: 2, WATER: 1, SAND: 2}
weighted = []
for t in opts:
weighted.extend([t] * weights.get(t, 1))
chosen = choice(weighted) if weighted else GRASS
possibilities[(x, y)] = {chosen}
result[(x, y)] = chosen
propagate(x, y)
# Fill remaining
for y in range(height):
for x in range(width):
if (x, y) not in result:
opts = list(possibilities[(x, y)])
result[(x, y)] = choice(opts) if opts else GRASS
# Draw
for y in range(height):
for x in range(width):
px = 20 + x * cell_size
py = 20 + y * cell_size
cell = mcrfpy.Frame(pos=(px, py), size=(cell_size - 1, cell_size - 1))
cell.fill_color = colors[result[(x, y)]]
scene.children.append(cell)
# Title
title = mcrfpy.Caption(text="Wave Function Collapse", pos=(20, 445))
title.fill_color = mcrfpy.Color(200, 200, 200)
title.font_size = 18
scene.children.append(title)
subtitle = mcrfpy.Caption(text="Constraint-based terrain (seeded lake + path)", pos=(20, 468))
subtitle.fill_color = mcrfpy.Color(130, 140, 130)
subtitle.font_size = 12
scene.children.append(subtitle)
# Legend
for i, (name, tid) in enumerate([("Grass", GRASS), ("Dirt", DIRT), ("Sand", SAND), ("Water", WATER)]):
lx, ly = 480, 445 + i * 14
swatch = mcrfpy.Frame(pos=(lx, ly), size=(12, 12))
swatch.fill_color = colors[tid]
scene.children.append(swatch)
label = mcrfpy.Caption(text=name, pos=(lx + 16, ly))
label.fill_color = mcrfpy.Color(150, 150, 150)
label.font_size = 11
scene.children.append(label)
mcrfpy.step(0.1)
automation.screenshot(OUTPUT_DIR + "/procgen_wfc.png")
print("Saved: procgen_wfc.png")
if __name__ == "__main__":
screenshot_cellular_caves()
screenshot_wfc()
print("\nDone!")
sys.exit(0)

View file

@ -1,103 +0,0 @@
#!/usr/bin/env python3
"""
Simple Tutorial Screenshot Generator
This creates ONE screenshot - the part01 tutorial showcase.
Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/simple_showcase.py
NOTE: In headless mode, automation.screenshot() is SYNCHRONOUS - it renders
and captures immediately. No timer dance needed!
"""
import mcrfpy
from mcrfpy import automation
import sys
import os
# Output
OUTPUT_PATH = "/opt/goblincorps/repos/mcrogueface.github.io/images/tutorials/part_01_grid_movement.png"
# Tile sprites from the labeled tileset
PLAYER_KNIGHT = 84
FLOOR_STONE = 42
WALL_STONE = 30
TORCH = 72
BARREL = 73
SKULL = 74
def main():
"""Create the part01 showcase screenshot."""
# Ensure output dir exists
os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True)
# Create scene
scene = mcrfpy.Scene("showcase")
# Load texture
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create grid - bigger zoom for visibility
grid = mcrfpy.Grid(
pos=(50, 80),
size=(700, 480),
grid_size=(12, 9),
texture=texture,
zoom=3.5
)
grid.fill_color = mcrfpy.Color(20, 20, 30)
scene.children.append(grid)
# Fill with floor
for y in range(9):
for x in range(12):
grid.at(x, y).tilesprite = FLOOR_STONE
# Add wall border
for x in range(12):
grid.at(x, 0).tilesprite = WALL_STONE
grid.at(x, 0).walkable = False
grid.at(x, 8).tilesprite = WALL_STONE
grid.at(x, 8).walkable = False
for y in range(9):
grid.at(0, y).tilesprite = WALL_STONE
grid.at(0, y).walkable = False
grid.at(11, y).tilesprite = WALL_STONE
grid.at(11, y).walkable = False
# Add player entity - a knight!
player = mcrfpy.Entity(
grid_pos=(6, 4),
texture=texture,
sprite_index=PLAYER_KNIGHT
)
grid.entities.append(player)
# Add decorations
for pos, sprite in [((2, 2), TORCH), ((9, 2), TORCH), ((2, 6), BARREL), ((9, 6), SKULL)]:
entity = mcrfpy.Entity(grid_pos=pos, texture=texture, sprite_index=sprite)
grid.entities.append(entity)
# Center camera on player
grid.center = (6 * 16 + 8, 4 * 16 + 8)
# Add title
title = mcrfpy.Caption(text="Part 1: The '@' and the Dungeon Grid", pos=(50, 20))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 28
scene.children.append(title)
subtitle = mcrfpy.Caption(text="Creating a grid, placing entities, handling input", pos=(50, 50))
subtitle.fill_color = mcrfpy.Color(180, 180, 200)
subtitle.font_size = 16
scene.children.append(subtitle)
# Activate scene
scene.activate()
# In headless mode, screenshot() is synchronous - renders then captures!
result = automation.screenshot(OUTPUT_PATH)
print(f"Screenshot saved: {OUTPUT_PATH} (result: {result})")
sys.exit(0)
# Run it
main()

View file

@ -1,169 +0,0 @@
#!/usr/bin/env python3
"""
Tutorial Screenshot Generator
Usage:
./mcrogueface --headless --exec tests/demo/tutorial_screenshots.py
Extracts code from tutorial markdown files and generates screenshots.
"""
import mcrfpy
from mcrfpy import automation
import sys
import os
import re
# Paths
DOCS_REPO = "/opt/goblincorps/repos/mcrogueface.github.io"
TUTORIAL_DIR = os.path.join(DOCS_REPO, "tutorial")
OUTPUT_DIR = os.path.join(DOCS_REPO, "images", "tutorials")
# Tutorials to process (in order)
TUTORIALS = [
"part_01_grid_movement.md",
"part_02_tiles_collision.md",
"part_03_dungeon_generation.md",
"part_04_fov.md",
"part_05_enemies.md",
"part_06_combat.md",
"part_07_ui.md",
]
def extract_code_from_markdown(filepath):
"""Extract the main Python code block from a tutorial markdown file."""
with open(filepath, 'r') as f:
content = f.read()
# Find code blocks after "## The Complete Code" header
# Look for the first python code block after that header
complete_code_match = re.search(
r'##\s+The Complete Code.*?```python\s*\n(.*?)```',
content,
re.DOTALL | re.IGNORECASE
)
if complete_code_match:
return complete_code_match.group(1)
# Fallback: just get the first large python code block
code_blocks = re.findall(r'```python\s*\n(.*?)```', content, re.DOTALL)
if code_blocks:
# Return the largest code block (likely the main example)
return max(code_blocks, key=len)
return None
def add_screenshot_hook(code, screenshot_path):
"""Add screenshot capture code to the end of the script."""
# Add code to take screenshot after a brief delay
hook_code = f'''
# === Screenshot capture hook (added by tutorial_screenshots.py) ===
import mcrfpy
from mcrfpy import automation
import sys
_screenshot_taken = [False]
def _take_screenshot(timer, runtime):
if not _screenshot_taken[0]:
_screenshot_taken[0] = True
automation.screenshot("{screenshot_path}")
print(f"Screenshot saved: {screenshot_path}")
sys.exit(0)
# Wait a moment for scene to render, then capture
mcrfpy.Timer("_screenshot_hook", _take_screenshot, 200)
'''
return code + hook_code
class TutorialScreenshotter:
"""Manages tutorial screenshot generation."""
def __init__(self):
self.tutorials = []
self.current_index = 0
def load_tutorials(self):
"""Load and parse all tutorial files."""
for filename in TUTORIALS:
filepath = os.path.join(TUTORIAL_DIR, filename)
if not os.path.exists(filepath):
print(f"Warning: {filepath} not found, skipping")
continue
code = extract_code_from_markdown(filepath)
if code:
# Generate output filename
base = os.path.splitext(filename)[0]
screenshot_name = f"{base}.png"
self.tutorials.append({
'name': filename,
'code': code,
'screenshot': screenshot_name,
'filepath': filepath,
})
print(f"Loaded: {filename}")
else:
print(f"Warning: No code found in {filename}")
def run(self):
"""Generate all screenshots."""
# Ensure output directory exists
os.makedirs(OUTPUT_DIR, exist_ok=True)
print(f"\nGenerating {len(self.tutorials)} tutorial screenshots...")
print(f"Output directory: {OUTPUT_DIR}\n")
self.process_next()
def process_next(self):
"""Process the next tutorial."""
if self.current_index >= len(self.tutorials):
print("\nAll screenshots generated!")
sys.exit(0)
return
tutorial = self.tutorials[self.current_index]
print(f"[{self.current_index + 1}/{len(self.tutorials)}] Processing {tutorial['name']}...")
# Add screenshot hook to the code
screenshot_path = os.path.join(OUTPUT_DIR, tutorial['screenshot'])
modified_code = add_screenshot_hook(tutorial['code'], screenshot_path)
# Write to temp file and execute
temp_path = f"/tmp/tutorial_screenshot_{self.current_index}.py"
with open(temp_path, 'w') as f:
f.write(modified_code)
try:
# Execute the code
exec(compile(modified_code, temp_path, 'exec'), {'__name__': '__main__'})
except Exception as e:
print(f"Error processing {tutorial['name']}: {e}")
self.current_index += 1
self.process_next()
finally:
try:
os.unlink(temp_path)
except:
pass
def main():
"""Main entry point."""
screenshotter = TutorialScreenshotter()
screenshotter.load_tutorials()
if not screenshotter.tutorials:
print("No tutorials found to process!")
sys.exit(1)
screenshotter.run()
# Run when executed
main()

View file

@ -1,426 +0,0 @@
#!/usr/bin/env python3
"""
Tutorial Screenshot Showcase - ALL THE SCREENSHOTS!
Generates beautiful screenshots for all tutorial parts.
Run with: xvfb-run -a ./build/mcrogueface --headless --exec tests/demo/tutorial_showcase.py
In headless mode, automation.screenshot() is SYNCHRONOUS - no timer dance needed!
"""
import mcrfpy
from mcrfpy import automation
import sys
import os
# Output directory
OUTPUT_DIR = "/opt/goblincorps/repos/mcrogueface.github.io/images/tutorials"
# Tile meanings from the labeled tileset - the FUN sprites!
TILES = {
# Players - knights and heroes!
'player_knight': 84,
'player_mage': 85,
'player_rogue': 86,
'player_warrior': 87,
'player_archer': 88,
'player_alt1': 96,
'player_alt2': 97,
'player_alt3': 98,
# Enemies - scary!
'enemy_slime': 108,
'enemy_bat': 109,
'enemy_spider': 110,
'enemy_rat': 111,
'enemy_orc': 120,
'enemy_troll': 121,
'enemy_ghost': 122,
'enemy_skeleton': 123,
'enemy_demon': 124,
'enemy_boss': 92,
# Terrain
'floor_stone': 42,
'floor_wood': 49,
'floor_grass': 48,
'floor_dirt': 50,
'wall_stone': 30,
'wall_brick': 14,
'wall_mossy': 28,
# Items
'item_potion': 113,
'item_scroll': 114,
'item_key': 115,
'item_coin': 116,
# Equipment
'equip_sword': 101,
'equip_shield': 102,
'equip_helm': 103,
'equip_armor': 104,
# Chests and doors
'chest_closed': 89,
'chest_open': 90,
'door_closed': 33,
'door_open': 35,
# Decorations
'torch': 72,
'barrel': 73,
'skull': 74,
'bones': 75,
}
class TutorialShowcase:
"""Creates beautiful showcase screenshots for tutorials."""
def __init__(self, scene_name, output_name):
self.scene = mcrfpy.Scene(scene_name)
self.output_path = os.path.join(OUTPUT_DIR, output_name)
self.grid = None
def setup_grid(self, width, height, zoom=3.0):
"""Create a grid with nice defaults."""
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
self.grid = mcrfpy.Grid(
pos=(50, 80),
size=(700, 500),
grid_size=(width, height),
texture=texture,
zoom=zoom
)
self.grid.fill_color = mcrfpy.Color(20, 20, 30)
self.scene.children.append(self.grid)
self.texture = texture
return self.grid
def add_title(self, text, subtitle=None):
"""Add a title to the scene."""
title = mcrfpy.Caption(text=text, pos=(50, 20))
title.fill_color = mcrfpy.Color(255, 255, 255)
title.font_size = 28
self.scene.children.append(title)
if subtitle:
sub = mcrfpy.Caption(text=subtitle, pos=(50, 50))
sub.fill_color = mcrfpy.Color(180, 180, 200)
sub.font_size = 16
self.scene.children.append(sub)
def fill_floor(self, tile=None):
"""Fill grid with floor tiles."""
if tile is None:
tile = TILES['floor_stone']
w, h = int(self.grid.grid_size[0]), int(self.grid.grid_size[1])
for y in range(h):
for x in range(w):
self.grid.at(x, y).tilesprite = tile
def add_walls(self, tile=None):
"""Add wall border."""
if tile is None:
tile = TILES['wall_stone']
w, h = int(self.grid.grid_size[0]), int(self.grid.grid_size[1])
for x in range(w):
self.grid.at(x, 0).tilesprite = tile
self.grid.at(x, 0).walkable = False
self.grid.at(x, h-1).tilesprite = tile
self.grid.at(x, h-1).walkable = False
for y in range(h):
self.grid.at(0, y).tilesprite = tile
self.grid.at(0, y).walkable = False
self.grid.at(w-1, y).tilesprite = tile
self.grid.at(w-1, y).walkable = False
def add_entity(self, x, y, sprite):
"""Add an entity to the grid."""
entity = mcrfpy.Entity(
grid_pos=(x, y),
texture=self.texture,
sprite_index=sprite
)
self.grid.entities.append(entity)
return entity
def center_on(self, x, y):
"""Center camera on a position."""
self.grid.center = (x * 16 + 8, y * 16 + 8)
def screenshot(self):
"""Take the screenshot - synchronous in headless mode!"""
self.scene.activate()
result = automation.screenshot(self.output_path)
print(f" -> {self.output_path} (result: {result})")
return result
def part01_grid_movement():
"""Part 1: Grid Movement - Knight in a dungeon room."""
showcase = TutorialShowcase("part01", "part_01_grid_movement.png")
showcase.setup_grid(12, 9, zoom=3.5)
showcase.add_title("Part 1: The '@' and the Dungeon Grid",
"Creating a grid, placing entities, handling input")
showcase.fill_floor(TILES['floor_stone'])
showcase.add_walls(TILES['wall_stone'])
# Add the player (a cool knight, not boring @)
showcase.add_entity(6, 4, TILES['player_knight'])
# Add some decorations to make it interesting
showcase.add_entity(2, 2, TILES['torch'])
showcase.add_entity(9, 2, TILES['torch'])
showcase.add_entity(2, 6, TILES['barrel'])
showcase.add_entity(9, 6, TILES['skull'])
showcase.center_on(6, 4)
showcase.screenshot()
def part02_tiles_collision():
"""Part 2: Tiles and Collision - Walls and walkability."""
showcase = TutorialShowcase("part02", "part_02_tiles_collision.png")
showcase.setup_grid(14, 10, zoom=3.0)
showcase.add_title("Part 2: Tiles, Collision, and Walkability",
"Different tile types and blocking movement")
showcase.fill_floor(TILES['floor_stone'])
showcase.add_walls(TILES['wall_brick'])
# Create some interior walls to show collision
for y in range(2, 5):
showcase.grid.at(5, y).tilesprite = TILES['wall_stone']
showcase.grid.at(5, y).walkable = False
for y in range(5, 8):
showcase.grid.at(9, y).tilesprite = TILES['wall_stone']
showcase.grid.at(9, y).walkable = False
# Add a door
showcase.grid.at(5, 5).tilesprite = TILES['door_closed']
showcase.grid.at(5, 5).walkable = False
# Player navigating the maze
showcase.add_entity(3, 4, TILES['player_warrior'])
# Chest as goal
showcase.add_entity(11, 5, TILES['chest_closed'])
showcase.center_on(7, 5)
showcase.screenshot()
def part03_dungeon_generation():
"""Part 3: Dungeon Generation - Procedural rooms and corridors."""
showcase = TutorialShowcase("part03", "part_03_dungeon_generation.png")
showcase.setup_grid(20, 14, zoom=2.5)
showcase.add_title("Part 3: Procedural Dungeon Generation",
"Random rooms connected by corridors")
# Fill with walls first
for y in range(14):
for x in range(20):
showcase.grid.at(x, y).tilesprite = TILES['wall_stone']
showcase.grid.at(x, y).walkable = False
# Carve out two rooms
# Room 1 (left)
for y in range(3, 8):
for x in range(2, 8):
showcase.grid.at(x, y).tilesprite = TILES['floor_stone']
showcase.grid.at(x, y).walkable = True
# Room 2 (right)
for y in range(6, 12):
for x in range(12, 18):
showcase.grid.at(x, y).tilesprite = TILES['floor_stone']
showcase.grid.at(x, y).walkable = True
# Corridor connecting them
for x in range(7, 13):
showcase.grid.at(x, 6).tilesprite = TILES['floor_dirt']
showcase.grid.at(x, 6).walkable = True
for y in range(6, 9):
showcase.grid.at(12, y).tilesprite = TILES['floor_dirt']
showcase.grid.at(12, y).walkable = True
# Player in first room
showcase.add_entity(4, 5, TILES['player_knight'])
# Some loot in second room
showcase.add_entity(14, 9, TILES['chest_closed'])
showcase.add_entity(16, 8, TILES['item_potion'])
# Torches
showcase.add_entity(3, 3, TILES['torch'])
showcase.add_entity(6, 3, TILES['torch'])
showcase.add_entity(13, 7, TILES['torch'])
showcase.center_on(10, 7)
showcase.screenshot()
def part04_fov():
"""Part 4: Field of View - Showing explored vs visible areas."""
showcase = TutorialShowcase("part04", "part_04_fov.png")
showcase.setup_grid(16, 12, zoom=2.8)
showcase.add_title("Part 4: Field of View and Fog of War",
"What the player can see vs. the unknown")
showcase.fill_floor(TILES['floor_stone'])
showcase.add_walls(TILES['wall_brick'])
# Some interior pillars to block sight
for pos in [(5, 4), (5, 7), (10, 5), (10, 8)]:
showcase.grid.at(pos[0], pos[1]).tilesprite = TILES['wall_mossy']
showcase.grid.at(pos[0], pos[1]).walkable = False
# Player with "light"
showcase.add_entity(8, 6, TILES['player_mage'])
# Hidden enemy (player wouldn't see this!)
showcase.add_entity(12, 3, TILES['enemy_ghost'])
# Visible enemies
showcase.add_entity(3, 5, TILES['enemy_bat'])
showcase.add_entity(6, 8, TILES['enemy_spider'])
showcase.center_on(8, 6)
showcase.screenshot()
def part05_enemies():
"""Part 5: Enemies - A dungeon full of monsters."""
showcase = TutorialShowcase("part05", "part_05_enemies.png")
showcase.setup_grid(18, 12, zoom=2.5)
showcase.add_title("Part 5: Adding Enemies",
"Different monster types with AI behavior")
showcase.fill_floor(TILES['floor_stone'])
showcase.add_walls(TILES['wall_stone'])
# The hero
showcase.add_entity(3, 5, TILES['player_warrior'])
# A variety of enemies
showcase.add_entity(7, 3, TILES['enemy_slime'])
showcase.add_entity(10, 6, TILES['enemy_bat'])
showcase.add_entity(8, 8, TILES['enemy_spider'])
showcase.add_entity(14, 4, TILES['enemy_orc'])
showcase.add_entity(15, 8, TILES['enemy_skeleton'])
showcase.add_entity(12, 5, TILES['enemy_rat'])
# Boss at the end
showcase.add_entity(15, 6, TILES['enemy_boss'])
# Some decorations
showcase.add_entity(5, 2, TILES['bones'])
showcase.add_entity(13, 9, TILES['skull'])
showcase.add_entity(2, 8, TILES['torch'])
showcase.add_entity(16, 2, TILES['torch'])
showcase.center_on(9, 5)
showcase.screenshot()
def part06_combat():
"""Part 6: Combat - Battle in progress!"""
showcase = TutorialShowcase("part06", "part_06_combat.png")
showcase.setup_grid(14, 10, zoom=3.0)
showcase.add_title("Part 6: Combat System",
"HP, attack, defense, and turn-based fighting")
showcase.fill_floor(TILES['floor_dirt'])
showcase.add_walls(TILES['wall_brick'])
# Battle scene - player vs enemy
showcase.add_entity(5, 5, TILES['player_knight'])
showcase.add_entity(8, 5, TILES['enemy_orc'])
# Fallen enemies (show combat has happened)
showcase.add_entity(4, 3, TILES['bones'])
showcase.add_entity(9, 7, TILES['skull'])
# Equipment the player has
showcase.add_entity(3, 6, TILES['equip_shield'])
showcase.add_entity(10, 4, TILES['item_potion'])
showcase.center_on(6, 5)
showcase.screenshot()
def part07_ui():
"""Part 7: User Interface - Health bars and menus."""
showcase = TutorialShowcase("part07", "part_07_ui.png")
showcase.setup_grid(12, 8, zoom=3.0)
showcase.add_title("Part 7: User Interface",
"Health bars, message logs, and menus")
showcase.fill_floor(TILES['floor_wood'])
showcase.add_walls(TILES['wall_brick'])
# Player
showcase.add_entity(6, 4, TILES['player_rogue'])
# Some items to interact with
showcase.add_entity(4, 3, TILES['chest_open'])
showcase.add_entity(8, 5, TILES['item_coin'])
# Add UI overlay example - health bar frame
ui_frame = mcrfpy.Frame(pos=(50, 520), size=(200, 40))
ui_frame.fill_color = mcrfpy.Color(40, 40, 50, 200)
ui_frame.outline = 2
ui_frame.outline_color = mcrfpy.Color(80, 80, 100)
showcase.scene.children.append(ui_frame)
# Health label
hp_label = mcrfpy.Caption(text="HP: 45/50", pos=(10, 10))
hp_label.fill_color = mcrfpy.Color(255, 100, 100)
hp_label.font_size = 18
ui_frame.children.append(hp_label)
# Health bar background
hp_bg = mcrfpy.Frame(pos=(90, 12), size=(100, 16))
hp_bg.fill_color = mcrfpy.Color(60, 20, 20)
ui_frame.children.append(hp_bg)
# Health bar fill
hp_fill = mcrfpy.Frame(pos=(90, 12), size=(90, 16)) # 90% health
hp_fill.fill_color = mcrfpy.Color(200, 50, 50)
ui_frame.children.append(hp_fill)
showcase.center_on(6, 4)
showcase.screenshot()
def main():
"""Generate all showcase screenshots!"""
os.makedirs(OUTPUT_DIR, exist_ok=True)
print("=== Tutorial Screenshot Showcase ===")
print(f"Output: {OUTPUT_DIR}\n")
showcases = [
('Part 1: Grid Movement', part01_grid_movement),
('Part 2: Tiles & Collision', part02_tiles_collision),
('Part 3: Dungeon Generation', part03_dungeon_generation),
('Part 4: Field of View', part04_fov),
('Part 5: Enemies', part05_enemies),
('Part 6: Combat', part06_combat),
('Part 7: UI', part07_ui),
]
for name, func in showcases:
print(f"Generating {name}...")
try:
func()
except Exception as e:
print(f" ERROR: {e}")
print("\n=== All screenshots generated! ===")
sys.exit(0)
main()

View file

@ -1,129 +0,0 @@
# McRogueFace API Test Findings
*Generated by Frack, January 14, 2026*
## Summary
Tested code snippets from docs site against actual runtime API.
Tests created in `/tests/docs/` with screenshots in `/tests/docs/screenshots/`.
---
## Entity Constructor
**Correct:**
```python
entity = mcrfpy.Entity(grid_pos=(10, 7), texture=texture, sprite_index=84)
```
**Wrong:**
```python
entity = mcrfpy.Entity(pos=(10, 7), ...) # FAILS - no 'pos' kwarg
entity = mcrfpy.Entity(grid_x=10, grid_y=7, ...) # FAILS - no separate kwargs
```
---
## Scene API
**Modern (WORKS):**
```python
scene = mcrfpy.Scene("name")
scene.children.append(frame)
scene.on_key = handler
scene.activate()
```
**Deprecated → Modern:**
```python
# OLD → NEW
mcrfpy.createScene("name") → scene = mcrfpy.Scene("name")
mcrfpy.sceneUI("name") → scene.children
mcrfpy.setScene("name") → scene.activate()
mcrfpy.keypressScene(fn) → scene.on_key = fn
mcrfpy.currentScene() → mcrfpy.current_scene # property, not function!
```
---
## Grid.at() Signature
Both work:
```python
point = grid.at((x, y)) # Tuple - documented
point = grid.at(x, y) # Separate args - also works!
```
---
## Animation System
**Works:**
- `x`, `y`, `w`, `h` properties animate correctly
- Callbacks fire as expected
- Multiple simultaneous animations work
- Easing functions work
**Does NOT work:**
- `opacity` - property exists, can set directly, but animation ignores it
---
## Timer API
**Modern:**
```python
timer = mcrfpy.Timer("name", callback, seconds)
# Callback signature: def callback(timer, runtime):
```
**Deprecated:**
```python
mcrfpy.setTimer("name", callback, milliseconds) # Wrong signature too
```
---
## Color API
Always use `mcrfpy.Color()`:
```python
frame.fill_color = mcrfpy.Color(255, 0, 0) # CORRECT
frame.fill_color = mcrfpy.Color(255, 0, 0, 128) # With alpha
```
Tuples no longer work:
```python
frame.fill_color = (255, 0, 0) # FAILS
```
---
## Available Globals
**All exist:**
- `mcrfpy.default_texture` - Texture for kenney_tinydungeon.png
- `mcrfpy.default_font` - Font for JetBrains Mono
- `mcrfpy.default_fov` - (default FOV settings)
---
## Files Requiring Updates
1. `quickstart.md` - All 4 code blocks use deprecated API
2. `features/scenes.md` - "Procedural API" section entirely deprecated
3. `features/animation.md` - opacity examples don't work
4. Any file using `setTimer`, `createScene`, `sceneUI`, `setScene`, `keypressScene`
---
## Test Files Created
| Test | Status | Notes |
|------|--------|-------|
| test_quickstart_simple_scene.py | PASS | |
| test_quickstart_main_menu.py | PASS | |
| test_quickstart_entities.py | PASS | Uses grid_pos= |
| test_quickstart_sprites.py | PASS | |
| test_features_animation.py | PASS | opacity test skipped |
| test_features_scenes.py | PASS | Documents deprecated API |
| test_entity_api.py | INFO | Verifies grid_pos= works |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View file

@ -1,18 +0,0 @@
#!/usr/bin/env python3
"""Verify mcrfpy.current_scene property."""
import mcrfpy
import sys
scene = mcrfpy.Scene("test")
scene.activate()
mcrfpy.step(0.1)
try:
current = mcrfpy.current_scene
print(f"mcrfpy.current_scene = {current}")
print(f"type: {type(current)}")
print("VERIFIED: mcrfpy.current_scene WORKS")
except AttributeError as e:
print(f"FAILED: {e}")
sys.exit(0)

View file

@ -1,30 +0,0 @@
#!/usr/bin/env python3
"""Test mcrfpy default resources."""
import mcrfpy
import sys
scene = mcrfpy.Scene("test")
scene.activate()
mcrfpy.step(0.01)
print("Checking mcrfpy defaults:")
try:
dt = mcrfpy.default_texture
print(f" default_texture = {dt}")
except AttributeError as e:
print(f" default_texture: NOT FOUND")
try:
df = mcrfpy.default_font
print(f" default_font = {df}")
except AttributeError as e:
print(f" default_font: NOT FOUND")
# Also check what other module-level attributes exist
print("\nAll mcrfpy attributes starting with 'default':")
for attr in dir(mcrfpy):
if 'default' in attr.lower():
print(f" {attr}")
sys.exit(0)

View file

@ -1,30 +0,0 @@
#!/usr/bin/env python3
"""Quick test to verify Entity constructor signature."""
import mcrfpy
import sys
scene = mcrfpy.Scene("test")
scene.activate()
mcrfpy.step(0.01)
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
grid = mcrfpy.Grid(grid_size=(20, 15), texture=texture, pos=(10, 10), size=(640, 480))
scene.children.append(grid)
# Test grid_pos vs grid_x/grid_y
try:
e1 = mcrfpy.Entity(grid_pos=(5, 5), texture=texture, sprite_index=85)
grid.entities.append(e1)
print("grid_pos= WORKS")
except TypeError as e:
print(f"grid_pos= FAILS: {e}")
try:
e2 = mcrfpy.Entity(grid_x=7, grid_y=7, texture=texture, sprite_index=85)
grid.entities.append(e2)
print("grid_x=/grid_y= WORKS")
except TypeError as e:
print(f"grid_x=/grid_y= FAILS: {e}")
print("Entity API test complete")
sys.exit(0)

View file

@ -1,86 +0,0 @@
#!/usr/bin/env python3
"""Test for features/animation.md examples.
Tests the modern API equivalents of animation examples.
"""
import mcrfpy
from mcrfpy import automation
import sys
# Setup scene using modern API
scene = mcrfpy.Scene("animation_demo")
scene.activate()
mcrfpy.step(0.01)
# Test 1: Basic Animation (lines 9-19)
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
frame.fill_color = mcrfpy.Color(255, 0, 0)
scene.children.append(frame)
# Animate x position
anim = mcrfpy.Animation("x", 500.0, 2.0, "easeInOut")
anim.start(frame)
print("Test 1: Basic animation started")
# Step forward to run animation
mcrfpy.step(2.5)
# Verify animation completed
if abs(frame.x - 500.0) < 1.0:
print("Test 1: PASS - frame moved to x=500")
else:
print(f"Test 1: FAIL - frame at x={frame.x}, expected 500")
sys.exit(1)
# Test 2: Multiple simultaneous animations (lines 134-144)
frame2 = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
frame2.fill_color = mcrfpy.Color(0, 255, 0)
scene.children.append(frame2)
mcrfpy.Animation("x", 200.0, 1.0, "easeInOut").start(frame2)
mcrfpy.Animation("y", 150.0, 1.0, "easeInOut").start(frame2)
mcrfpy.Animation("w", 300.0, 1.0, "easeInOut").start(frame2)
mcrfpy.Animation("h", 200.0, 1.0, "easeInOut").start(frame2)
mcrfpy.step(1.5)
if abs(frame2.x - 200.0) < 1.0 and abs(frame2.y - 150.0) < 1.0:
print("Test 2: PASS - multiple animations completed")
else:
print(f"Test 2: FAIL - frame2 at ({frame2.x}, {frame2.y})")
sys.exit(1)
# Test 3: Callback (lines 105-112)
callback_fired = False
def on_complete(animation, target):
global callback_fired
callback_fired = True
print("Test 3: Callback fired!")
frame3 = mcrfpy.Frame(pos=(0, 300), size=(50, 50))
frame3.fill_color = mcrfpy.Color(0, 0, 255)
scene.children.append(frame3)
anim3 = mcrfpy.Animation("x", 300.0, 0.5, "easeInOut", callback=on_complete)
anim3.start(frame3)
mcrfpy.step(1.0)
if callback_fired:
print("Test 3: PASS - callback executed")
else:
print("Test 3: FAIL - callback not executed")
sys.exit(1)
# Test 4: NOTE - Opacity animation documented in features/animation.md
# but DOES NOT WORK on Frame. The property exists but animation
# system doesn't animate it. This is a DOCS BUG to report.
# Skipping test 4 - opacity animation not supported.
print("Test 4: SKIPPED - opacity animation not supported on Frame (docs bug)")
# Take screenshot showing animation results
automation.screenshot("/opt/goblincorps/repos/McRogueFace/tests/docs/screenshots/features_animation.png")
print("\nAll animation tests PASS")
sys.exit(0)

View file

@ -1,84 +0,0 @@
#!/usr/bin/env python3
"""Test for features/scenes.md examples.
Tests both modern and procedural APIs from the docs.
"""
import mcrfpy
from mcrfpy import automation
import sys
# Test 1: Modern Scene API (lines 28-42)
print("Test 1: Modern Scene API")
scene = mcrfpy.Scene("test_modern")
scene.children.append(mcrfpy.Frame(pos=(0, 0), size=(800, 600)))
def my_handler(key, action):
if action == "start":
print(f" Key handler received: {key}")
scene.on_key = my_handler
scene.activate()
mcrfpy.step(0.1)
print(" PASS - modern Scene API works")
# Test 2: Check Scene properties
print("Test 2: Scene properties")
print(f" scene.name = {scene.name}")
print(f" scene.active = {scene.active}")
print(f" len(scene.children) = {len(scene.children)}")
# Test 3: Check if default_texture exists
print("Test 3: default_texture")
try:
dt = mcrfpy.default_texture
print(f" mcrfpy.default_texture = {dt}")
except AttributeError:
print(" default_texture NOT FOUND - docs bug!")
# Test 4: Check if currentScene exists
print("Test 4: currentScene()")
try:
current = mcrfpy.currentScene()
print(f" mcrfpy.currentScene() = {current}")
except AttributeError:
print(" currentScene() NOT FOUND - docs bug!")
# Test 5: Check Grid.at() signature
print("Test 5: Grid.at() signature")
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
grid = mcrfpy.Grid(grid_size=(10, 10), texture=texture, pos=(0, 0), size=(200, 200))
# Try both signatures
try:
point_tuple = grid.at((5, 5))
print(" grid.at((x, y)) - tuple WORKS")
except Exception as e:
print(f" grid.at((x, y)) FAILS: {e}")
try:
point_sep = grid.at(5, 5)
print(" grid.at(x, y) - separate args WORKS")
except Exception as e:
print(f" grid.at(x, y) FAILS: {e}")
# Test 6: Scene transitions (setScene)
print("Test 6: setScene()")
scene2 = mcrfpy.Scene("test_transitions")
scene2.activate()
mcrfpy.step(0.1)
# Check if setScene exists
try:
mcrfpy.setScene("test_modern")
print(" mcrfpy.setScene() WORKS")
except AttributeError:
print(" mcrfpy.setScene() NOT FOUND - use scene.activate() instead")
except Exception as e:
print(f" mcrfpy.setScene() error: {e}")
# Take screenshot
mcrfpy.step(0.1)
automation.screenshot("/opt/goblincorps/repos/McRogueFace/tests/docs/screenshots/features_scenes.png")
print("\nAll scene tests complete")
sys.exit(0)

View file

@ -1,70 +0,0 @@
#!/usr/bin/env python3
"""Test for quickstart.md 'Game Entity' example.
Original (DEPRECATED - lines 133-168):
mcrfpy.createScene("game")
grid = mcrfpy.Grid(20, 15, texture, (10, 10), (640, 480))
ui = mcrfpy.sceneUI("game")
player = mcrfpy.Entity((10, 7), texture, 85)
mcrfpy.keypressScene(handle_keys)
mcrfpy.setScene("game")
Modern equivalent below.
"""
import mcrfpy
from mcrfpy import automation
import sys
# Create scene using modern API
scene = mcrfpy.Scene("game")
scene.activate()
mcrfpy.step(0.01)
# Load texture
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create grid using modern keyword API
grid = mcrfpy.Grid(
grid_size=(20, 15),
texture=texture,
pos=(10, 10),
size=(640, 480)
)
scene.children.append(grid)
# Add player entity
player = mcrfpy.Entity(grid_pos=(10, 7), texture=texture, sprite_index=85)
grid.entities.append(player)
# Add NPC entity
npc = mcrfpy.Entity(grid_pos=(5, 5), texture=texture, sprite_index=109)
grid.entities.append(npc)
# Add treasure chest
treasure = mcrfpy.Entity(grid_pos=(15, 10), texture=texture, sprite_index=89)
grid.entities.append(treasure)
# Movement handler using modern API
def handle_keys(key, state):
if state == "start":
x, y = player.pos[0], player.pos[1]
if key == "W":
player.pos = (x, y - 1)
elif key == "S":
player.pos = (x, y + 1)
elif key == "A":
player.pos = (x - 1, y)
elif key == "D":
player.pos = (x + 1, y)
scene.on_key = handle_keys
# Center grid on player
grid.center = (10, 7)
# Render and screenshot
mcrfpy.step(0.1)
automation.screenshot("/opt/goblincorps/repos/McRogueFace/tests/docs/screenshots/quickstart_entities.png")
print("PASS - quickstart entities")
sys.exit(0)

View file

@ -1,74 +0,0 @@
#!/usr/bin/env python3
"""Test for quickstart.md 'Main Menu' example.
Original (DEPRECATED - lines 82-125):
mcrfpy.createScene("main_menu")
ui = mcrfpy.sceneUI("main_menu")
bg = mcrfpy.Frame(0, 0, 1024, 768, fill_color=(20, 20, 40))
title = mcrfpy.Caption((312, 100), "My Awesome Game", font, fill_color=(255, 255, 100))
button_frame.click = start_game
mcrfpy.setScene("main_menu")
Modern equivalent below.
"""
import mcrfpy
from mcrfpy import automation
import sys
# Create scene using modern API
scene = mcrfpy.Scene("main_menu")
scene.activate()
mcrfpy.step(0.01)
# Load font
font = mcrfpy.Font("assets/JetbrainsMono.ttf")
# Add background using modern Frame API (keyword args)
bg = mcrfpy.Frame(
pos=(0, 0),
size=(1024, 768),
fill_color=mcrfpy.Color(20, 20, 40)
)
scene.children.append(bg)
# Add title using modern Caption API
title = mcrfpy.Caption(
pos=(312, 100),
text="My Awesome Game",
font=font,
fill_color=mcrfpy.Color(255, 255, 100)
)
title.font_size = 48
title.outline = 2
title.outline_color = mcrfpy.Color(0, 0, 0)
scene.children.append(title)
# Create button frame
button_frame = mcrfpy.Frame(
pos=(362, 300),
size=(300, 80),
fill_color=mcrfpy.Color(50, 150, 50)
)
# Button caption
button_caption = mcrfpy.Caption(
pos=(90, 25), # Centered in button
text="Start Game",
fill_color=mcrfpy.Color(255, 255, 255)
)
button_caption.font_size = 24
button_frame.children.append(button_caption)
# Click handler using modern on_click (3 args: x, y, button)
def start_game(x, y, button):
print("Starting the game!")
button_frame.on_click = start_game
scene.children.append(button_frame)
# Render and screenshot
mcrfpy.step(0.1)
automation.screenshot("/opt/goblincorps/repos/McRogueFace/tests/docs/screenshots/quickstart_main_menu.png")
print("PASS - quickstart main menu")
sys.exit(0)

View file

@ -1,48 +0,0 @@
#!/usr/bin/env python3
"""Test for quickstart.md 'Simple Test Scene' example.
Original (DEPRECATED - lines 48-74):
mcrfpy.createScene("test")
grid = mcrfpy.Grid(20, 15, texture, (10, 10), (800, 600))
ui = mcrfpy.sceneUI("test")
mcrfpy.setScene("test")
mcrfpy.keypressScene(move_around)
Modern equivalent below.
"""
import mcrfpy
from mcrfpy import automation
import sys
# Create scene using modern API
scene = mcrfpy.Scene("test")
scene.activate()
mcrfpy.step(0.01) # Initialize
# Load texture
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create grid using modern keyword API
grid = mcrfpy.Grid(
grid_size=(20, 15),
texture=texture,
pos=(10, 10),
size=(800, 600)
)
# Add to scene's children (not sceneUI)
scene.children.append(grid)
# Add keyboard controls using modern API
def move_around(key, state):
if state == "start":
print(f"You pressed {key}")
scene.on_key = move_around
# Render and screenshot
mcrfpy.step(0.1)
automation.screenshot("/opt/goblincorps/repos/McRogueFace/tests/docs/screenshots/quickstart_simple_scene.png")
print("PASS - quickstart simple scene")
sys.exit(0)

View file

@ -1,49 +0,0 @@
#!/usr/bin/env python3
"""Test for quickstart.md 'Custom Sprite Sheet' example.
Original (DEPRECATED - lines 176-201):
mcrfpy.createScene("game")
grid = mcrfpy.Grid(20, 15, my_texture, (10, 10), (640, 480))
grid.at(5, 5).sprite = 10
ui = mcrfpy.sceneUI("game")
mcrfpy.setScene("game")
Modern equivalent below.
"""
import mcrfpy
from mcrfpy import automation
import sys
# Create scene using modern API
scene = mcrfpy.Scene("game")
scene.activate()
mcrfpy.step(0.01)
# Load sprite sheet (using existing texture for test)
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
# Create grid using modern keyword API
grid = mcrfpy.Grid(
grid_size=(20, 15),
texture=texture,
pos=(10, 10),
size=(640, 480)
)
# Set specific tiles using modern API
# grid.at() returns a GridPoint with tilesprite property
grid.at((5, 5)).tilesprite = 10 # Note: tuple for position
grid.at((6, 5)).tilesprite = 11
# Set walkability
grid.at((6, 5)).walkable = False
# Add grid to scene
scene.children.append(grid)
# Render and screenshot
mcrfpy.step(0.1)
automation.screenshot("/opt/goblincorps/repos/McRogueFace/tests/docs/screenshots/quickstart_sprites.png")
print("PASS - quickstart sprites")
sys.exit(0)

View file

@ -1,14 +0,0 @@
[pytest]
# McRogueFace test scripts run via subprocess, not as Python modules
# They contain `import mcrfpy` which is only available inside McRogueFace
# Don't try to import test scripts from these directories
norecursedirs = unit integration regression benchmarks demo geometry_demo notes vllm_demo
# Run test_*.py files in tests/ root that are pytest-native wrappers
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# Custom option for timeout
addopts = -v

View file

@ -7,8 +7,6 @@ Usage:
python3 tests/run_tests.py # Run all tests
python3 tests/run_tests.py unit # Run only unit tests
python3 tests/run_tests.py -v # Verbose output
python3 tests/run_tests.py -q # Quiet (no checksums)
python3 tests/run_tests.py --timeout=30 # Custom timeout
"""
import os
import subprocess
@ -20,9 +18,8 @@ from pathlib import Path
# Configuration
TESTS_DIR = Path(__file__).parent
BUILD_DIR = TESTS_DIR.parent / "build"
LIB_DIR = TESTS_DIR.parent / "__lib"
MCROGUEFACE = BUILD_DIR / "mcrogueface"
DEFAULT_TIMEOUT = 10 # seconds per test
TIMEOUT = 10 # seconds per test
# Test directories to run (in order)
TEST_DIRS = ['unit', 'integration', 'regression']
@ -42,7 +39,7 @@ def get_screenshot_checksum(test_dir):
checksums[png.name] = hashlib.md5(f.read()).hexdigest()[:8]
return checksums
def run_test(test_path, verbose=False, timeout=DEFAULT_TIMEOUT):
def run_test(test_path, verbose=False):
"""Run a single test and return (passed, duration, output)."""
start = time.time()
@ -50,19 +47,13 @@ def run_test(test_path, verbose=False, timeout=DEFAULT_TIMEOUT):
for png in BUILD_DIR.glob("test_*.png"):
png.unlink()
# Set up environment with library path
env = os.environ.copy()
existing_ld = env.get('LD_LIBRARY_PATH', '')
env['LD_LIBRARY_PATH'] = f"{LIB_DIR}:{existing_ld}" if existing_ld else str(LIB_DIR)
try:
result = subprocess.run(
[str(MCROGUEFACE), '--headless', '--exec', str(test_path)],
capture_output=True,
text=True,
timeout=timeout,
cwd=str(BUILD_DIR),
env=env
timeout=TIMEOUT,
cwd=str(BUILD_DIR)
)
duration = time.time() - start
passed = result.returncode == 0
@ -75,7 +66,7 @@ def run_test(test_path, verbose=False, timeout=DEFAULT_TIMEOUT):
return passed, duration, output
except subprocess.TimeoutExpired:
return False, timeout, "TIMEOUT"
return False, TIMEOUT, "TIMEOUT"
except Exception as e:
return False, 0, str(e)
@ -88,16 +79,6 @@ def find_tests(directory):
def main():
verbose = '-v' in sys.argv or '--verbose' in sys.argv
quiet = '-q' in sys.argv or '--quiet' in sys.argv
# Parse --timeout=N
timeout = DEFAULT_TIMEOUT
for arg in sys.argv[1:]:
if arg.startswith('--timeout='):
try:
timeout = int(arg.split('=')[1])
except ValueError:
pass
# Determine which directories to test
dirs_to_test = []
@ -108,7 +89,7 @@ def main():
dirs_to_test = TEST_DIRS
print(f"{BOLD}McRogueFace Test Runner{RESET}")
print(f"Testing: {', '.join(dirs_to_test)} (timeout: {timeout}s)")
print(f"Testing: {', '.join(dirs_to_test)}")
print("=" * 60)
results = {'pass': 0, 'fail': 0, 'total_time': 0}
@ -123,7 +104,7 @@ def main():
for test_path in tests:
test_name = test_path.name
passed, duration, output = run_test(test_path, verbose, timeout)
passed, duration, output = run_test(test_path, verbose)
results['total_time'] += duration
if passed:
@ -134,12 +115,11 @@ def main():
status = f"{RED}FAIL{RESET}"
failures.append((test_dir, test_name, output))
# Get screenshot checksums if any were generated (skip in quiet mode)
# Get screenshot checksums if any were generated
checksums = get_screenshot_checksum(BUILD_DIR)
checksum_str = ""
if not quiet:
checksums = get_screenshot_checksum(BUILD_DIR)
if checksums:
checksum_str = f" [{', '.join(f'{k}:{v}' for k,v in checksums.items())}]"
if checksums:
checksum_str = f" [{', '.join(f'{k}:{v}' for k,v in checksums.items())}]"
print(f" {status} {test_name} ({duration:.2f}s){checksum_str}")

View file

@ -1,116 +0,0 @@
"""
Pytest wrapper for McRogueFace test scripts.
This file discovers and runs all McRogueFace test scripts in unit/, integration/,
and regression/ directories via subprocess.
Usage:
pytest tests/test_mcrogueface.py -q # Quiet output
pytest tests/test_mcrogueface.py -k "bsp" # Filter by name
pytest tests/test_mcrogueface.py --mcrf-timeout=30 # Custom timeout
pytest tests/test_mcrogueface.py -x # Stop on first failure
"""
import os
import subprocess
import pytest
from pathlib import Path
# Paths
TESTS_DIR = Path(__file__).parent
BUILD_DIR = TESTS_DIR.parent / "build"
LIB_DIR = TESTS_DIR.parent / "__lib"
MCROGUEFACE = BUILD_DIR / "mcrogueface"
# Test directories
TEST_DIRS = ['unit', 'integration', 'regression']
# Default timeout
DEFAULT_TIMEOUT = 10
def discover_tests():
"""Find all test scripts in test directories."""
tests = []
for test_dir in TEST_DIRS:
dir_path = TESTS_DIR / test_dir
if dir_path.exists():
for test_file in sorted(dir_path.glob("*.py")):
if test_file.name != '__init__.py':
rel_path = f"{test_dir}/{test_file.name}"
tests.append(rel_path)
return tests
def get_env():
"""Get environment with LD_LIBRARY_PATH set."""
env = os.environ.copy()
existing_ld = env.get('LD_LIBRARY_PATH', '')
env['LD_LIBRARY_PATH'] = f"{LIB_DIR}:{existing_ld}" if existing_ld else str(LIB_DIR)
return env
def run_mcrf_test(script_path, timeout=DEFAULT_TIMEOUT):
"""Run a McRogueFace test script and return (passed, output)."""
full_path = TESTS_DIR / script_path
env = get_env()
try:
result = subprocess.run(
[str(MCROGUEFACE), '--headless', '--exec', str(full_path)],
capture_output=True,
text=True,
timeout=timeout,
cwd=str(BUILD_DIR),
env=env
)
output = result.stdout + result.stderr
passed = result.returncode == 0
# Check for PASS/FAIL in output
if 'FAIL' in output and 'PASS' not in output.split('FAIL')[-1]:
passed = False
return passed, output
except subprocess.TimeoutExpired:
return False, "TIMEOUT"
except Exception as e:
return False, str(e)
# Discover tests at module load time
ALL_TESTS = discover_tests()
@pytest.fixture
def mcrf_timeout(request):
"""Get timeout from command line or default."""
return request.config.getoption("--mcrf-timeout", default=DEFAULT_TIMEOUT)
def pytest_addoption(parser):
"""Add --mcrf-timeout option."""
try:
parser.addoption(
"--mcrf-timeout",
action="store",
default=DEFAULT_TIMEOUT,
type=int,
help="Timeout in seconds for McRogueFace tests"
)
except ValueError:
# Option already added
pass
@pytest.mark.parametrize("script_path", ALL_TESTS, ids=lambda x: x.replace('/', '::'))
def test_mcrogueface_script(script_path, request):
"""Run a McRogueFace test script."""
timeout = request.config.getoption("--mcrf-timeout", default=DEFAULT_TIMEOUT)
passed, output = run_mcrf_test(script_path, timeout=timeout)
if not passed:
# Show last 15 lines of output on failure
lines = output.strip().split('\n')[-15:]
pytest.fail('\n'.join(lines))

View file

@ -1,11 +1,47 @@
#!/usr/bin/env python3
"""Example of CORRECT test pattern using mcrfpy.step() for automation
Refactored from timer-based approach to synchronous step() pattern.
"""
"""Example of CORRECT test pattern using timer callbacks for automation"""
import mcrfpy
from mcrfpy import automation
from datetime import datetime
import sys
def run_automation_tests(timer, runtime):
"""This runs AFTER the game loop has started and rendered frames"""
print("\n=== Automation Test Running (1 second after start) ===")
# NOW we can take screenshots that will show content!
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"WORKING_screenshot_{timestamp}.png"
# Take screenshot - this should now show our red frame
result = automation.screenshot(filename)
print(f"Screenshot taken: {filename} - Result: {result}")
# Test clicking on the frame
automation.click(200, 200) # Click in center of red frame
# Test keyboard input
automation.typewrite("Hello from timer callback!")
# Take another screenshot to show any changes
filename2 = f"WORKING_screenshot_after_click_{timestamp}.png"
automation.screenshot(filename2)
print(f"Second screenshot: {filename2}")
print("Test completed successfully!")
print("\nThis works because:")
print("1. The game loop has been running for 1 second")
print("2. The scene has been rendered multiple times")
print("3. The RenderTexture now contains actual rendered content")
# Cancel this timer so it doesn't repeat
timer.stop()
# Optional: exit after a moment
def exit_game(t, r):
print("Exiting...")
mcrfpy.exit()
global exit_timer
exit_timer = mcrfpy.Timer("exit", exit_game, 500, once=True)
# This code runs during --exec script execution
print("=== Setting Up Test Scene ===")
@ -13,8 +49,6 @@ print("=== Setting Up Test Scene ===")
# Create scene with visible content
timer_test_scene = mcrfpy.Scene("timer_test_scene")
timer_test_scene.activate()
mcrfpy.step(0.01) # Initialize scene
ui = timer_test_scene.children
# Add a bright red frame that should be visible
@ -26,57 +60,23 @@ ui.append(frame)
# Add text
caption = mcrfpy.Caption(pos=(150, 150),
text="STEP TEST - SHOULD BE VISIBLE",
text="TIMER TEST - SHOULD BE VISIBLE",
fill_color=mcrfpy.Color(255, 255, 255))
caption.font_size = 24
frame.children.append(caption)
# Add click handler to demonstrate interaction
click_received = False
def frame_clicked(x, y, button):
global click_received
click_received = True
print(f"Frame clicked at ({x}, {y}) with button {button}")
frame.on_click = frame_clicked
print("Scene setup complete.")
print("Scene setup complete. Setting timer for automation tests...")
# Step to render the scene
mcrfpy.step(0.1)
# THIS IS THE KEY: Set timer to run AFTER the game loop starts
automation_test_timer = mcrfpy.Timer("automation_test", run_automation_tests, 1000, once=True)
print("\n=== Automation Test Running ===")
print("Timer set. Game loop will start after this script completes.")
print("Automation tests will run 1 second later when content is visible.")
# NOW we can take screenshots that will show content!
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"WORKING_screenshot_{timestamp}.png"
# Take screenshot - this should now show our red frame
result = automation.screenshot(filename)
print(f"Screenshot taken: {filename} - Result: {result}")
# Test clicking on the frame
automation.click(200, 200) # Click in center of red frame
# Step to process the click
mcrfpy.step(0.1)
# Test keyboard input
automation.typewrite("Hello from step-based test!")
# Step to process keyboard input
mcrfpy.step(0.1)
# Take another screenshot to show any changes
filename2 = f"WORKING_screenshot_after_click_{timestamp}.png"
automation.screenshot(filename2)
print(f"Second screenshot: {filename2}")
print("Test completed successfully!")
print("\nThis works because:")
print("1. mcrfpy.step() advances simulation synchronously")
print("2. The scene renders during step() calls")
print("3. The RenderTexture contains actual rendered content")
print("PASS")
sys.exit(0)
# Script ends here - game loop starts next

View file

@ -46,12 +46,10 @@ print("Test 3: Checking margin properties...")
try:
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
# Check default margins:
# - margin returns 0 when both horiz/vert are unset (effective default)
# - horiz_margin/vert_margin return -1 (sentinel for "not set")
assert frame.margin == 0.0, f"Expected margin=0 (effective default), got {frame.margin}"
assert frame.horiz_margin == -1.0, f"Expected horiz_margin=-1 (unset), got {frame.horiz_margin}"
assert frame.vert_margin == -1.0, f"Expected vert_margin=-1 (unset), got {frame.vert_margin}"
# Check default margins are 0
assert frame.margin == 0, f"Expected margin=0, got {frame.margin}"
assert frame.horiz_margin == 0, f"Expected horiz_margin=0, got {frame.horiz_margin}"
assert frame.vert_margin == 0, f"Expected vert_margin=0, got {frame.vert_margin}"
# Set margins when no alignment
frame.margin = 10.0

View file

@ -1,23 +1,28 @@
#!/usr/bin/env python3
"""Test to verify timer-based screenshots work using mcrfpy.step() for synchronous execution"""
"""Simplified test to verify timer-based screenshots work"""
import mcrfpy
from mcrfpy import automation
import sys
# Counter to track timer calls
call_count = 0
def take_screenshot(timer, runtime):
"""Timer callback that takes screenshot"""
def take_screenshot_and_exit(timer, runtime):
"""Timer callback that takes screenshot then exits"""
global call_count
call_count += 1
print(f"Timer callback fired! (call #{call_count}, runtime={runtime})")
print(f"\nTimer callback fired! (call #{call_count})")
# Take screenshot
filename = f"timer_screenshot_test_{call_count}.png"
result = automation.screenshot(filename)
print(f"Screenshot result: {result} -> {filename}")
# Exit after first call
if call_count >= 1:
print("Exiting game...")
mcrfpy.exit()
# Set up a simple scene
print("Creating test scene...")
test = mcrfpy.Scene("test")
@ -30,17 +35,6 @@ frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200),
ui.append(frame)
print("Setting timer to fire in 100ms...")
timer = mcrfpy.Timer("screenshot_timer", take_screenshot, 100, once=True)
print(f"Timer created: {timer}")
mcrfpy.Timer("screenshot_timer", take_screenshot_and_exit, 100, once=True)
# Use mcrfpy.step() to advance simulation synchronously instead of waiting
print("Advancing simulation by 200ms using step()...")
mcrfpy.step(0.2) # Advance 200ms - timer at 100ms should fire
# Verify timer fired
if call_count >= 1:
print("SUCCESS: Timer fired and screenshot taken!")
sys.exit(0)
else:
print(f"FAIL: Expected call_count >= 1, got {call_count}")
sys.exit(1)
print("Setup complete. Game loop starting...")

View file

@ -1,55 +1,73 @@
#!/usr/bin/env python3
"""Simple test for animation callbacks using mcrfpy.step() for synchronous execution"""
"""Simple test for animation callbacks - demonstrates basic usage"""
import mcrfpy
import sys
print("Animation Callback Demo")
print("=" * 30)
# Global state to track callback
callback_count = 0
callback_demo = None # Will be set in setup_and_run
def my_callback(anim, target):
"""Simple callback that prints when animation completes"""
global callback_count
callback_count += 1
print(f"Animation completed! Callback #{callback_count}")
# For now, anim and target are None - future enhancement
# Create scene
callback_demo = mcrfpy.Scene("callback_demo")
callback_demo.activate()
def setup_and_run():
"""Set up scene and run animation with callback"""
global callback_demo
# Create scene
callback_demo = mcrfpy.Scene("callback_demo")
callback_demo.activate()
# Create a frame to animate
frame = mcrfpy.Frame((100, 100), (200, 200), fill_color=(255, 0, 0))
ui = callback_demo.children
ui.append(frame)
# Create a frame to animate
frame = mcrfpy.Frame((100, 100), (200, 200), fill_color=(255, 0, 0))
ui = callback_demo.children
ui.append(frame)
# Test 1: Animation with callback
print("Starting animation with callback (1.0s duration)...")
anim = mcrfpy.Animation("x", 400.0, 1.0, "easeInOutQuad", callback=my_callback)
anim.start(frame)
# Create animation with callback
print("Starting animation with callback...")
anim = mcrfpy.Animation("x", 400.0, 1.0, "easeInOutQuad", callback=my_callback)
anim.start(frame)
# Use mcrfpy.step() to advance past animation completion
mcrfpy.step(1.5) # Advance 1.5 seconds - animation completes at 1.0s
# Schedule check after animation should complete
mcrfpy.Timer("check", check_result, 1500, once=True)
if callback_count != 1:
print(f"FAIL: Expected 1 callback, got {callback_count}")
sys.exit(1)
print("SUCCESS: Callback fired exactly once!")
def check_result(timer, runtime):
"""Check if callback fired correctly"""
global callback_count, callback_demo
# Test 2: Animation without callback
print("\nTesting animation without callback (0.5s duration)...")
anim2 = mcrfpy.Animation("y", 300.0, 0.5, "linear")
anim2.start(frame)
if callback_count == 1:
print("SUCCESS: Callback fired exactly once!")
# Advance past second animation
mcrfpy.step(0.7)
# Test 2: Animation without callback
print("\nTesting animation without callback...")
ui = callback_demo.children
frame = ui[0]
if callback_count != 1:
print(f"FAIL: Callback count changed to {callback_count}")
sys.exit(1)
anim2 = mcrfpy.Animation("y", 300.0, 0.5, "linear")
anim2.start(frame)
print("SUCCESS: No unexpected callbacks fired!")
print("\nAnimation callback feature working correctly!")
sys.exit(0)
mcrfpy.Timer("final", final_check, 700, once=True)
else:
print(f"FAIL: Expected 1 callback, got {callback_count}")
sys.exit(1)
def final_check(timer, runtime):
"""Final check - callback count should still be 1"""
global callback_count
if callback_count == 1:
print("SUCCESS: No unexpected callbacks fired!")
print("\nAnimation callback feature working correctly!")
sys.exit(0)
else:
print(f"FAIL: Callback count changed to {callback_count}")
sys.exit(1)
# Start the demo
print("Animation Callback Demo")
print("=" * 30)
setup_and_run()

View file

@ -210,7 +210,7 @@ def test_8_replace_completes_old():
test_result("Replace completes old animation", False, str(e))
def run_all_tests():
def run_all_tests(timer, runtime):
"""Run all property locking tests"""
print("\nRunning Animation Property Locking Tests...")
print("-" * 50)
@ -245,8 +245,5 @@ def run_all_tests():
test = mcrfpy.Scene("test")
test.activate()
# Use mcrfpy.step() to advance simulation for scene initialization
mcrfpy.step(0.1) # Brief step to initialize scene
# Run tests directly (no timer needed with step-based approach)
run_all_tests()
# Start tests after a brief delay to allow scene to initialize
mcrfpy.Timer("start", run_all_tests, 100, once=True)

View file

@ -2,7 +2,6 @@
"""
Test the RAII AnimationManager implementation.
This verifies that weak_ptr properly handles all crash scenarios.
Uses mcrfpy.step() for synchronous test execution.
"""
import mcrfpy
@ -20,14 +19,189 @@ def test_result(name, passed, details=""):
global tests_passed, tests_failed
if passed:
tests_passed += 1
result = f"PASS: {name}"
result = f" {name}"
else:
tests_failed += 1
result = f"FAIL: {name}: {details}"
result = f" {name}: {details}"
print(result)
test_results.append((name, passed, details))
# Setup scene
def test_1_basic_animation():
"""Test that basic animations still work"""
try:
ui = test.children
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
anim = mcrfpy.Animation("x", 200.0, 1000, "linear")
anim.start(frame)
# Check if animation has valid target
if hasattr(anim, 'hasValidTarget'):
valid = anim.hasValidTarget()
test_result("Basic animation with hasValidTarget", valid)
else:
test_result("Basic animation", True)
except Exception as e:
test_result("Basic animation", False, str(e))
def test_2_remove_animated_object():
"""Test removing object with active animation"""
try:
ui = test.children
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
# Start animation
anim = mcrfpy.Animation("x", 500.0, 2000, "easeInOut")
anim.start(frame)
# Remove the frame
ui.remove(0)
# Check if animation knows target is gone
if hasattr(anim, 'hasValidTarget'):
valid = anim.hasValidTarget()
test_result("Animation detects removed target", not valid)
else:
# If method doesn't exist, just check we didn't crash
test_result("Remove animated object", True)
except Exception as e:
test_result("Remove animated object", False, str(e))
def test_3_complete_animation():
"""Test completing animation immediately"""
try:
ui = test.children
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
# Start animation
anim = mcrfpy.Animation("x", 500.0, 2000, "linear")
anim.start(frame)
# Complete it
if hasattr(anim, 'complete'):
anim.complete()
# Frame should now be at x=500
test_result("Animation complete method", True)
else:
test_result("Animation complete method", True, "Method not available")
except Exception as e:
test_result("Animation complete method", False, str(e))
def test_4_multiple_animations_timer():
"""Test creating multiple animations in timer callback"""
success = False
def create_animations(timer, runtime):
nonlocal success
try:
ui = test.children
frame = mcrfpy.Frame(pos=(200, 200), size=(100, 100))
ui.append(frame)
# Create multiple animations rapidly (this used to crash)
for i in range(10):
anim = mcrfpy.Animation("x", 300.0 + i * 10, 1000, "linear")
anim.start(frame)
success = True
except Exception as e:
print(f"Timer animation error: {e}")
finally:
mcrfpy.Timer("exit", lambda t, r: None, 100, once=True)
# Clear scene
ui = test.children
while len(ui) > 0:
ui.remove(len(ui) - 1)
mcrfpy.Timer("test", create_animations, 50, once=True)
mcrfpy.Timer("check", lambda t, r: test_result("Multiple animations in timer", success), 200, once=True)
def test_5_scene_cleanup():
"""Test that changing scenes cleans up animations"""
try:
# Create a second scene
test2 = mcrfpy.Scene("test2")
# Add animated objects to first scene
ui = test.children
for i in range(5):
frame = mcrfpy.Frame(pos=(50 * i, 100), size=(40, 40))
ui.append(frame)
anim = mcrfpy.Animation("y", 300.0, 2000, "easeOutBounce")
anim.start(frame)
# Switch scenes (animations should become invalid)
test2.activate()
# Switch back
test.activate()
test_result("Scene change cleanup", True)
except Exception as e:
test_result("Scene change cleanup", False, str(e))
def test_6_animation_after_clear():
"""Test animations after clearing UI"""
try:
ui = test.children
# Create and animate
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
anim = mcrfpy.Animation("w", 200.0, 1500, "easeInOutCubic")
anim.start(frame)
# Clear all UI
while len(ui) > 0:
ui.remove(len(ui) - 1)
# Animation should handle this gracefully
if hasattr(anim, 'hasValidTarget'):
valid = anim.hasValidTarget()
test_result("Animation after UI clear", not valid)
else:
test_result("Animation after UI clear", True)
except Exception as e:
test_result("Animation after UI clear", False, str(e))
def run_all_tests(timer, runtime):
"""Run all RAII tests"""
print("\nRunning RAII Animation Tests...")
print("-" * 40)
test_1_basic_animation()
test_2_remove_animated_object()
test_3_complete_animation()
test_4_multiple_animations_timer()
test_5_scene_cleanup()
test_6_animation_after_clear()
# Schedule result summary
mcrfpy.Timer("results", print_results, 500, once=True)
def print_results(timer, runtime):
"""Print test results"""
print("\n" + "=" * 40)
print(f"Tests passed: {tests_passed}")
print(f"Tests failed: {tests_failed}")
if tests_failed == 0:
print("\n+ All tests passed! RAII implementation is working correctly.")
else:
print(f"\nx {tests_failed} tests failed.")
print("\nFailed tests:")
for name, passed, details in test_results:
if not passed:
print(f" - {name}: {details}")
# Exit
mcrfpy.Timer("exit", lambda t, r: sys.exit(0 if tests_failed == 0 else 1), 500, once=True)
# Setup and run
test = mcrfpy.Scene("test")
test.activate()
@ -37,125 +211,5 @@ bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768))
bg.fill_color = mcrfpy.Color(20, 20, 30)
ui.append(bg)
# Initialize scene
mcrfpy.step(0.1)
print("\nRunning RAII Animation Tests...")
print("-" * 40)
# Test 1: Basic animation
try:
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
anim = mcrfpy.Animation("x", 200.0, 1000, "linear")
anim.start(frame)
if hasattr(anim, 'hasValidTarget'):
valid = anim.hasValidTarget()
test_result("Basic animation with hasValidTarget", valid)
else:
test_result("Basic animation", True)
except Exception as e:
test_result("Basic animation", False, str(e))
# Test 2: Remove animated object
try:
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
anim = mcrfpy.Animation("x", 500.0, 2000, "easeInOut")
anim.start(frame)
ui.remove(frame)
if hasattr(anim, 'hasValidTarget'):
valid = anim.hasValidTarget()
test_result("Animation detects removed target", not valid)
else:
test_result("Remove animated object", True)
except Exception as e:
test_result("Remove animated object", False, str(e))
# Test 3: Complete animation immediately
try:
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
anim = mcrfpy.Animation("x", 500.0, 2000, "linear")
anim.start(frame)
if hasattr(anim, 'complete'):
anim.complete()
test_result("Animation complete method", True)
else:
test_result("Animation complete method", True, "Method not available")
except Exception as e:
test_result("Animation complete method", False, str(e))
# Test 4: Multiple animations rapidly
try:
frame = mcrfpy.Frame(pos=(200, 200), size=(100, 100))
ui.append(frame)
for i in range(10):
anim = mcrfpy.Animation("x", 300.0 + i * 10, 1000, "linear")
anim.start(frame)
test_result("Multiple animations rapidly", True)
except Exception as e:
test_result("Multiple animations rapidly", False, str(e))
# Test 5: Scene cleanup
try:
test2 = mcrfpy.Scene("test2")
for i in range(5):
frame = mcrfpy.Frame(pos=(50 * i, 100), size=(40, 40))
ui.append(frame)
anim = mcrfpy.Animation("y", 300.0, 2000, "easeOutBounce")
anim.start(frame)
test2.activate()
mcrfpy.step(0.1)
test.activate()
mcrfpy.step(0.1)
test_result("Scene change cleanup", True)
except Exception as e:
test_result("Scene change cleanup", False, str(e))
# Test 6: Animation after clearing UI
try:
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
anim = mcrfpy.Animation("w", 200.0, 1500, "easeInOutCubic")
anim.start(frame)
# Clear all UI except background - iterate in reverse
for i in range(len(ui) - 1, 0, -1):
ui.remove(ui[i])
if hasattr(anim, 'hasValidTarget'):
valid = anim.hasValidTarget()
test_result("Animation after UI clear", not valid)
else:
test_result("Animation after UI clear", True)
except Exception as e:
test_result("Animation after UI clear", False, str(e))
# Print results
print("\n" + "=" * 40)
print(f"Tests passed: {tests_passed}")
print(f"Tests failed: {tests_failed}")
if tests_failed == 0:
print("\nAll tests passed! RAII implementation is working correctly.")
else:
print(f"\n{tests_failed} tests failed.")
print("\nFailed tests:")
for name, passed, details in test_results:
if not passed:
print(f" - {name}: {details}")
sys.exit(0 if tests_failed == 0 else 1)
# Start tests
start_timer = mcrfpy.Timer("start", run_all_tests, 100, once=True)

View file

@ -1,14 +1,40 @@
#!/usr/bin/env python3
"""
Test if the crash is related to removing animated objects.
Uses mcrfpy.step() for synchronous test execution.
Test if the crash is related to removing animated objects
"""
import mcrfpy
import sys
print("Animation Removal Test")
print("=" * 40)
def clear_and_recreate(timer, runtime):
"""Clear UI and recreate - mimics demo switching"""
print(f"\nTimer called at {runtime}")
ui = test.children
# Remove all but first 2 items (like clear_demo_objects)
print(f"Scene has {len(ui)} elements before clearing")
while len(ui) > 2:
ui.remove(len(ui)-1)
print(f"Scene has {len(ui)} elements after clearing")
# Create new animated objects
print("Creating new animated objects...")
for i in range(5):
f = mcrfpy.Frame(100 + i*50, 200, 40, 40)
f.fill_color = mcrfpy.Color(100 + i*30, 50, 200)
ui.append(f)
# Start animation on the new frame
target_x = 300 + i * 50
anim = mcrfpy.Animation("x", float(target_x), 1.0, "easeInOut")
anim.start(f)
print("New objects created and animated")
# Schedule exit
global exit_timer
exit_timer = mcrfpy.Timer("exit", lambda t, r: sys.exit(0), 2000, once=True)
# Create initial scene
print("Creating scene...")
@ -21,61 +47,20 @@ title = mcrfpy.Caption(pos=(400, 20), text="Test Title")
subtitle = mcrfpy.Caption(pos=(400, 50), text="Test Subtitle")
ui.extend([title, subtitle])
# Initialize scene
mcrfpy.step(0.1)
# Create initial animated objects
print("Creating initial animated objects...")
initial_frames = []
for i in range(10):
f = mcrfpy.Frame(pos=(50 + i*30, 100), size=(25, 25))
f.fill_color = mcrfpy.Color(255, 100, 100)
ui.append(f)
initial_frames.append(f)
# Animate them
anim = mcrfpy.Animation("y", 300.0, 2.0, "easeOutBounce")
anim.start(f)
print(f"Initial scene has {len(ui)} elements")
# Let animations run a bit
mcrfpy.step(0.5)
# Schedule the clear and recreate
switch_timer = mcrfpy.Timer("switch", clear_and_recreate, 1000, once=True)
# Clear and recreate - mimics demo switching
print("\nClearing and recreating...")
print(f"Scene has {len(ui)} elements before clearing")
# Remove all but first 2 items (like clear_demo_objects)
# Use reverse iteration to remove by element
while len(ui) > 2:
ui.remove(ui[-1])
print(f"Scene has {len(ui)} elements after clearing")
# Create new animated objects
print("Creating new animated objects...")
for i in range(5):
f = mcrfpy.Frame(pos=(100 + i*50, 200), size=(40, 40))
f.fill_color = mcrfpy.Color(100 + i*30, 50, 200)
ui.append(f)
# Start animation on the new frame
target_x = 300 + i * 50
anim = mcrfpy.Animation("x", float(target_x), 1.0, "easeInOut")
anim.start(f)
print("New objects created and animated")
print(f"Scene now has {len(ui)} elements")
# Let new animations run
mcrfpy.step(1.5)
# Final check
print(f"\nFinal scene has {len(ui)} elements")
if len(ui) == 7: # 2 captions + 5 new frames
print("SUCCESS: Animation removal test passed!")
sys.exit(0)
else:
print(f"FAIL: Expected 7 elements, got {len(ui)}")
sys.exit(1)
print("\nEntering game loop...")

View file

@ -1,181 +1,182 @@
#!/usr/bin/env python3
"""
Test #94: Color helper methods - from_hex, to_hex, lerp
Refactored to use mcrfpy.step() for synchronous execution.
"""
import mcrfpy
import sys
# Initialize scene
def test_color_helpers(timer, runtime):
"""Test Color helper methods"""
all_pass = True
# Test 1: from_hex with # prefix
try:
c1 = mcrfpy.Color.from_hex("#FF0000")
assert c1.r == 255 and c1.g == 0 and c1.b == 0 and c1.a == 255, f"from_hex('#FF0000') failed: {c1}"
print("+ Color.from_hex('#FF0000') works")
except Exception as e:
print(f"x Color.from_hex('#FF0000') failed: {e}")
all_pass = False
# Test 2: from_hex without # prefix
try:
c2 = mcrfpy.Color.from_hex("00FF00")
assert c2.r == 0 and c2.g == 255 and c2.b == 0 and c2.a == 255, f"from_hex('00FF00') failed: {c2}"
print("+ Color.from_hex('00FF00') works")
except Exception as e:
print(f"x Color.from_hex('00FF00') failed: {e}")
all_pass = False
# Test 3: from_hex with alpha
try:
c3 = mcrfpy.Color.from_hex("#0000FF80")
assert c3.r == 0 and c3.g == 0 and c3.b == 255 and c3.a == 128, f"from_hex('#0000FF80') failed: {c3}"
print("+ Color.from_hex('#0000FF80') with alpha works")
except Exception as e:
print(f"x Color.from_hex('#0000FF80') failed: {e}")
all_pass = False
# Test 4: from_hex error handling
try:
c4 = mcrfpy.Color.from_hex("GGGGGG")
print("x from_hex should fail on invalid hex")
all_pass = False
except ValueError as e:
print("+ Color.from_hex() correctly rejects invalid hex")
# Test 5: from_hex wrong length
try:
c5 = mcrfpy.Color.from_hex("FF00")
print("x from_hex should fail on wrong length")
all_pass = False
except ValueError as e:
print("+ Color.from_hex() correctly rejects wrong length")
# Test 6: to_hex without alpha
try:
c6 = mcrfpy.Color(255, 128, 64)
hex_str = c6.to_hex()
assert hex_str == "#FF8040", f"to_hex() failed: {hex_str}"
print("+ Color.to_hex() works")
except Exception as e:
print(f"x Color.to_hex() failed: {e}")
all_pass = False
# Test 7: to_hex with alpha
try:
c7 = mcrfpy.Color(255, 128, 64, 127)
hex_str = c7.to_hex()
assert hex_str == "#FF80407F", f"to_hex() with alpha failed: {hex_str}"
print("+ Color.to_hex() with alpha works")
except Exception as e:
print(f"x Color.to_hex() with alpha failed: {e}")
all_pass = False
# Test 8: Round-trip hex conversion
try:
original_hex = "#ABCDEF"
c8 = mcrfpy.Color.from_hex(original_hex)
result_hex = c8.to_hex()
assert result_hex == original_hex, f"Round-trip failed: {original_hex} -> {result_hex}"
print("+ Hex round-trip conversion works")
except Exception as e:
print(f"x Hex round-trip failed: {e}")
all_pass = False
# Test 9: lerp at t=0
try:
red = mcrfpy.Color(255, 0, 0)
blue = mcrfpy.Color(0, 0, 255)
result = red.lerp(blue, 0.0)
assert result.r == 255 and result.g == 0 and result.b == 0, f"lerp(t=0) failed: {result}"
print("+ Color.lerp(t=0) returns start color")
except Exception as e:
print(f"x Color.lerp(t=0) failed: {e}")
all_pass = False
# Test 10: lerp at t=1
try:
red = mcrfpy.Color(255, 0, 0)
blue = mcrfpy.Color(0, 0, 255)
result = red.lerp(blue, 1.0)
assert result.r == 0 and result.g == 0 and result.b == 255, f"lerp(t=1) failed: {result}"
print("+ Color.lerp(t=1) returns end color")
except Exception as e:
print(f"x Color.lerp(t=1) failed: {e}")
all_pass = False
# Test 11: lerp at t=0.5
try:
red = mcrfpy.Color(255, 0, 0)
blue = mcrfpy.Color(0, 0, 255)
result = red.lerp(blue, 0.5)
# Expect roughly (127, 0, 127)
assert 126 <= result.r <= 128 and result.g == 0 and 126 <= result.b <= 128, f"lerp(t=0.5) failed: {result}"
print("+ Color.lerp(t=0.5) returns midpoint")
except Exception as e:
print(f"x Color.lerp(t=0.5) failed: {e}")
all_pass = False
# Test 12: lerp with alpha
try:
c1 = mcrfpy.Color(255, 0, 0, 255)
c2 = mcrfpy.Color(0, 255, 0, 0)
result = c1.lerp(c2, 0.5)
assert 126 <= result.r <= 128 and 126 <= result.g <= 128 and result.b == 0, f"lerp color components failed"
assert 126 <= result.a <= 128, f"lerp alpha failed: {result.a}"
print("+ Color.lerp() with alpha works")
except Exception as e:
print(f"x Color.lerp() with alpha failed: {e}")
all_pass = False
# Test 13: lerp clamps t < 0
try:
red = mcrfpy.Color(255, 0, 0)
blue = mcrfpy.Color(0, 0, 255)
result = red.lerp(blue, -0.5)
assert result.r == 255 and result.g == 0 and result.b == 0, f"lerp(t<0) should clamp to 0"
print("+ Color.lerp() clamps t < 0")
except Exception as e:
print(f"x Color.lerp(t<0) failed: {e}")
all_pass = False
# Test 14: lerp clamps t > 1
try:
red = mcrfpy.Color(255, 0, 0)
blue = mcrfpy.Color(0, 0, 255)
result = red.lerp(blue, 1.5)
assert result.r == 0 and result.g == 0 and result.b == 255, f"lerp(t>1) should clamp to 1"
print("+ Color.lerp() clamps t > 1")
except Exception as e:
print(f"x Color.lerp(t>1) failed: {e}")
all_pass = False
# Test 15: Practical use case - gradient
try:
start = mcrfpy.Color.from_hex("#FF0000") # Red
end = mcrfpy.Color.from_hex("#0000FF") # Blue
# Create 5-step gradient
steps = []
for i in range(5):
t = i / 4.0
color = start.lerp(end, t)
steps.append(color.to_hex())
assert steps[0] == "#FF0000", "Gradient start should be red"
assert steps[4] == "#0000FF", "Gradient end should be blue"
assert len(set(steps)) == 5, "All gradient steps should be unique"
print("+ Gradient generation works correctly")
except Exception as e:
print(f"x Gradient generation failed: {e}")
all_pass = False
print(f"\n{'PASS' if all_pass else 'FAIL'}")
sys.exit(0 if all_pass else 1)
# Run test
test = mcrfpy.Scene("test")
test.activate()
mcrfpy.step(0.01)
all_pass = True
# Test 1: from_hex with # prefix
try:
c1 = mcrfpy.Color.from_hex("#FF0000")
assert c1.r == 255 and c1.g == 0 and c1.b == 0 and c1.a == 255, f"from_hex('#FF0000') failed: {c1}"
print("+ Color.from_hex('#FF0000') works")
except Exception as e:
print(f"x Color.from_hex('#FF0000') failed: {e}")
all_pass = False
# Test 2: from_hex without # prefix
try:
c2 = mcrfpy.Color.from_hex("00FF00")
assert c2.r == 0 and c2.g == 255 and c2.b == 0 and c2.a == 255, f"from_hex('00FF00') failed: {c2}"
print("+ Color.from_hex('00FF00') works")
except Exception as e:
print(f"x Color.from_hex('00FF00') failed: {e}")
all_pass = False
# Test 3: from_hex with alpha
try:
c3 = mcrfpy.Color.from_hex("#0000FF80")
assert c3.r == 0 and c3.g == 0 and c3.b == 255 and c3.a == 128, f"from_hex('#0000FF80') failed: {c3}"
print("+ Color.from_hex('#0000FF80') with alpha works")
except Exception as e:
print(f"x Color.from_hex('#0000FF80') failed: {e}")
all_pass = False
# Test 4: from_hex error handling
try:
c4 = mcrfpy.Color.from_hex("GGGGGG")
print("x from_hex should fail on invalid hex")
all_pass = False
except ValueError as e:
print("+ Color.from_hex() correctly rejects invalid hex")
# Test 5: from_hex wrong length
try:
c5 = mcrfpy.Color.from_hex("FF00")
print("x from_hex should fail on wrong length")
all_pass = False
except ValueError as e:
print("+ Color.from_hex() correctly rejects wrong length")
# Test 6: to_hex without alpha
try:
c6 = mcrfpy.Color(255, 128, 64)
hex_str = c6.to_hex()
assert hex_str == "#FF8040", f"to_hex() failed: {hex_str}"
print("+ Color.to_hex() works")
except Exception as e:
print(f"x Color.to_hex() failed: {e}")
all_pass = False
# Test 7: to_hex with alpha
try:
c7 = mcrfpy.Color(255, 128, 64, 127)
hex_str = c7.to_hex()
assert hex_str == "#FF80407F", f"to_hex() with alpha failed: {hex_str}"
print("+ Color.to_hex() with alpha works")
except Exception as e:
print(f"x Color.to_hex() with alpha failed: {e}")
all_pass = False
# Test 8: Round-trip hex conversion
try:
original_hex = "#ABCDEF"
c8 = mcrfpy.Color.from_hex(original_hex)
result_hex = c8.to_hex()
assert result_hex == original_hex, f"Round-trip failed: {original_hex} -> {result_hex}"
print("+ Hex round-trip conversion works")
except Exception as e:
print(f"x Hex round-trip failed: {e}")
all_pass = False
# Test 9: lerp at t=0
try:
red = mcrfpy.Color(255, 0, 0)
blue = mcrfpy.Color(0, 0, 255)
result = red.lerp(blue, 0.0)
assert result.r == 255 and result.g == 0 and result.b == 0, f"lerp(t=0) failed: {result}"
print("+ Color.lerp(t=0) returns start color")
except Exception as e:
print(f"x Color.lerp(t=0) failed: {e}")
all_pass = False
# Test 10: lerp at t=1
try:
red = mcrfpy.Color(255, 0, 0)
blue = mcrfpy.Color(0, 0, 255)
result = red.lerp(blue, 1.0)
assert result.r == 0 and result.g == 0 and result.b == 255, f"lerp(t=1) failed: {result}"
print("+ Color.lerp(t=1) returns end color")
except Exception as e:
print(f"x Color.lerp(t=1) failed: {e}")
all_pass = False
# Test 11: lerp at t=0.5
try:
red = mcrfpy.Color(255, 0, 0)
blue = mcrfpy.Color(0, 0, 255)
result = red.lerp(blue, 0.5)
# Expect roughly (127, 0, 127)
assert 126 <= result.r <= 128 and result.g == 0 and 126 <= result.b <= 128, f"lerp(t=0.5) failed: {result}"
print("+ Color.lerp(t=0.5) returns midpoint")
except Exception as e:
print(f"x Color.lerp(t=0.5) failed: {e}")
all_pass = False
# Test 12: lerp with alpha
try:
c1 = mcrfpy.Color(255, 0, 0, 255)
c2 = mcrfpy.Color(0, 255, 0, 0)
result = c1.lerp(c2, 0.5)
assert 126 <= result.r <= 128 and 126 <= result.g <= 128 and result.b == 0, f"lerp color components failed"
assert 126 <= result.a <= 128, f"lerp alpha failed: {result.a}"
print("+ Color.lerp() with alpha works")
except Exception as e:
print(f"x Color.lerp() with alpha failed: {e}")
all_pass = False
# Test 13: lerp clamps t < 0
try:
red = mcrfpy.Color(255, 0, 0)
blue = mcrfpy.Color(0, 0, 255)
result = red.lerp(blue, -0.5)
assert result.r == 255 and result.g == 0 and result.b == 0, f"lerp(t<0) should clamp to 0"
print("+ Color.lerp() clamps t < 0")
except Exception as e:
print(f"x Color.lerp(t<0) failed: {e}")
all_pass = False
# Test 14: lerp clamps t > 1
try:
red = mcrfpy.Color(255, 0, 0)
blue = mcrfpy.Color(0, 0, 255)
result = red.lerp(blue, 1.5)
assert result.r == 0 and result.g == 0 and result.b == 255, f"lerp(t>1) should clamp to 1"
print("+ Color.lerp() clamps t > 1")
except Exception as e:
print(f"x Color.lerp(t>1) failed: {e}")
all_pass = False
# Test 15: Practical use case - gradient
try:
start = mcrfpy.Color.from_hex("#FF0000") # Red
end = mcrfpy.Color.from_hex("#0000FF") # Blue
# Create 5-step gradient
steps = []
for i in range(5):
t = i / 4.0
color = start.lerp(end, t)
steps.append(color.to_hex())
assert steps[0] == "#FF0000", "Gradient start should be red"
assert steps[4] == "#0000FF", "Gradient end should be blue"
assert len(set(steps)) == 5, "All gradient steps should be unique"
print("+ Gradient generation works correctly")
except Exception as e:
print(f"x Gradient generation failed: {e}")
all_pass = False
print(f"\n{'PASS' if all_pass else 'FAIL'}")
sys.exit(0 if all_pass else 1)
test_timer = mcrfpy.Timer("test", test_color_helpers, 100, once=True)

View file

@ -1,28 +1,20 @@
#!/usr/bin/env python3
"""
Test if AnimationManager crashes with no animations
Refactored to use mcrfpy.step() for synchronous execution.
"""
import mcrfpy
import sys
print("Creating empty scene...")
test = mcrfpy.Scene("test")
test.activate()
print("Scene created, no animations added")
print("Advancing simulation with step()...")
print("Starting game loop in 100ms...")
# Step multiple times to simulate game loop running
# If AnimationManager crashes with empty state, this will fail
try:
for i in range(10):
mcrfpy.step(0.1) # 10 steps of 0.1s = 1 second simulated
def check_alive(timer, runtime):
print(f"Timer fired at {runtime}ms - AnimationManager survived!")
mcrfpy.Timer("exit", lambda t, r: mcrfpy.exit(), 100, once=True)
print("AnimationManager survived 10 steps with no animations!")
print("PASS")
sys.exit(0)
except Exception as e:
print(f"FAIL: AnimationManager crashed: {e}")
sys.exit(1)
mcrfpy.Timer("check", check_alive, 1000, once=True)
print("If this crashes immediately, AnimationManager has an issue with empty state")

View file

@ -1,118 +1,135 @@
#!/usr/bin/env python3
"""Test UIFrame clipping functionality
Refactored to use mcrfpy.step() for synchronous execution.
"""
"""Test UIFrame clipping functionality"""
import mcrfpy
from mcrfpy import Color, Frame, Caption, automation
from mcrfpy import Color, Frame, Caption
import sys
# Module-level state to avoid closures
_test_state = {}
def take_second_screenshot(timer, runtime):
"""Take final screenshot and exit"""
timer.stop()
from mcrfpy import automation
automation.screenshot("frame_clipping_animated.png")
print("\nTest completed successfully!")
print("Screenshots saved:")
print(" - frame_clipping_test.png (initial state)")
print(" - frame_clipping_animated.png (with animation)")
sys.exit(0)
def animate_frames(timer, runtime):
"""Animate frames to demonstrate clipping"""
timer.stop()
scene = test.children
# Move child frames
parent1 = scene[0]
parent2 = scene[1]
parent1.children[1].x = 50
parent2.children[1].x = 50
global screenshot2_timer
screenshot2_timer = mcrfpy.Timer("screenshot2", take_second_screenshot, 500, once=True)
def test_clipping(timer, runtime):
"""Test that clip_children property works correctly"""
timer.stop()
print("Testing UIFrame clipping functionality...")
scene = test.children
# Create parent frame with clipping disabled (default)
parent1 = Frame(pos=(50, 50), size=(200, 150),
fill_color=Color(100, 100, 200),
outline_color=Color(255, 255, 255),
outline=2)
parent1.name = "parent1"
scene.append(parent1)
# Create parent frame with clipping enabled
parent2 = Frame(pos=(300, 50), size=(200, 150),
fill_color=Color(200, 100, 100),
outline_color=Color(255, 255, 255),
outline=2)
parent2.name = "parent2"
parent2.clip_children = True
scene.append(parent2)
# Add captions to both frames
caption1 = Caption(text="This text should overflow the frame bounds", pos=(10, 10))
caption1.font_size = 16
caption1.fill_color = Color(255, 255, 255)
parent1.children.append(caption1)
caption2 = Caption(text="This text should be clipped to frame bounds", pos=(10, 10))
caption2.font_size = 16
caption2.fill_color = Color(255, 255, 255)
parent2.children.append(caption2)
# Add child frames that extend beyond parent bounds
child1 = Frame(pos=(150, 100), size=(100, 100),
fill_color=Color(50, 255, 50),
outline_color=Color(0, 0, 0),
outline=1)
parent1.children.append(child1)
child2 = Frame(pos=(150, 100), size=(100, 100),
fill_color=Color(50, 255, 50),
outline_color=Color(0, 0, 0),
outline=1)
parent2.children.append(child2)
# Add caption to show clip state
status = Caption(text=f"Left frame: clip_children={parent1.clip_children}\n"
f"Right frame: clip_children={parent2.clip_children}",
pos=(50, 250))
status.font_size = 14
status.fill_color = Color(255, 255, 255)
scene.append(status)
# Add instructions
instructions = Caption(text="Left: Children should overflow (no clipping)\n"
"Right: Children should be clipped to frame bounds\n"
"Press 'c' to toggle clipping on left frame",
pos=(50, 300))
instructions.font_size = 12
instructions.fill_color = Color(200, 200, 200)
scene.append(instructions)
# Take screenshot
from mcrfpy import automation
automation.screenshot("frame_clipping_test.png")
print(f"Parent1 clip_children: {parent1.clip_children}")
print(f"Parent2 clip_children: {parent2.clip_children}")
# Test toggling clip_children
parent1.clip_children = True
print(f"After toggle - Parent1 clip_children: {parent1.clip_children}")
# Verify the property setter works
try:
parent1.clip_children = "not a bool"
print("ERROR: clip_children accepted non-boolean value")
except TypeError as e:
print(f"PASS: clip_children correctly rejected non-boolean: {e}")
# Start animation after a short delay
global animate_timer
animate_timer = mcrfpy.Timer("animate", animate_frames, 100, once=True)
def handle_keypress(key, modifiers):
if key == "c":
scene = test.children
parent1 = scene[0]
parent1.clip_children = not parent1.clip_children
print(f"Toggled parent1 clip_children to: {parent1.clip_children}")
# Main execution
print("Creating test scene...")
test = mcrfpy.Scene("test")
test.activate()
mcrfpy.step(0.01) # Initialize
print("Testing UIFrame clipping functionality...")
scene = test.children
# Create parent frame with clipping disabled (default)
parent1 = Frame(pos=(50, 50), size=(200, 150),
fill_color=Color(100, 100, 200),
outline_color=Color(255, 255, 255),
outline=2)
parent1.name = "parent1"
scene.append(parent1)
# Create parent frame with clipping enabled
parent2 = Frame(pos=(300, 50), size=(200, 150),
fill_color=Color(200, 100, 100),
outline_color=Color(255, 255, 255),
outline=2)
parent2.name = "parent2"
parent2.clip_children = True
scene.append(parent2)
# Add captions to both frames
caption1 = Caption(text="This text should overflow the frame bounds", pos=(10, 10))
caption1.font_size = 16
caption1.fill_color = Color(255, 255, 255)
parent1.children.append(caption1)
caption2 = Caption(text="This text should be clipped to frame bounds", pos=(10, 10))
caption2.font_size = 16
caption2.fill_color = Color(255, 255, 255)
parent2.children.append(caption2)
# Add child frames that extend beyond parent bounds
child1 = Frame(pos=(150, 100), size=(100, 100),
fill_color=Color(50, 255, 50),
outline_color=Color(0, 0, 0),
outline=1)
parent1.children.append(child1)
child2 = Frame(pos=(150, 100), size=(100, 100),
fill_color=Color(50, 255, 50),
outline_color=Color(0, 0, 0),
outline=1)
parent2.children.append(child2)
# Add caption to show clip state
status = Caption(text=f"Left frame: clip_children={parent1.clip_children}\n"
f"Right frame: clip_children={parent2.clip_children}",
pos=(50, 250))
status.font_size = 14
status.fill_color = Color(255, 255, 255)
scene.append(status)
# Add instructions
instructions = Caption(text="Left: Children should overflow (no clipping)\n"
"Right: Children should be clipped to frame bounds",
pos=(50, 300))
instructions.font_size = 12
instructions.fill_color = Color(200, 200, 200)
scene.append(instructions)
# Step to render
mcrfpy.step(0.1)
# Take screenshot
automation.screenshot("frame_clipping_test.png")
print(f"Parent1 clip_children: {parent1.clip_children}")
print(f"Parent2 clip_children: {parent2.clip_children}")
# Test toggling clip_children
parent1.clip_children = True
print(f"After toggle - Parent1 clip_children: {parent1.clip_children}")
# Verify the property setter works
test_passed = True
try:
parent1.clip_children = "not a bool"
print("ERROR: clip_children accepted non-boolean value")
test_passed = False
except TypeError as e:
print(f"PASS: clip_children correctly rejected non-boolean: {e}")
# Animate frames (move children)
parent1.children[1].x = 50
parent2.children[1].x = 50
# Step to render animation
mcrfpy.step(0.1)
# Take second screenshot
automation.screenshot("frame_clipping_animated.png")
print("\nTest completed successfully!")
print("Screenshots saved:")
print(" - frame_clipping_test.png (initial state)")
print(" - frame_clipping_animated.png (with animation)")
if test_passed:
print("PASS")
sys.exit(0)
else:
print("FAIL")
sys.exit(1)
test.on_key = handle_keypress
test_clipping_timer = mcrfpy.Timer("test_clipping", test_clipping, 100, once=True)
print("Test scheduled, running...")

View file

@ -1,95 +1,105 @@
#!/usr/bin/env python3
"""Advanced test for UIFrame clipping with nested frames
Refactored to use mcrfpy.step() for synchronous execution.
"""
"""Advanced test for UIFrame clipping with nested frames"""
import mcrfpy
from mcrfpy import Color, Frame, Caption, Vector, automation
from mcrfpy import Color, Frame, Caption, Vector
import sys
def test_nested_clipping(timer, runtime):
"""Test nested frames with clipping"""
timer.stop()
print("Testing advanced UIFrame clipping with nested frames...")
# Create test scene
scene = test.children
# Create outer frame with clipping enabled
outer = Frame(pos=(50, 50), size=(400, 300),
fill_color=Color(50, 50, 150),
outline_color=Color(255, 255, 255),
outline=3)
outer.name = "outer"
outer.clip_children = True
scene.append(outer)
# Create inner frame that extends beyond outer bounds
inner = Frame(pos=(200, 150), size=(300, 200),
fill_color=Color(150, 50, 50),
outline_color=Color(255, 255, 0),
outline=2)
inner.name = "inner"
inner.clip_children = True # Also enable clipping on inner frame
outer.children.append(inner)
# Add content to inner frame that extends beyond its bounds
for i in range(5):
caption = Caption(text=f"Line {i+1}: This text should be double-clipped", pos=(10, 30 * i))
caption.font_size = 14
caption.fill_color = Color(255, 255, 255)
inner.children.append(caption)
# Add a child frame to inner that extends way out
deeply_nested = Frame(pos=(250, 100), size=(200, 150),
fill_color=Color(50, 150, 50),
outline_color=Color(255, 0, 255),
outline=2)
deeply_nested.name = "deeply_nested"
inner.children.append(deeply_nested)
# Add status text
status = Caption(text="Nested clipping test:\n"
"- Blue outer frame clips red inner frame\n"
"- Red inner frame clips green deeply nested frame\n"
"- All text should be clipped to frame bounds",
pos=(50, 380))
status.font_size = 12
status.fill_color = Color(200, 200, 200)
scene.append(status)
# Test render texture size handling
print(f"Outer frame size: {outer.w}x{outer.h}")
print(f"Inner frame size: {inner.w}x{inner.h}")
# Dynamically resize frames to test RenderTexture recreation
def resize_test(timer, runtime):
timer.stop()
print("Resizing frames to test RenderTexture recreation...")
outer.w = 450
outer.h = 350
inner.w = 350
inner.h = 250
print(f"New outer frame size: {outer.w}x{outer.h}")
print(f"New inner frame size: {inner.w}x{inner.h}")
# Take screenshot after resize
global screenshot_resize_timer
screenshot_resize_timer = mcrfpy.Timer("screenshot_resize", take_resize_screenshot, 500, once=True)
def take_resize_screenshot(timer, runtime):
timer.stop()
from mcrfpy import automation
automation.screenshot("frame_clipping_resized.png")
print("\nAdvanced test completed!")
print("Screenshots saved:")
print(" - frame_clipping_resized.png (after resize)")
sys.exit(0)
# Take initial screenshot
from mcrfpy import automation
automation.screenshot("frame_clipping_nested.png")
print("Initial screenshot saved: frame_clipping_nested.png")
# Schedule resize test
global resize_test_timer
resize_test_timer = mcrfpy.Timer("resize_test", resize_test, 1000, once=True)
# Main execution
print("Creating advanced test scene...")
test = mcrfpy.Scene("test")
test.activate()
mcrfpy.step(0.01)
print("Testing advanced UIFrame clipping with nested frames...")
# Schedule the test
test_nested_clipping_timer = mcrfpy.Timer("test_nested_clipping", test_nested_clipping, 100, once=True)
# Create test scene
scene = test.children
# Create outer frame with clipping enabled
outer = Frame(pos=(50, 50), size=(400, 300),
fill_color=Color(50, 50, 150),
outline_color=Color(255, 255, 255),
outline=3)
outer.name = "outer"
outer.clip_children = True
scene.append(outer)
# Create inner frame that extends beyond outer bounds
inner = Frame(pos=(200, 150), size=(300, 200),
fill_color=Color(150, 50, 50),
outline_color=Color(255, 255, 0),
outline=2)
inner.name = "inner"
inner.clip_children = True # Also enable clipping on inner frame
outer.children.append(inner)
# Add content to inner frame that extends beyond its bounds
for i in range(5):
caption = Caption(text=f"Line {i+1}: This text should be double-clipped", pos=(10, 30 * i))
caption.font_size = 14
caption.fill_color = Color(255, 255, 255)
inner.children.append(caption)
# Add a child frame to inner that extends way out
deeply_nested = Frame(pos=(250, 100), size=(200, 150),
fill_color=Color(50, 150, 50),
outline_color=Color(255, 0, 255),
outline=2)
deeply_nested.name = "deeply_nested"
inner.children.append(deeply_nested)
# Add status text
status = Caption(text="Nested clipping test:\n"
"- Blue outer frame clips red inner frame\n"
"- Red inner frame clips green deeply nested frame\n"
"- All text should be clipped to frame bounds",
pos=(50, 380))
status.font_size = 12
status.fill_color = Color(200, 200, 200)
scene.append(status)
# Test render texture size handling
print(f"Outer frame size: {outer.w}x{outer.h}")
print(f"Inner frame size: {inner.w}x{inner.h}")
# Step to render
mcrfpy.step(0.1)
# Take initial screenshot
automation.screenshot("frame_clipping_nested.png")
print("Initial screenshot saved: frame_clipping_nested.png")
# Dynamically resize frames to test RenderTexture recreation
print("Resizing frames to test RenderTexture recreation...")
outer.w = 450
outer.h = 350
inner.w = 350
inner.h = 250
print(f"New outer frame size: {outer.w}x{outer.h}")
print(f"New inner frame size: {inner.w}x{inner.h}")
# Step to render resize
mcrfpy.step(0.1)
# Take screenshot after resize
automation.screenshot("frame_clipping_resized.png")
print("\nAdvanced test completed!")
print("Screenshots saved:")
print(" - frame_clipping_nested.png (initial)")
print(" - frame_clipping_resized.png (after resize)")
print("PASS")
sys.exit(0)
print("Advanced test scheduled, running...")

View file

@ -1,125 +1,129 @@
#!/usr/bin/env python3
"""Test Grid.children collection - Issue #132
Refactored to use mcrfpy.step() for synchronous execution.
"""
"""Test Grid.children collection - Issue #132"""
import mcrfpy
from mcrfpy import automation
import sys
print("Creating test scene...")
def take_screenshot(timer, runtime):
"""Take screenshot after render completes"""
timer.stop()
automation.screenshot("test_grid_children_result.png")
print("Screenshot saved to test_grid_children_result.png")
print("PASS - Grid.children test completed")
sys.exit(0)
def run_test(timer, runtime):
"""Main test - runs after scene is set up"""
timer.stop()
# Get the scene UI
ui = test.children
# Create a grid without texture (uses default 16x16 cells)
print("Test 1: Creating Grid with children...")
grid = mcrfpy.Grid(grid_size=(20, 15), pos=(50, 50), size=(320, 240))
grid.fill_color = mcrfpy.Color(30, 30, 60)
ui.append(grid)
# Verify entities and children properties exist
print(f" grid.entities = {grid.entities}")
print(f" grid.children = {grid.children}")
# Test 2: Add UIDrawable children to the grid
print("\nTest 2: Adding UIDrawable children...")
# Speech bubble style caption - positioned in grid-world pixels
# At cell (5, 3) which is 5*16=80, 3*16=48 in pixels
caption = mcrfpy.Caption(text="Hello!", pos=(80, 48))
caption.fill_color = mcrfpy.Color(255, 255, 200)
caption.outline = 1
caption.outline_color = mcrfpy.Color(0, 0, 0)
grid.children.append(caption)
print(f" Added caption at (80, 48)")
# A highlight circle around cell (10, 7) = (160, 112)
# Circle needs center, not pos
circle = mcrfpy.Circle(center=(168, 120), radius=20,
fill_color=mcrfpy.Color(255, 255, 0, 100),
outline_color=mcrfpy.Color(255, 255, 0),
outline=2)
grid.children.append(circle)
print(f" Added highlight circle at (168, 120)")
# A line indicating a path from (2,2) to (8,6)
# In pixels: (32, 32) to (128, 96)
line = mcrfpy.Line(start=(32, 32), end=(128, 96),
color=mcrfpy.Color(0, 255, 0), thickness=3)
grid.children.append(line)
print(f" Added path line from (32,32) to (128,96)")
# An arc for range indicator at (15, 10) = (240, 160)
arc = mcrfpy.Arc(center=(240, 160), radius=40, start_angle=0, end_angle=270,
color=mcrfpy.Color(255, 0, 255), thickness=4)
grid.children.append(arc)
print(f" Added range arc at (240, 160)")
# Test 3: Verify children count
print(f"\nTest 3: Verifying children count...")
print(f" grid.children count = {len(grid.children)}")
assert len(grid.children) == 4, f"Expected 4 children, got {len(grid.children)}"
# Test 4: Children should be accessible by index
print("\nTest 4: Accessing children by index...")
child0 = grid.children[0]
print(f" grid.children[0] = {child0}")
child1 = grid.children[1]
print(f" grid.children[1] = {child1}")
# Test 5: Modify a child's position (should update in grid)
print("\nTest 5: Modifying child position...")
original_pos = (caption.pos.x, caption.pos.y)
caption.pos = mcrfpy.Vector(90, 58)
new_pos = (caption.pos.x, caption.pos.y)
print(f" Moved caption from {original_pos} to {new_pos}")
# Test 6: Test z_index for children
print("\nTest 6: Testing z_index ordering...")
# Add overlapping elements with different z_index
frame1 = mcrfpy.Frame(pos=(150, 80), size=(40, 40))
frame1.fill_color = mcrfpy.Color(255, 0, 0, 200)
frame1.z_index = 10
grid.children.append(frame1)
frame2 = mcrfpy.Frame(pos=(160, 90), size=(40, 40))
frame2.fill_color = mcrfpy.Color(0, 255, 0, 200)
frame2.z_index = 5 # Lower z_index, rendered first (behind)
grid.children.append(frame2)
print(f" Added overlapping frames: red z=10, green z=5")
# Test 7: Test visibility
print("\nTest 7: Testing child visibility...")
frame3 = mcrfpy.Frame(pos=(50, 150), size=(30, 30))
frame3.fill_color = mcrfpy.Color(0, 0, 255)
frame3.visible = False
grid.children.append(frame3)
print(f" Added invisible blue frame (should not appear)")
# Test 8: Pan the grid and verify children move with it
print("\nTest 8: Testing pan (children should follow grid camera)...")
# Center the view on cell (10, 7.5) - default was grid center
grid.center = (160, 120) # Center on pixel (160, 120)
print(f" Centered grid on (160, 120)")
# Test 9: Test zoom
print("\nTest 9: Testing zoom...")
grid.zoom = 1.5
print(f" Set zoom to 1.5")
print(f"\nFinal children count: {len(grid.children)}")
# Schedule screenshot for next frame
mcrfpy.Timer("screenshot", take_screenshot, 100, once=True)
# Create a test scene
test = mcrfpy.Scene("test")
test.activate()
mcrfpy.step(0.01) # Initialize
# Get the scene UI
ui = test.children
# Test 1: Creating Grid with children
print("Test 1: Creating Grid with children...")
grid = mcrfpy.Grid(grid_size=(20, 15), pos=(50, 50), size=(320, 240))
grid.fill_color = mcrfpy.Color(30, 30, 60)
ui.append(grid)
# Verify entities and children properties exist
print(f" grid.entities = {grid.entities}")
print(f" grid.children = {grid.children}")
# Test 2: Add UIDrawable children to the grid
print("\nTest 2: Adding UIDrawable children...")
# Speech bubble style caption - positioned in grid-world pixels
# At cell (5, 3) which is 5*16=80, 3*16=48 in pixels
caption = mcrfpy.Caption(text="Hello!", pos=(80, 48))
caption.fill_color = mcrfpy.Color(255, 255, 200)
caption.outline = 1
caption.outline_color = mcrfpy.Color(0, 0, 0)
grid.children.append(caption)
print(f" Added caption at (80, 48)")
# A highlight circle around cell (10, 7) = (160, 112)
# Circle needs center, not pos
circle = mcrfpy.Circle(center=(168, 120), radius=20,
fill_color=mcrfpy.Color(255, 255, 0, 100),
outline_color=mcrfpy.Color(255, 255, 0),
outline=2)
grid.children.append(circle)
print(f" Added highlight circle at (168, 120)")
# A line indicating a path from (2,2) to (8,6)
# In pixels: (32, 32) to (128, 96)
line = mcrfpy.Line(start=(32, 32), end=(128, 96),
color=mcrfpy.Color(0, 255, 0), thickness=3)
grid.children.append(line)
print(f" Added path line from (32,32) to (128,96)")
# An arc for range indicator at (15, 10) = (240, 160)
arc = mcrfpy.Arc(center=(240, 160), radius=40, start_angle=0, end_angle=270,
color=mcrfpy.Color(255, 0, 255), thickness=4)
grid.children.append(arc)
print(f" Added range arc at (240, 160)")
# Test 3: Verify children count
print(f"\nTest 3: Verifying children count...")
print(f" grid.children count = {len(grid.children)}")
if len(grid.children) != 4:
print(f"FAIL: Expected 4 children, got {len(grid.children)}")
sys.exit(1)
# Test 4: Children should be accessible by index
print("\nTest 4: Accessing children by index...")
child0 = grid.children[0]
print(f" grid.children[0] = {child0}")
child1 = grid.children[1]
print(f" grid.children[1] = {child1}")
# Test 5: Modify a child's position (should update in grid)
print("\nTest 5: Modifying child position...")
original_pos = (caption.pos.x, caption.pos.y)
caption.pos = mcrfpy.Vector(90, 58)
new_pos = (caption.pos.x, caption.pos.y)
print(f" Moved caption from {original_pos} to {new_pos}")
# Test 6: Test z_index for children
print("\nTest 6: Testing z_index ordering...")
# Add overlapping elements with different z_index
frame1 = mcrfpy.Frame(pos=(150, 80), size=(40, 40))
frame1.fill_color = mcrfpy.Color(255, 0, 0, 200)
frame1.z_index = 10
grid.children.append(frame1)
frame2 = mcrfpy.Frame(pos=(160, 90), size=(40, 40))
frame2.fill_color = mcrfpy.Color(0, 255, 0, 200)
frame2.z_index = 5 # Lower z_index, rendered first (behind)
grid.children.append(frame2)
print(f" Added overlapping frames: red z=10, green z=5")
# Test 7: Test visibility
print("\nTest 7: Testing child visibility...")
frame3 = mcrfpy.Frame(pos=(50, 150), size=(30, 30))
frame3.fill_color = mcrfpy.Color(0, 0, 255)
frame3.visible = False
grid.children.append(frame3)
print(f" Added invisible blue frame (should not appear)")
# Test 8: Pan the grid and verify children move with it
print("\nTest 8: Testing pan (children should follow grid camera)...")
# Center the view on cell (10, 7.5) - default was grid center
grid.center = (160, 120) # Center on pixel (160, 120)
print(f" Centered grid on (160, 120)")
# Test 9: Test zoom
print("\nTest 9: Testing zoom...")
grid.zoom = 1.5
print(f" Set zoom to 1.5")
print(f"\nFinal children count: {len(grid.children)}")
# Step to render everything
mcrfpy.step(0.1)
# Take screenshot
automation.screenshot("test_grid_children_result.png")
print("Screenshot saved to test_grid_children_result.png")
print("PASS - Grid.children test completed")
sys.exit(0)
# Schedule test to run after game loop starts
mcrfpy.Timer("test", run_test, 50, once=True)

View file

@ -2,94 +2,90 @@
"""
Test that all UI classes can be instantiated without arguments.
This verifies the fix for requiring arguments even with safe default constructors.
Refactored to use mcrfpy.step() for synchronous execution.
"""
import mcrfpy
import sys
import traceback
# Initialize scene
test = mcrfpy.Scene("test")
test.activate()
mcrfpy.step(0.01)
print("Testing UI class instantiation without arguments...")
all_pass = True
# Test UICaption with no arguments
try:
caption = mcrfpy.Caption()
print("PASS: Caption() - Success")
print(f" Position: ({caption.x}, {caption.y})")
print(f" Text: '{caption.text}'")
assert caption.x == 0.0
assert caption.y == 0.0
assert caption.text == ""
except Exception as e:
print(f"FAIL: Caption() - {e}")
traceback.print_exc()
all_pass = False
# Test UIFrame with no arguments
try:
frame = mcrfpy.Frame()
print("PASS: Frame() - Success")
print(f" Position: ({frame.x}, {frame.y})")
print(f" Size: ({frame.w}, {frame.h})")
assert frame.x == 0.0
assert frame.y == 0.0
assert frame.w == 0.0
assert frame.h == 0.0
except Exception as e:
print(f"FAIL: Frame() - {e}")
traceback.print_exc()
all_pass = False
# Test UIGrid with no arguments
try:
grid = mcrfpy.Grid()
print("PASS: Grid() - Success")
print(f" Grid size: {grid.grid_x} x {grid.grid_y}")
print(f" Position: ({grid.x}, {grid.y})")
assert grid.grid_x == 1
assert grid.grid_y == 1
assert grid.x == 0.0
assert grid.y == 0.0
except Exception as e:
print(f"FAIL: Grid() - {e}")
traceback.print_exc()
all_pass = False
# Test UIEntity with no arguments
try:
entity = mcrfpy.Entity()
print("PASS: Entity() - Success")
print(f" Position: ({entity.x}, {entity.y})")
assert entity.x == 0.0
assert entity.y == 0.0
except Exception as e:
print(f"FAIL: Entity() - {e}")
traceback.print_exc()
all_pass = False
# Test UISprite with no arguments (if it has a default constructor)
try:
sprite = mcrfpy.Sprite()
print("PASS: Sprite() - Success")
print(f" Position: ({sprite.x}, {sprite.y})")
assert sprite.x == 0.0
assert sprite.y == 0.0
except Exception as e:
print(f"FAIL: Sprite() - {e}")
# Sprite might still require arguments, which is okay
print("\nAll tests complete!")
if all_pass:
print("PASS")
def test_ui_constructors(timer, runtime):
"""Test that UI classes can be instantiated without arguments"""
print("Testing UI class instantiation without arguments...")
# Test UICaption with no arguments
try:
caption = mcrfpy.Caption()
print("PASS: Caption() - Success")
print(f" Position: ({caption.x}, {caption.y})")
print(f" Text: '{caption.text}'")
assert caption.x == 0.0
assert caption.y == 0.0
assert caption.text == ""
except Exception as e:
print(f"FAIL: Caption() - {e}")
import traceback
traceback.print_exc()
# Test UIFrame with no arguments
try:
frame = mcrfpy.Frame()
print("PASS: Frame() - Success")
print(f" Position: ({frame.x}, {frame.y})")
print(f" Size: ({frame.w}, {frame.h})")
assert frame.x == 0.0
assert frame.y == 0.0
assert frame.w == 0.0
assert frame.h == 0.0
except Exception as e:
print(f"FAIL: Frame() - {e}")
import traceback
traceback.print_exc()
# Test UIGrid with no arguments
try:
grid = mcrfpy.Grid()
print("PASS: Grid() - Success")
print(f" Grid size: {grid.grid_x} x {grid.grid_y}")
print(f" Position: ({grid.x}, {grid.y})")
assert grid.grid_x == 1
assert grid.grid_y == 1
assert grid.x == 0.0
assert grid.y == 0.0
except Exception as e:
print(f"FAIL: Grid() - {e}")
import traceback
traceback.print_exc()
# Test UIEntity with no arguments
try:
entity = mcrfpy.Entity()
print("PASS: Entity() - Success")
print(f" Position: ({entity.x}, {entity.y})")
assert entity.x == 0.0
assert entity.y == 0.0
except Exception as e:
print(f"FAIL: Entity() - {e}")
import traceback
traceback.print_exc()
# Test UISprite with no arguments (if it has a default constructor)
try:
sprite = mcrfpy.Sprite()
print("PASS: Sprite() - Success")
print(f" Position: ({sprite.x}, {sprite.y})")
assert sprite.x == 0.0
assert sprite.y == 0.0
except Exception as e:
print(f"FAIL: Sprite() - {e}")
# Sprite might still require arguments, which is okay
print("\nAll tests complete!")
# Exit cleanly
sys.exit(0)
else:
print("FAIL")
sys.exit(1)
# Create a basic scene so the game can start
test = mcrfpy.Scene("test")
# Schedule the test to run after game initialization
test_timer = mcrfpy.Timer("test", test_ui_constructors, 100, once=True)

View file

@ -1,67 +1,57 @@
#!/usr/bin/env python3
"""Quick test of drawable properties
Refactored to use mcrfpy.step() for synchronous execution.
"""
"""Quick test of drawable properties"""
import mcrfpy
import sys
# Initialize scene
test = mcrfpy.Scene("test")
test.activate()
mcrfpy.step(0.01)
print("\n=== Testing Properties ===")
all_pass = True
# Test Frame
try:
frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100))
print(f"Frame visible: {frame.visible}")
frame.visible = False
print(f"Frame visible after setting to False: {frame.visible}")
print(f"Frame opacity: {frame.opacity}")
frame.opacity = 0.5
print(f"Frame opacity after setting to 0.5: {frame.opacity}")
bounds = frame.get_bounds()
print(f"Frame bounds: {bounds}")
frame.move(5, 5)
bounds2 = frame.get_bounds()
print(f"Frame bounds after move(5,5): {bounds2}")
print("+ Frame properties work!")
except Exception as e:
print(f"x Frame failed: {e}")
all_pass = False
# Test Entity
try:
entity = mcrfpy.Entity()
print(f"\nEntity visible: {entity.visible}")
entity.visible = False
print(f"Entity visible after setting to False: {entity.visible}")
print(f"Entity opacity: {entity.opacity}")
entity.opacity = 0.7
print(f"Entity opacity after setting to 0.7: {entity.opacity}")
bounds = entity.get_bounds()
print(f"Entity bounds: {bounds}")
entity.move(3, 3)
print(f"Entity position after move(3,3): ({entity.x}, {entity.y})")
print("+ Entity properties work!")
except Exception as e:
print(f"x Entity failed: {e}")
all_pass = False
if all_pass:
print("\nPASS")
def test_properties(timer, runtime):
timer.stop()
print("\n=== Testing Properties ===")
# Test Frame
try:
frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100))
print(f"Frame visible: {frame.visible}")
frame.visible = False
print(f"Frame visible after setting to False: {frame.visible}")
print(f"Frame opacity: {frame.opacity}")
frame.opacity = 0.5
print(f"Frame opacity after setting to 0.5: {frame.opacity}")
bounds = frame.get_bounds()
print(f"Frame bounds: {bounds}")
frame.move(5, 5)
bounds2 = frame.get_bounds()
print(f"Frame bounds after move(5,5): {bounds2}")
print("✓ Frame properties work!")
except Exception as e:
print(f"✗ Frame failed: {e}")
# Test Entity
try:
entity = mcrfpy.Entity()
print(f"\nEntity visible: {entity.visible}")
entity.visible = False
print(f"Entity visible after setting to False: {entity.visible}")
print(f"Entity opacity: {entity.opacity}")
entity.opacity = 0.7
print(f"Entity opacity after setting to 0.7: {entity.opacity}")
bounds = entity.get_bounds()
print(f"Entity bounds: {bounds}")
entity.move(3, 3)
print(f"Entity position after move(3,3): ({entity.x}, {entity.y})")
print("✓ Entity properties work!")
except Exception as e:
print(f"✗ Entity failed: {e}")
sys.exit(0)
else:
print("\nFAIL")
sys.exit(1)
test = mcrfpy.Scene("test")
test_properties_timer = mcrfpy.Timer("test_properties", test_properties, 100, once=True)

View file

@ -4,7 +4,6 @@ Test for Python object cache - verifies that derived Python classes
maintain their identity when stored in and retrieved from collections.
Issue #112: Object Splitting - Preserve Python derived types in collections
Refactored to use mcrfpy.step() for synchronous execution.
"""
import mcrfpy
@ -17,128 +16,136 @@ test_results = []
def test(condition, message):
global test_passed
if condition:
test_results.append(f"+ {message}")
test_results.append(f" {message}")
else:
test_results.append(f"x {message}")
test_results.append(f" {message}")
test_passed = False
def run_tests(timer, runtime):
"""Timer callback to run tests after game loop starts"""
global test_passed
print("\n=== Testing Python Object Cache ===")
# Test 1: Create derived Frame class
class MyFrame(mcrfpy.Frame):
def __init__(self, x=0, y=0):
super().__init__(pos=(x, y), size=(100, 100))
self.custom_data = "I am a custom frame"
self.test_value = 42
# Test 2: Create instance and add to scene
frame = MyFrame(50, 50)
scene_ui = test_scene.children
scene_ui.append(frame)
# Test 3: Retrieve from collection and check type
retrieved = scene_ui[0]
test(type(retrieved) == MyFrame, "Retrieved object maintains derived type")
test(isinstance(retrieved, MyFrame), "isinstance check passes")
test(hasattr(retrieved, 'custom_data'), "Custom attribute exists")
if hasattr(retrieved, 'custom_data'):
test(retrieved.custom_data == "I am a custom frame", "Custom attribute value preserved")
if hasattr(retrieved, 'test_value'):
test(retrieved.test_value == 42, "Numeric attribute value preserved")
# Test 4: Check object identity (same Python object)
test(retrieved is frame, "Retrieved object is the same Python object")
test(id(retrieved) == id(frame), "Object IDs match")
# Test 5: Multiple retrievals return same object
retrieved2 = scene_ui[0]
test(retrieved2 is retrieved, "Multiple retrievals return same object")
# Test 6: Test with other UI types
class MySprite(mcrfpy.Sprite):
def __init__(self):
# Use default texture by passing None
super().__init__(texture=None, sprite_index=0)
self.sprite_data = "custom sprite"
sprite = MySprite()
sprite.x = 200
sprite.y = 200
scene_ui.append(sprite)
retrieved_sprite = scene_ui[1]
test(type(retrieved_sprite) == MySprite, "Sprite maintains derived type")
if hasattr(retrieved_sprite, 'sprite_data'):
test(retrieved_sprite.sprite_data == "custom sprite", "Sprite custom data preserved")
# Test 7: Test with Caption
class MyCaption(mcrfpy.Caption):
def __init__(self, text):
# Use default font by passing None
super().__init__(text=text, font=None)
self.caption_id = "test_caption"
caption = MyCaption("Test Caption")
caption.x = 10
caption.y = 10
scene_ui.append(caption)
retrieved_caption = scene_ui[2]
test(type(retrieved_caption) == MyCaption, "Caption maintains derived type")
if hasattr(retrieved_caption, 'caption_id'):
test(retrieved_caption.caption_id == "test_caption", "Caption custom data preserved")
# Test 8: Test removal and re-addition
# Use del to remove by index (Python standard), or .remove(element) to remove by value
print(f"before remove: {len(scene_ui)=}")
del scene_ui[-1] # Remove last element by index
print(f"after remove: {len(scene_ui)=}")
scene_ui.append(frame)
retrieved3 = scene_ui[-1] # Get last element
test(retrieved3 is frame, "Object identity preserved after removal/re-addition")
# Test 9: Test with Grid
class MyGrid(mcrfpy.Grid):
def __init__(self, w, h):
super().__init__(grid_size=(w, h))
self.grid_name = "custom_grid"
grid = MyGrid(10, 10)
grid.x = 300
grid.y = 100
scene_ui.append(grid)
retrieved_grid = scene_ui[-1]
test(type(retrieved_grid) == MyGrid, "Grid maintains derived type")
if hasattr(retrieved_grid, 'grid_name'):
test(retrieved_grid.grid_name == "custom_grid", "Grid custom data preserved")
# Test 10: Test with nested collections (Frame with children)
parent = MyFrame(400, 400)
child = MyFrame(10, 10)
child.custom_data = "I am a child"
parent.children.append(child)
scene_ui.append(parent)
retrieved_parent = scene_ui[-1]
test(type(retrieved_parent) == MyFrame, "Parent frame maintains type")
if len(retrieved_parent.children) > 0:
retrieved_child = retrieved_parent.children[0]
test(type(retrieved_child) == MyFrame, "Child frame maintains type in nested collection")
if hasattr(retrieved_child, 'custom_data'):
test(retrieved_child.custom_data == "I am a child", "Child custom data preserved")
# Print results
print("\n=== Test Results ===")
for result in test_results:
print(result)
print(f"\n{'PASS' if test_passed else 'FAIL'}: {sum(1 for r in test_results if r.startswith(''))}/{len(test_results)} tests passed")
sys.exit(0 if test_passed else 1)
# Create test scene
test_scene = mcrfpy.Scene("test_scene")
test_scene.activate()
mcrfpy.step(0.01)
print("\n=== Testing Python Object Cache ===")
# Schedule tests to run after game loop starts
test_timer = mcrfpy.Timer("test", run_tests, 100, once=True)
# Test 1: Create derived Frame class
class MyFrame(mcrfpy.Frame):
def __init__(self, x=0, y=0):
super().__init__(pos=(x, y), size=(100, 100))
self.custom_data = "I am a custom frame"
self.test_value = 42
# Test 2: Create instance and add to scene
frame = MyFrame(50, 50)
scene_ui = test_scene.children
scene_ui.append(frame)
# Test 3: Retrieve from collection and check type
retrieved = scene_ui[0]
test(type(retrieved) == MyFrame, "Retrieved object maintains derived type")
test(isinstance(retrieved, MyFrame), "isinstance check passes")
test(hasattr(retrieved, 'custom_data'), "Custom attribute exists")
if hasattr(retrieved, 'custom_data'):
test(retrieved.custom_data == "I am a custom frame", "Custom attribute value preserved")
if hasattr(retrieved, 'test_value'):
test(retrieved.test_value == 42, "Numeric attribute value preserved")
# Test 4: Check object identity (same Python object)
test(retrieved is frame, "Retrieved object is the same Python object")
test(id(retrieved) == id(frame), "Object IDs match")
# Test 5: Multiple retrievals return same object
retrieved2 = scene_ui[0]
test(retrieved2 is retrieved, "Multiple retrievals return same object")
# Test 6: Test with other UI types
class MySprite(mcrfpy.Sprite):
def __init__(self):
# Use default texture by passing None
super().__init__(texture=None, sprite_index=0)
self.sprite_data = "custom sprite"
sprite = MySprite()
sprite.x = 200
sprite.y = 200
scene_ui.append(sprite)
retrieved_sprite = scene_ui[1]
test(type(retrieved_sprite) == MySprite, "Sprite maintains derived type")
if hasattr(retrieved_sprite, 'sprite_data'):
test(retrieved_sprite.sprite_data == "custom sprite", "Sprite custom data preserved")
# Test 7: Test with Caption
class MyCaption(mcrfpy.Caption):
def __init__(self, text):
# Use default font by passing None
super().__init__(text=text, font=None)
self.caption_id = "test_caption"
caption = MyCaption("Test Caption")
caption.x = 10
caption.y = 10
scene_ui.append(caption)
retrieved_caption = scene_ui[2]
test(type(retrieved_caption) == MyCaption, "Caption maintains derived type")
if hasattr(retrieved_caption, 'caption_id'):
test(retrieved_caption.caption_id == "test_caption", "Caption custom data preserved")
# Test 8: Test removal and re-addition
# Use del to remove by index (Python standard), or .remove(element) to remove by value
print(f"before remove: {len(scene_ui)=}")
del scene_ui[-1] # Remove last element by index
print(f"after remove: {len(scene_ui)=}")
scene_ui.append(frame)
retrieved3 = scene_ui[-1] # Get last element
test(retrieved3 is frame, "Object identity preserved after removal/re-addition")
# Test 9: Test with Grid
class MyGrid(mcrfpy.Grid):
def __init__(self, w, h):
super().__init__(grid_size=(w, h))
self.grid_name = "custom_grid"
grid = MyGrid(10, 10)
grid.x = 300
grid.y = 100
scene_ui.append(grid)
retrieved_grid = scene_ui[-1]
test(type(retrieved_grid) == MyGrid, "Grid maintains derived type")
if hasattr(retrieved_grid, 'grid_name'):
test(retrieved_grid.grid_name == "custom_grid", "Grid custom data preserved")
# Test 10: Test with nested collections (Frame with children)
parent = MyFrame(400, 400)
child = MyFrame(10, 10)
child.custom_data = "I am a child"
parent.children.append(child)
scene_ui.append(parent)
retrieved_parent = scene_ui[-1]
test(type(retrieved_parent) == MyFrame, "Parent frame maintains type")
if len(retrieved_parent.children) > 0:
retrieved_child = retrieved_parent.children[0]
test(type(retrieved_child) == MyFrame, "Child frame maintains type in nested collection")
if hasattr(retrieved_child, 'custom_data'):
test(retrieved_child.custom_data == "I am a child", "Child custom data preserved")
# Print results
print("\n=== Test Results ===")
for result in test_results:
print(result)
print(f"\n{'PASS' if test_passed else 'FAIL'}: {sum(1 for r in test_results if r.startswith('+'))}/{len(test_results)} tests passed")
sys.exit(0 if test_passed else 1)
print("Python object cache test initialized. Running tests...")

View file

@ -1,32 +1,14 @@
#!/usr/bin/env python3
"""Very simple callback test - refactored to use mcrfpy.step()"""
"""Very simple callback test"""
import mcrfpy
import sys
callback_fired = False
def cb(a, t):
global callback_fired
callback_fired = True
print("CB")
# Setup scene
test = mcrfpy.Scene("test")
test.activate()
mcrfpy.step(0.01) # Initialize
# Create entity and animation
e = mcrfpy.Entity((0, 0), texture=None, sprite_index=0)
a = mcrfpy.Animation("x", 1.0, 0.1, "linear", callback=cb)
a.start(e)
# Advance past animation duration (0.1s)
mcrfpy.step(0.15)
# Verify callback fired
if callback_fired:
print("PASS: Callback fired")
sys.exit(0)
else:
print("FAIL: Callback did not fire")
sys.exit(1)
mcrfpy.Timer("exit", lambda t, r: sys.exit(0), 200, once=True)

View file

@ -1,32 +1,30 @@
#!/usr/bin/env python3
"""Simple test to isolate drawable issue
Refactored to use mcrfpy.step() for synchronous execution.
"""
"""Simple test to isolate drawable issue"""
import mcrfpy
import sys
# Initialize scene
test = mcrfpy.Scene("test")
test.activate()
mcrfpy.step(0.01)
try:
# Test basic functionality
frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100))
print(f"Frame created: visible={frame.visible}, opacity={frame.opacity}")
bounds = frame.get_bounds()
print(f"Bounds: {bounds}")
frame.move(5, 5)
print("Move completed")
frame.resize(150, 150)
print("Resize completed")
print("PASS - No crash!")
def simple_test(timer, runtime):
timer.stop()
try:
# Test basic functionality
frame = mcrfpy.Frame(pos=(10, 10), size=(100, 100))
print(f"Frame created: visible={frame.visible}, opacity={frame.opacity}")
bounds = frame.get_bounds()
print(f"Bounds: {bounds}")
frame.move(5, 5)
print("Move completed")
frame.resize(150, 150)
print("Resize completed")
print("PASS - No crash!")
except Exception as e:
print(f"ERROR: {e}")
sys.exit(0)
except Exception as e:
print(f"ERROR: {e}")
print("FAIL")
sys.exit(1)
test = mcrfpy.Scene("test")
simple_test_timer = mcrfpy.Timer("simple_test", simple_test, 100, once=True)

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python3
"""
Test timer callback arguments with new Timer API (#173)
Uses mcrfpy.step() for synchronous test execution.
"""
import mcrfpy
import sys
@ -15,6 +14,9 @@ def new_style_callback(timer, runtime):
print(f"Callback called with: timer={timer} (type: {type(timer)}), runtime={runtime} (type: {type(runtime)})")
if hasattr(timer, 'once'):
print(f"Got Timer object! once={timer.once}")
if call_count >= 2:
print("PASS")
sys.exit(0)
# Set up the scene
test_scene = mcrfpy.Scene("test_scene")
@ -23,14 +25,3 @@ test_scene.activate()
print("Testing new Timer callback signature (timer, runtime)...")
timer = mcrfpy.Timer("test_timer", new_style_callback, 100)
print(f"Timer created: {timer}")
# Advance time to let timer fire - each step() processes timers once
mcrfpy.step(0.15) # First fire
mcrfpy.step(0.15) # Second fire
if call_count >= 2:
print("PASS: Timer callback received correct arguments")
sys.exit(0)
else:
print(f"FAIL: Expected at least 2 callbacks, got {call_count}")
sys.exit(1)