Update Headless Mode

John McCardle 2026-02-07 23:49:00 +00:00
commit 48a9ed0f7f

@ -1,269 +1,269 @@
# Headless Mode # 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. 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:** **Related Pages:**
- [[Writing-Tests]] - Test patterns using headless mode - [[Writing-Tests]] - Test patterns using headless mode
- [[Input-and-Events]] - Timer and event system - [[Input-and-Events]] - Timer and event system
- [[Animation-System]] - Animations work with step() - [[Animation-System]] - Animations work with step()
**Key Files:** **Key Files:**
- `src/GameEngine.cpp::step()` - Simulation advancement - `src/GameEngine.cpp::step()` - Simulation advancement
- `src/McRFPy_Automation.cpp::_screenshot()` - Synchronous capture - `src/McRFPy_Automation.cpp::_screenshot()` - Synchronous capture
--- ---
## Running in Headless Mode ## Running in Headless Mode
Launch McRogueFace with the `--headless` flag: Launch McRogueFace with the `--headless` flag:
```bash ```bash
# Run a script in headless mode # Run a script in headless mode
./mcrogueface --headless --exec my_script.py ./mcrogueface --headless --exec my_script.py
# Run inline Python # Run inline Python
./mcrogueface --headless -c "import mcrfpy; print('Hello headless')" ./mcrogueface --headless -c "import mcrfpy; print('Hello headless')"
``` ```
In headless mode: In headless mode:
- No window is created (uses RenderTexture internally) - No window is created (uses RenderTexture internally)
- Simulation time is frozen until `step()` is called - Simulation time is frozen until `step()` is called
- Screenshots capture current state synchronously - Screenshots capture current state synchronously
--- ---
## Simulation Control with step() ## Simulation Control with step()
The `mcrfpy.step()` function advances simulation time in headless mode: The `mcrfpy.step()` function advances simulation time in headless mode:
```python ```python
import mcrfpy import mcrfpy
# Advance by specific duration (seconds) # Advance by specific duration (seconds)
dt = mcrfpy.step(0.1) # Advance 100ms dt = mcrfpy.step(0.1) # Advance 100ms
print(f"Advanced by {dt} seconds") print(f"Advanced by {dt} seconds")
# Advance by integer (converts to float) # Advance by integer (converts to float)
dt = mcrfpy.step(1) # Advance 1 second dt = mcrfpy.step(1) # Advance 1 second
# Advance to next scheduled event (timer or animation) # Advance to next scheduled event (timer or animation)
dt = mcrfpy.step(None) # or mcrfpy.step() dt = mcrfpy.step(None) # or mcrfpy.step()
print(f"Advanced {dt} seconds to next event") print(f"Advanced {dt} seconds to next event")
``` ```
### What step() Does ### What step() Does
1. Advances internal `simulation_time` by the specified duration 1. Advances internal `simulation_time` by the specified duration
2. Updates all active animations 2. Updates all active animations
3. Fires any timers whose intervals have elapsed 3. Fires any timers whose intervals have elapsed
4. Does NOT render (call `screenshot()` to trigger render) 4. Does NOT render (call `screenshot()` to trigger render)
--- ---
## Timers in Headless Mode ## Timers in Headless Mode
Timers work with simulation time. Timer callbacks receive `(timer_object, runtime_ms)`: Timers work with simulation time. Timer callbacks receive `(timer_object, runtime_ms)`:
```python ```python
import mcrfpy import mcrfpy
import sys import sys
fired = [False] fired = [False]
def on_timer(timer, runtime): def on_timer(timer, runtime):
"""Timer callback receives (timer, runtime_ms).""" """Timer callback receives (timer, runtime_ms)."""
fired[0] = True fired[0] = True
print(f"Timer fired at {runtime}ms") print(f"Timer fired at {runtime}ms")
# Create timer for 500ms interval # Create timer for 500ms interval
t = mcrfpy.Timer("my_timer", on_timer, 500) t = mcrfpy.Timer("my_timer", on_timer, 500)
# Advance past the timer interval # Advance past the timer interval
mcrfpy.step(0.6) # 600ms - timer will fire mcrfpy.step(0.6) # 600ms - timer will fire
if fired[0]: if fired[0]:
print("Success!") print("Success!")
t.stop() # Clean up timer t.stop() # Clean up timer
``` ```
### Timer API ### Timer API
```python ```python
t = mcrfpy.Timer("name", callback, interval_ms) t = mcrfpy.Timer("name", callback, interval_ms)
# Control methods # Control methods
t.stop() # Stop the timer t.stop() # Stop the timer
t.pause() # Pause (can resume later) t.pause() # Pause (can resume later)
t.resume() # Resume after pause t.resume() # Resume after pause
t.restart() # Restart from beginning t.restart() # Restart from beginning
# Properties # Properties
t.name # Timer name (str) t.name # Timer name (str)
t.interval # Interval in ms (int) t.interval # Interval in ms (int)
t.active # Is timer running? (bool) t.active # Is timer running? (bool)
t.paused # Is timer paused? (bool) t.paused # Is timer paused? (bool)
t.stopped # Is timer stopped? (bool) t.stopped # Is timer stopped? (bool)
t.remaining # Time until next fire (float) t.remaining # Time until next fire (float)
t.once # Fire only once? (bool) t.once # Fire only once? (bool)
t.callback # The callback function t.callback # The callback function
``` ```
--- ---
## Animations with step() ## Animations with step()
Animations update when `step()` is called: Animations update when `step()` is called:
```python ```python
import mcrfpy import mcrfpy
scene = mcrfpy.Scene("test") scene = mcrfpy.Scene("test")
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
scene.children.append(frame) scene.children.append(frame)
mcrfpy.current_scene = scene mcrfpy.current_scene = scene
# Start animation: move x from 0 to 500 over 2 seconds # Start animation: move x from 0 to 500 over 2 seconds
frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_IN_OUT) frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_IN_OUT)
# Advance halfway through animation # Advance halfway through animation
mcrfpy.step(1.0) mcrfpy.step(1.0)
print(f"Frame x: {frame.x}") # ~250 (halfway) print(f"Frame x: {frame.x}") # ~250 (halfway)
# Complete the animation # Complete the animation
mcrfpy.step(1.0) mcrfpy.step(1.0)
print(f"Frame x: {frame.x}") # 500 (complete) print(f"Frame x: {frame.x}") # 500 (complete)
``` ```
--- ---
## Synchronous Screenshots ## Synchronous Screenshots
In headless mode, `automation.screenshot()` is synchronous - it renders the current scene state before capturing: In headless mode, `automation.screenshot()` is synchronous - it renders the current scene state before capturing:
```python ```python
import mcrfpy import mcrfpy
from mcrfpy import automation from mcrfpy import automation
scene = mcrfpy.Scene("test") scene = mcrfpy.Scene("test")
ui = scene.children ui = scene.children
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200), frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200),
fill_color=mcrfpy.Color(255, 0, 0)) fill_color=mcrfpy.Color(255, 0, 0))
ui.append(frame) ui.append(frame)
mcrfpy.current_scene = scene mcrfpy.current_scene = scene
# Screenshot captures current state immediately (no timer needed) # Screenshot captures current state immediately (no timer needed)
automation.screenshot("/tmp/red_frame.png") automation.screenshot("/tmp/red_frame.png")
# Change to green # Change to green
frame.fill_color = mcrfpy.Color(0, 255, 0) frame.fill_color = mcrfpy.Color(0, 255, 0)
# Next screenshot shows green # Next screenshot shows green
automation.screenshot("/tmp/green_frame.png") automation.screenshot("/tmp/green_frame.png")
``` ```
### Windowed vs Headless Screenshot Behavior ### Windowed vs Headless Screenshot Behavior
| Mode | Behavior | | Mode | Behavior |
|------|----------| |------|----------|
| Windowed | Captures previous frame's buffer (async) | | Windowed | Captures previous frame's buffer (async) |
| Headless | Renders current state, then captures (sync) | | Headless | Renders current state, then captures (sync) |
In windowed mode, you need timer callbacks to ensure the frame has rendered: In windowed mode, you need timer callbacks to ensure the frame has rendered:
```python ```python
# Windowed mode pattern (NOT needed in headless) # Windowed mode pattern (NOT needed in headless)
def capture(timer, runtime): def capture(timer, runtime):
automation.screenshot("output.png") automation.screenshot("output.png")
sys.exit(0) sys.exit(0)
capture_timer = mcrfpy.Timer("capture", capture, 100) capture_timer = mcrfpy.Timer("capture", capture, 100)
``` ```
--- ---
## Use Cases ## Use Cases
### Automated Testing ### Automated Testing
```python ```python
"""Headless test example.""" """Headless test example."""
import mcrfpy import mcrfpy
from mcrfpy import automation from mcrfpy import automation
import sys import sys
# Setup # Setup
scene = mcrfpy.Scene("test") scene = mcrfpy.Scene("test")
mcrfpy.current_scene = scene mcrfpy.current_scene = scene
frame = mcrfpy.Frame(pos=(50, 50), size=(100, 100)) frame = mcrfpy.Frame(pos=(50, 50), size=(100, 100))
scene.children.append(frame) scene.children.append(frame)
# Verify # Verify
assert frame.x == 50 assert frame.x == 50
assert frame.w == 100 assert frame.w == 100
# Take verification screenshot # Take verification screenshot
automation.screenshot("/tmp/test_output.png") automation.screenshot("/tmp/test_output.png")
print("PASS") print("PASS")
sys.exit(0) sys.exit(0)
``` ```
### LLM Agent Orchestration ### LLM Agent Orchestration
The `step()` function enables external agents to control simulation pacing: The `step()` function enables external agents to control simulation pacing:
```python ```python
import mcrfpy import mcrfpy
from mcrfpy import automation from mcrfpy import automation
def agent_loop(): def agent_loop():
"""External agent controls simulation.""" """External agent controls simulation."""
while not game_over(): while not game_over():
# Agent observes current state # Agent observes current state
automation.screenshot("/tmp/current_state.png") automation.screenshot("/tmp/current_state.png")
# Agent decides action # Agent decides action
action = get_agent_action("/tmp/current_state.png") action = get_agent_action("/tmp/current_state.png")
# Execute action # Execute action
execute_action(action) execute_action(action)
# Advance simulation to see results # Advance simulation to see results
mcrfpy.step(0.1) mcrfpy.step(0.1)
``` ```
### Batch Rendering ### Batch Rendering
Generate multiple frames without real-time constraints: Generate multiple frames without real-time constraints:
```python ```python
import mcrfpy import mcrfpy
from mcrfpy import automation from mcrfpy import automation
for i in range(100): for i in range(100):
update_scene(i) update_scene(i)
mcrfpy.step(1/60) # 60 FPS equivalent mcrfpy.step(1/60) # 60 FPS equivalent
automation.screenshot(f"/tmp/frame_{i:04d}.png") automation.screenshot(f"/tmp/frame_{i:04d}.png")
``` ```
--- ---
## API Reference ## API Reference
**Module Functions:** **Module Functions:**
- `mcrfpy.step(dt=None)` - Advance simulation time - `mcrfpy.step(dt=None)` - Advance simulation time
- `dt` (float or None): Seconds to advance, or None for next event - `dt` (float or None): Seconds to advance, or None for next event
- Returns: Actual time advanced (float) - Returns: Actual time advanced (float)
- In windowed mode: Returns 0.0 (no-op) - In windowed mode: Returns 0.0 (no-op)
**Automation Functions:** **Automation Functions:**
- `automation.screenshot(filename)` - Capture current frame - `automation.screenshot(filename)` - Capture current frame
- In headless: Synchronous (renders then captures) - In headless: Synchronous (renders then captures)
- In windowed: Asynchronous (captures previous frame buffer) - In windowed: Asynchronous (captures previous frame buffer)
--- ---
*Last updated: 2026-02-07* *Last updated: 2026-02-07*