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>
5.6 KiB
5.6 KiB
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:
mcrogueface --headless --exec game.py
Or use xvfb for a virtual framebuffer (required for rendering):
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:
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:
# 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:
# 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:
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:
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
#!/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
# 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
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
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:
- Observe: Capture screenshots, read game state
- Decide: Process with vision models or state analysis
- Act: Send input commands, modify game state
- Verify: Check results, capture new state
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
- Use
mcrfpy.step()instead of real-time waiting for all headless tests - Initialize scenes with a brief
step(0.1)afteractivate() - Be deterministic - same inputs should produce same outputs
- Test incrementally - advance time in small steps to catch intermediate states
- Use
sys.exit(0/1)for clear pass/fail signals to test runners - Multiple
step()calls to fire repeating timers multiple times
Running the Test Suite
# 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 - How animations work
- Scene API - Managing scenes
- Timer API - Timer details