Table of Contents
Headless Mode
McRogueFace supports headless operation for automated testing, CI pipelines, and LLM agent orchestration. In headless mode, no window is created and simulation time is controlled programmatically via Python.
Related Pages:
- Writing-Tests - Test patterns using headless mode
- Input-and-Events - Timer and event system
- Animation-System - Animations work with step()
Key Files:
src/GameEngine.cpp::step()- Simulation advancementsrc/McRFPy_Automation.cpp::_screenshot()- Synchronous capturetests/unit/test_step_function.py- step() teststests/unit/test_synchronous_screenshot.py- Screenshot tests
Running in Headless Mode
Launch McRogueFace with the --headless flag:
# Run a script in headless mode
./mcrogueface --headless --exec my_script.py
# Run inline Python
./mcrogueface --headless -c "import mcrfpy; print('Hello headless')"
In headless mode:
- No window is created (uses RenderTexture internally)
- Simulation time is frozen until
step()is called - Screenshots capture current state synchronously
Simulation Control with step()
The mcrfpy.step() function advances simulation time in headless mode:
import mcrfpy
# Advance by specific duration (seconds)
dt = mcrfpy.step(0.1) # Advance 100ms
print(f"Advanced by {dt} seconds")
# Advance by integer (converts to float)
dt = mcrfpy.step(1) # Advance 1 second
# Advance to next scheduled event (timer or animation)
dt = mcrfpy.step(None) # or mcrfpy.step()
print(f"Advanced {dt} seconds to next event")
Return Value
step() returns the actual time advanced (float, in seconds):
- In headless mode: Returns the requested dt (or time to next event)
- In windowed mode: Returns 0.0 (no-op, simulation runs via game loop)
What step() Does
- Advances internal
simulation_timeby the specified duration - Updates all active animations
- Fires any timers whose intervals have elapsed
- Does NOT render (call
screenshot()to trigger render)
Timers in Headless Mode
Timers work with simulation time in headless mode:
import mcrfpy
import sys
fired = [False]
def on_timer(runtime):
"""Timer callback receives simulation time in milliseconds"""
fired[0] = True
print(f"Timer fired at {runtime}ms")
# Set timer for 500ms
mcrfpy.setTimer("my_timer", on_timer, 500)
# Advance past the timer interval
mcrfpy.step(0.6) # 600ms - timer will fire
if fired[0]:
print("Success!")
mcrfpy.delTimer("my_timer")
Timer Behavior
- Timers use
simulation_timein headless mode (not wall-clock time) - Timer callbacks receive the current simulation time in milliseconds
- Multiple timers can fire in a single
step()call if intervals overlap
Synchronous Screenshots
In headless mode, automation.screenshot() is synchronous - it renders the current scene state before capturing:
import mcrfpy
from mcrfpy import automation
mcrfpy.createScene("test")
ui = mcrfpy.sceneUI("test")
# Add a red frame
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
frame.fill_color = mcrfpy.Color(255, 0, 0)
ui.append(frame)
mcrfpy.setScene("test")
# Screenshot captures current state immediately (no timer needed)
automation.screenshot("/tmp/red_frame.png")
# Change to green
frame.fill_color = mcrfpy.Color(0, 255, 0)
# Next screenshot shows green (not red!)
automation.screenshot("/tmp/green_frame.png")
Windowed vs Headless Screenshot Behavior
| Mode | Behavior |
|---|---|
| Windowed | Captures previous frame's buffer (async) |
| Headless | Renders current state, then captures (sync) |
In windowed mode, you need timer callbacks to ensure the frame has rendered:
# Windowed mode pattern (NOT needed in headless)
def capture(dt):
automation.screenshot("output.png")
sys.exit(0)
mcrfpy.setTimer("capture", capture, 100)
Animations with step()
Animations update when step() is called:
import mcrfpy
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
ui.append(frame)
# Start animation: move x from 0 to 500 over 2 seconds
anim = mcrfpy.Animation("x", 500.0, 2.0, "easeInOut")
anim.start(frame)
# Advance halfway through animation
mcrfpy.step(1.0)
print(f"Frame x: {frame.x}") # ~250 (halfway)
# Complete the animation
mcrfpy.step(1.0)
print(f"Frame x: {frame.x}") # 500 (complete)
Use Cases
Automated Testing
#!/usr/bin/env python3
"""Headless test example"""
import mcrfpy
from mcrfpy import automation
import sys
# Setup
mcrfpy.createScene("test")
mcrfpy.setScene("test")
# Test logic
ui = mcrfpy.sceneUI("test")
frame = mcrfpy.Frame(pos=(50, 50), size=(100, 100))
ui.append(frame)
# Verify
assert frame.x == 50
assert frame.w == 100
# Take verification screenshot
automation.screenshot("/tmp/test_output.png")
print("PASS")
sys.exit(0)
LLM Agent Orchestration
The step() function enables external agents to control simulation pacing:
import mcrfpy
from mcrfpy import automation
def agent_loop():
"""External agent controls simulation"""
while not game_over():
# Agent observes current state
automation.screenshot("/tmp/current_state.png")
# Agent decides action (external LLM call)
action = get_agent_action("/tmp/current_state.png")
# Execute action
execute_action(action)
# Advance simulation to see results
mcrfpy.step(0.1)
Batch Rendering
Generate multiple frames without real-time constraints:
import mcrfpy
from mcrfpy import automation
for i in range(100):
# Update scene state
update_scene(i)
# Advance animations
mcrfpy.step(1/60) # 60 FPS equivalent
# Capture frame
automation.screenshot(f"/tmp/frame_{i:04d}.png")
API Reference
Module Functions:
mcrfpy.step(dt=None)- Advance simulation timedt(float or None): Seconds to advance, or None for next event- Returns: Actual time advanced (float)
- In windowed mode: Returns 0.0 (no-op)
Automation Functions:
automation.screenshot(filename)- Capture current frame- In headless: Synchronous (renders then captures)
- In windowed: Asynchronous (captures previous frame buffer)
Last updated: 2025-12-01 - Added for #153