Compare commits

..

7 commits

Author SHA1 Message Date
Frick
a1b692bb1f Add cookbook and tutorial showcase demos
tests/demo/:
- cookbook_showcase.py: Interactive demo of cookbook recipes
- tutorial_showcase.py: Visual walkthrough of tutorial content
- tutorial_screenshots.py: Automated screenshot generation
- new_features_showcase.py: Demo of modern API features
- procgen_showcase.py: Procedural generation examples
- simple_showcase.py: Minimal working examples

Created during docs modernization to verify cookbook examples work.

🤖 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-15 04:06:24 +00:00
Frick
23afae69ad Add API verification test suite and documentation
tests/docs/:
- API_FINDINGS.md: Comprehensive migration guide from deprecated to modern API
- test_*.py: 9 executable tests verifying actual runtime behavior
- screenshots/: Visual verification of working examples

tests/conftest.py:
- Add 'docs' and 'demo' to pytest collection paths

Key findings documented:
- Entity uses grid_pos= not pos=
- Scene API: Scene() + activate() replaces createScene/setScene
- scene.children replaces sceneUI()
- scene.on_key replaces keypressScene()
- mcrfpy.current_scene (property) replaces currentScene() (function)
- Timer callback signature: (timer, runtime)
- Opacity animation does NOT work on Frame (documented bug)

🤖 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-15 04:05:32 +00:00
Frick
be450286f8 Refactor 11 more tests to mcrfpy.step() pattern
Converted from Timer-based async to step()-based sync:
- 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

Only 4 tests remain with Timer-based patterns (2 are headless detection
tests that may require special handling).

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Frack <frack@goblincorps.dev>
Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-14 03:09:47 +00:00
Frick
bb86cece2b Add headless-automation.md explanation document
Comprehensive guide to headless mode and mcrfpy.step() testing:
- Time control with step() (seconds, not milliseconds)
- Timer behavior and callback signatures
- Screenshot automation
- Test pattern comparison table
- LLM agent integration patterns
- Best practices for deterministic testing

Based on Frick's draft, updated with patterns from test refactoring.

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-14 03:04:48 +00:00
Frick
4528ece0a7 Refactor timing tests to use mcrfpy.step() for synchronous execution
Converts tests from Timer-based async patterns to step()-based sync
patterns, eliminating timeout issues in headless testing.

Refactored tests:
- 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

Also updates KNOWN_ISSUES.md with comprehensive documentation on
the step()-based testing pattern including examples and best practices.

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-14 02:56:21 +00:00
Frick
f063d0af0c Fix alignment_test.py margin default expectations
- margin returns 0 when unset (effective default)
- horiz_margin/vert_margin return -1 (sentinel for unset)

🤖 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-14 02:02:08 +00:00
Frick
4579be2791 Test suite modernization: pytest wrapper and runner fixes
- Add LD_LIBRARY_PATH auto-configuration in run_tests.py
- Add --timeout and --quiet command-line flags
- Create pytest wrapper (conftest.py, test_mcrogueface.py) for IDE integration
- Configure pytest.ini to avoid importing mcrfpy modules
- Document known issues: 120/179 passing, 40 timeouts, 19 failures

🤖 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-14 01:54:31 +00:00
46 changed files with 4183 additions and 1151 deletions

View file

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

134
tests/KNOWN_ISSUES.md Normal file
View file

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

190
tests/conftest.py Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

129
tests/docs/API_FINDINGS.md Normal file
View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

14
tests/pytest.ini Normal file
View file

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

View file

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

116
tests/test_mcrogueface.py Normal file
View file

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

View file

@ -1,12 +1,51 @@
#!/usr/bin/env python3
"""Example of CORRECT test pattern using timer callbacks for automation"""
"""Example of CORRECT test pattern using mcrfpy.step() for automation
Refactored from timer-based approach to synchronous step() pattern.
"""
import mcrfpy
from mcrfpy import automation
from datetime import datetime
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) ===")
# This code runs during --exec script execution
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
frame = mcrfpy.Frame(pos=(100, 100), size=(400, 300),
fill_color=mcrfpy.Color(255, 0, 0), # Bright red
outline_color=mcrfpy.Color(255, 255, 255), # White outline
outline=5.0)
ui.append(frame)
# Add text
caption = mcrfpy.Caption(pos=(150, 150),
text="STEP TEST - SHOULD BE VISIBLE",
fill_color=mcrfpy.Color(255, 255, 255))
caption.font_size = 24
frame.children.append(caption)
# Add click handler to demonstrate interaction
click_received = False
def frame_clicked(x, y, button):
global click_received
click_received = True
print(f"Frame clicked at ({x}, {y}) with button {button}")
frame.on_click = frame_clicked
print("Scene setup complete.")
# Step to render the scene
mcrfpy.step(0.1)
print("\n=== Automation Test Running ===")
# NOW we can take screenshots that will show content!
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
@ -19,8 +58,14 @@ def run_automation_tests(timer, runtime):
# 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 timer callback!")
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"
@ -29,54 +74,9 @@ def run_automation_tests(timer, runtime):
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")
print("1. mcrfpy.step() advances simulation synchronously")
print("2. The scene renders during step() calls")
print("3. The RenderTexture 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 ===")
# Create scene with visible content
timer_test_scene = mcrfpy.Scene("timer_test_scene")
timer_test_scene.activate()
ui = timer_test_scene.children
# Add a bright red frame that should be visible
frame = mcrfpy.Frame(pos=(100, 100), size=(400, 300),
fill_color=mcrfpy.Color(255, 0, 0), # Bright red
outline_color=mcrfpy.Color(255, 255, 255), # White outline
outline=5.0)
ui.append(frame)
# Add text
caption = mcrfpy.Caption(pos=(150, 150),
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
def frame_clicked(x, y, button):
print(f"Frame clicked at ({x}, {y}) with button {button}")
frame.on_click = frame_clicked
print("Scene setup complete. Setting timer for automation tests...")
# 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("Timer set. Game loop will start after this script completes.")
print("Automation tests will run 1 second later when content is visible.")
# Script ends here - game loop starts next
print("PASS")
sys.exit(0)

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -1,13 +1,16 @@
#!/usr/bin/env python3
"""
Test #94: Color helper methods - from_hex, to_hex, lerp
Refactored to use mcrfpy.step() for synchronous execution.
"""
import mcrfpy
import sys
def test_color_helpers(timer, runtime):
"""Test Color helper methods"""
# Initialize scene
test = mcrfpy.Scene("test")
test.activate()
mcrfpy.step(0.01)
all_pass = True
@ -176,7 +179,3 @@ def test_color_helpers(timer, runtime):
print(f"\n{'PASS' if all_pass else 'FAIL'}")
sys.exit(0 if all_pass else 1)
# Run test
test = mcrfpy.Scene("test")
test_timer = mcrfpy.Timer("test", test_color_helpers, 100, once=True)

View file

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

View file

@ -1,39 +1,16 @@
#!/usr/bin/env python3
"""Test UIFrame clipping functionality"""
"""Test UIFrame clipping functionality
Refactored to use mcrfpy.step() for synchronous execution.
"""
import mcrfpy
from mcrfpy import Color, Frame, Caption
from mcrfpy import Color, Frame, Caption, automation
import sys
# Module-level state to avoid closures
_test_state = {}
def take_second_screenshot(timer, runtime):
"""Take final screenshot and exit"""
timer.stop()
from mcrfpy import automation
automation.screenshot("frame_clipping_animated.png")
print("\nTest completed successfully!")
print("Screenshots saved:")
print(" - frame_clipping_test.png (initial state)")
print(" - frame_clipping_animated.png (with animation)")
sys.exit(0)
def animate_frames(timer, runtime):
"""Animate frames to demonstrate clipping"""
timer.stop()
scene = test.children
# Move child frames
parent1 = scene[0]
parent2 = scene[1]
parent1.children[1].x = 50
parent2.children[1].x = 50
global screenshot2_timer
screenshot2_timer = mcrfpy.Timer("screenshot2", take_second_screenshot, 500, once=True)
def test_clipping(timer, runtime):
"""Test that clip_children property works correctly"""
timer.stop()
print("Creating test scene...")
test = mcrfpy.Scene("test")
test.activate()
mcrfpy.step(0.01) # Initialize
print("Testing UIFrame clipping functionality...")
@ -90,15 +67,16 @@ def test_clipping(timer, runtime):
# 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",
"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
from mcrfpy import automation
automation.screenshot("frame_clipping_test.png")
print(f"Parent1 clip_children: {parent1.clip_children}")
@ -109,27 +87,32 @@ def test_clipping(timer, runtime):
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}")
# Start animation after a short delay
global animate_timer
animate_timer = mcrfpy.Timer("animate", animate_frames, 100, once=True)
# Animate frames (move children)
parent1.children[1].x = 50
parent2.children[1].x = 50
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}")
# Step to render animation
mcrfpy.step(0.1)
# Main execution
print("Creating test scene...")
test = mcrfpy.Scene("test")
test.activate()
test.on_key = handle_keypress
test_clipping_timer = mcrfpy.Timer("test_clipping", test_clipping, 100, once=True)
print("Test scheduled, running...")
# 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)

View file

@ -1,13 +1,16 @@
#!/usr/bin/env python3
"""Advanced test for UIFrame clipping with nested frames"""
"""Advanced test for UIFrame clipping with nested frames
Refactored to use mcrfpy.step() for synchronous execution.
"""
import mcrfpy
from mcrfpy import Color, Frame, Caption, Vector
from mcrfpy import Color, Frame, Caption, Vector, automation
import sys
def test_nested_clipping(timer, runtime):
"""Test nested frames with clipping"""
timer.stop()
print("Creating advanced test scene...")
test = mcrfpy.Scene("test")
test.activate()
mcrfpy.step(0.01)
print("Testing advanced UIFrame clipping with nested frames...")
@ -61,9 +64,14 @@ def test_nested_clipping(timer, runtime):
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
def resize_test(timer, runtime):
timer.stop()
print("Resizing frames to test RenderTexture recreation...")
outer.w = 450
outer.h = 350
@ -72,34 +80,16 @@ def test_nested_clipping(timer, runtime):
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)
# Step to render resize
mcrfpy.step(0.1)
def take_resize_screenshot(timer, runtime):
timer.stop()
from mcrfpy import automation
# 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)
# 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()
# Schedule the test
test_nested_clipping_timer = mcrfpy.Timer("test_nested_clipping", test_nested_clipping, 100, once=True)
print("Advanced test scheduled, running...")

View file

@ -1,26 +1,20 @@
#!/usr/bin/env python3
"""Test Grid.children collection - Issue #132"""
"""Test Grid.children collection - Issue #132
Refactored to use mcrfpy.step() for synchronous execution.
"""
import mcrfpy
from mcrfpy import automation
import sys
def take_screenshot(timer, runtime):
"""Take screenshot after render completes"""
timer.stop()
automation.screenshot("test_grid_children_result.png")
print("Screenshot saved to test_grid_children_result.png")
print("PASS - Grid.children test completed")
sys.exit(0)
def run_test(timer, runtime):
"""Main test - runs after scene is set up"""
timer.stop()
print("Creating test scene...")
test = mcrfpy.Scene("test")
test.activate()
mcrfpy.step(0.01) # Initialize
# Get the scene UI
ui = test.children
# Create a grid without texture (uses default 16x16 cells)
# 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)
@ -67,7 +61,9 @@ def run_test(timer, runtime):
# 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)}"
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...")
@ -118,12 +114,12 @@ def run_test(timer, runtime):
print(f"\nFinal children count: {len(grid.children)}")
# Schedule screenshot for next frame
mcrfpy.Timer("screenshot", take_screenshot, 100, once=True)
# Step to render everything
mcrfpy.step(0.1)
# Create a test scene
test = mcrfpy.Scene("test")
test.activate()
# Take screenshot
automation.screenshot("test_grid_children_result.png")
print("Screenshot saved to test_grid_children_result.png")
# Schedule test to run after game loop starts
mcrfpy.Timer("test", run_test, 50, once=True)
print("PASS - Grid.children test completed")
sys.exit(0)

View file

@ -2,16 +2,22 @@
"""
Test that all UI classes can be instantiated without arguments.
This verifies the fix for requiring arguments even with safe default constructors.
Refactored to use mcrfpy.step() for synchronous execution.
"""
import mcrfpy
import sys
import traceback
def test_ui_constructors(timer, runtime):
"""Test that UI classes can be instantiated without arguments"""
# 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()
@ -23,8 +29,8 @@ def test_ui_constructors(timer, runtime):
assert caption.text == ""
except Exception as e:
print(f"FAIL: Caption() - {e}")
import traceback
traceback.print_exc()
all_pass = False
# Test UIFrame with no arguments
try:
@ -38,8 +44,8 @@ def test_ui_constructors(timer, runtime):
assert frame.h == 0.0
except Exception as e:
print(f"FAIL: Frame() - {e}")
import traceback
traceback.print_exc()
all_pass = False
# Test UIGrid with no arguments
try:
@ -53,8 +59,8 @@ def test_ui_constructors(timer, runtime):
assert grid.y == 0.0
except Exception as e:
print(f"FAIL: Grid() - {e}")
import traceback
traceback.print_exc()
all_pass = False
# Test UIEntity with no arguments
try:
@ -65,8 +71,8 @@ def test_ui_constructors(timer, runtime):
assert entity.y == 0.0
except Exception as e:
print(f"FAIL: Entity() - {e}")
import traceback
traceback.print_exc()
all_pass = False
# Test UISprite with no arguments (if it has a default constructor)
try:
@ -81,11 +87,9 @@ def test_ui_constructors(timer, runtime):
print("\nAll tests complete!")
# Exit cleanly
if all_pass:
print("PASS")
sys.exit(0)
# 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)
else:
print("FAIL")
sys.exit(1)

View file

@ -1,13 +1,19 @@
#!/usr/bin/env python3
"""Quick test of drawable properties"""
"""Quick test of drawable properties
Refactored to use mcrfpy.step() for synchronous execution.
"""
import mcrfpy
import sys
def test_properties(timer, runtime):
timer.stop()
# 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))
@ -26,9 +32,10 @@ def test_properties(timer, runtime):
bounds2 = frame.get_bounds()
print(f"Frame bounds after move(5,5): {bounds2}")
print(" Frame properties work!")
print("+ Frame properties work!")
except Exception as e:
print(f"✗ Frame failed: {e}")
print(f"x Frame failed: {e}")
all_pass = False
# Test Entity
try:
@ -47,11 +54,14 @@ def test_properties(timer, runtime):
entity.move(3, 3)
print(f"Entity position after move(3,3): ({entity.x}, {entity.y})")
print(" Entity properties work!")
print("+ Entity properties work!")
except Exception as e:
print(f"✗ Entity failed: {e}")
print(f"x Entity failed: {e}")
all_pass = False
if all_pass:
print("\nPASS")
sys.exit(0)
test = mcrfpy.Scene("test")
test_properties_timer = mcrfpy.Timer("test_properties", test_properties, 100, once=True)
else:
print("\nFAIL")
sys.exit(1)

View file

@ -4,6 +4,7 @@ Test for Python object cache - verifies that derived Python classes
maintain their identity when stored in and retrieved from collections.
Issue #112: Object Splitting - Preserve Python derived types in collections
Refactored to use mcrfpy.step() for synchronous execution.
"""
import mcrfpy
@ -16,14 +17,15 @@ test_results = []
def test(condition, message):
global test_passed
if condition:
test_results.append(f" {message}")
test_results.append(f"+ {message}")
else:
test_results.append(f" {message}")
test_results.append(f"x {message}")
test_passed = False
def run_tests(timer, runtime):
"""Timer callback to run tests after game loop starts"""
global test_passed
# Create test scene
test_scene = mcrfpy.Scene("test_scene")
test_scene.activate()
mcrfpy.step(0.01)
print("\n=== Testing Python Object Cache ===")
@ -137,15 +139,6 @@ def run_tests(timer, runtime):
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")
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()
# Schedule tests to run after game loop starts
test_timer = mcrfpy.Timer("test", run_tests, 100, once=True)
print("Python object cache test initialized. Running tests...")

View file

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

View file

@ -1,10 +1,14 @@
#!/usr/bin/env python3
"""Simple test to isolate drawable issue"""
"""Simple test to isolate drawable issue
Refactored to use mcrfpy.step() for synchronous execution.
"""
import mcrfpy
import sys
def simple_test(timer, runtime):
timer.stop()
# Initialize scene
test = mcrfpy.Scene("test")
test.activate()
mcrfpy.step(0.01)
try:
# Test basic functionality
@ -21,10 +25,8 @@ def simple_test(timer, runtime):
print("Resize completed")
print("PASS - No crash!")
sys.exit(0)
except Exception as e:
print(f"ERROR: {e}")
sys.exit(0)
test = mcrfpy.Scene("test")
simple_test_timer = mcrfpy.Timer("simple_test", simple_test, 100, once=True)
print("FAIL")
sys.exit(1)

View file

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