Update "Headless-Mode.-"

John McCardle 2026-02-07 22:23:51 +00:00
commit d10d337eb5

@ -1,270 +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
- `tests/unit/test_step_function.py` - step() tests
- `tests/unit/test_synchronous_screenshot.py` - Screenshot tests ---
--- ## Running in Headless Mode
## Running in Headless Mode Launch McRogueFace with the `--headless` flag:
Launch McRogueFace with the `--headless` flag: ```bash
# Run a script in headless mode
```bash ./mcrogueface --headless --exec my_script.py
# Run a script in headless mode
./mcrogueface --headless --exec my_script.py # Run inline Python
./mcrogueface --headless -c "import mcrfpy; print('Hello headless')"
# Run inline Python ```
./mcrogueface --headless -c "import mcrfpy; print('Hello headless')"
``` In headless mode:
- No window is created (uses RenderTexture internally)
In headless mode: - Simulation time is frozen until `step()` is called
- No window is created (uses RenderTexture internally) - Screenshots capture current state synchronously
- Simulation time is frozen until `step()` is called
- 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
import mcrfpy
```python
import mcrfpy # Advance by specific duration (seconds)
dt = mcrfpy.step(0.1) # Advance 100ms
# Advance by specific duration (seconds) print(f"Advanced by {dt} 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 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()
# Advance to next scheduled event (timer or animation) print(f"Advanced {dt} seconds to next event")
dt = mcrfpy.step(None) # or mcrfpy.step() ```
print(f"Advanced {dt} seconds to next event")
``` ### What step() Does
### Return Value 1. Advances internal `simulation_time` by the specified duration
2. Updates all active animations
`step()` returns the actual time advanced (float, in seconds): 3. Fires any timers whose intervals have elapsed
- In headless mode: Returns the requested dt (or time to next event) 4. Does NOT render (call `screenshot()` to trigger render)
- In windowed mode: Returns 0.0 (no-op, simulation runs via game loop)
---
### What step() Does
## Timers in Headless Mode
1. Advances internal `simulation_time` by the specified duration
2. Updates all active animations Timers work with simulation time. Timer callbacks receive `(timer_object, runtime_ms)`:
3. Fires any timers whose intervals have elapsed
4. Does NOT render (call `screenshot()` to trigger render) ```python
import mcrfpy
--- import sys
## Timers in Headless Mode fired = [False]
Timers work with simulation time in headless mode: def on_timer(timer, runtime):
"""Timer callback receives (timer, runtime_ms)."""
```python fired[0] = True
import mcrfpy print(f"Timer fired at {runtime}ms")
import sys
# Create timer for 500ms interval
fired = [False] t = mcrfpy.Timer("my_timer", on_timer, 500)
def on_timer(runtime): # Advance past the timer interval
"""Timer callback receives simulation time in milliseconds""" mcrfpy.step(0.6) # 600ms - timer will fire
fired[0] = True
print(f"Timer fired at {runtime}ms") if fired[0]:
print("Success!")
# Set timer for 500ms
mcrfpy.setTimer("my_timer", on_timer, 500) t.stop() # Clean up timer
```
# Advance past the timer interval
mcrfpy.step(0.6) # 600ms - timer will fire ### Timer API
if fired[0]: ```python
print("Success!") t = mcrfpy.Timer("name", callback, interval_ms)
mcrfpy.delTimer("my_timer") # Control methods
``` t.stop() # Stop the timer
t.pause() # Pause (can resume later)
### Timer Behavior t.resume() # Resume after pause
t.restart() # Restart from beginning
- Timers use `simulation_time` in headless mode (not wall-clock time)
- Timer callbacks receive the current simulation time in milliseconds # Properties
- Multiple timers can fire in a single `step()` call if intervals overlap t.name # Timer name (str)
t.interval # Interval in ms (int)
--- t.active # Is timer running? (bool)
t.paused # Is timer paused? (bool)
## Synchronous Screenshots t.stopped # Is timer stopped? (bool)
t.remaining # Time until next fire (float)
In headless mode, `automation.screenshot()` is synchronous - it renders the current scene state before capturing: t.once # Fire only once? (bool)
t.callback # The callback function
```python ```
import mcrfpy
from mcrfpy import automation ---
mcrfpy.createScene("test") ## Animations with step()
ui = mcrfpy.sceneUI("test")
Animations update when `step()` is called:
# Add a red frame
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200)) ```python
frame.fill_color = mcrfpy.Color(255, 0, 0) import mcrfpy
ui.append(frame)
scene = mcrfpy.Scene("test")
mcrfpy.setScene("test") frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
scene.children.append(frame)
# Screenshot captures current state immediately (no timer needed) mcrfpy.current_scene = scene
automation.screenshot("/tmp/red_frame.png")
# Start animation: move x from 0 to 500 over 2 seconds
# Change to green frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_IN_OUT)
frame.fill_color = mcrfpy.Color(0, 255, 0)
# Advance halfway through animation
# Next screenshot shows green (not red!) mcrfpy.step(1.0)
automation.screenshot("/tmp/green_frame.png") print(f"Frame x: {frame.x}") # ~250 (halfway)
```
# Complete the animation
### Windowed vs Headless Screenshot Behavior mcrfpy.step(1.0)
print(f"Frame x: {frame.x}") # 500 (complete)
| Mode | Behavior | ```
|------|----------|
| Windowed | Captures previous frame's buffer (async) | ---
| Headless | Renders current state, then captures (sync) |
## Synchronous Screenshots
In windowed mode, you need timer callbacks to ensure the frame has rendered:
In headless mode, `automation.screenshot()` is synchronous - it renders the current scene state before capturing:
```python
# Windowed mode pattern (NOT needed in headless) ```python
def capture(dt): import mcrfpy
automation.screenshot("output.png") from mcrfpy import automation
sys.exit(0)
scene = mcrfpy.Scene("test")
mcrfpy.setTimer("capture", capture, 100) ui = scene.children
```
frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200),
--- fill_color=mcrfpy.Color(255, 0, 0))
ui.append(frame)
## Animations with step() mcrfpy.current_scene = scene
Animations update when `step()` is called: # Screenshot captures current state immediately (no timer needed)
automation.screenshot("/tmp/red_frame.png")
```python
import mcrfpy # Change to green
frame.fill_color = mcrfpy.Color(0, 255, 0)
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
ui.append(frame) # Next screenshot shows green
automation.screenshot("/tmp/green_frame.png")
# Start animation: move x from 0 to 500 over 2 seconds ```
anim = mcrfpy.Animation("x", 500.0, 2.0, "easeInOut")
anim.start(frame) ### Windowed vs Headless Screenshot Behavior
# Advance halfway through animation | Mode | Behavior |
mcrfpy.step(1.0) |------|----------|
print(f"Frame x: {frame.x}") # ~250 (halfway) | Windowed | Captures previous frame's buffer (async) |
| Headless | Renders current state, then captures (sync) |
# Complete the animation
mcrfpy.step(1.0) In windowed mode, you need timer callbacks to ensure the frame has rendered:
print(f"Frame x: {frame.x}") # 500 (complete)
``` ```python
# Windowed mode pattern (NOT needed in headless)
--- def capture(timer, runtime):
automation.screenshot("output.png")
## Use Cases sys.exit(0)
### Automated Testing capture_timer = mcrfpy.Timer("capture", capture, 100)
```
```python
#!/usr/bin/env python3 ---
"""Headless test example"""
import mcrfpy ## Use Cases
from mcrfpy import automation
import sys ### Automated Testing
# Setup ```python
mcrfpy.createScene("test") """Headless test example."""
mcrfpy.setScene("test") import mcrfpy
from mcrfpy import automation
# Test logic import sys
ui = mcrfpy.sceneUI("test")
frame = mcrfpy.Frame(pos=(50, 50), size=(100, 100)) # Setup
ui.append(frame) scene = mcrfpy.Scene("test")
mcrfpy.current_scene = scene
# Verify
assert frame.x == 50 frame = mcrfpy.Frame(pos=(50, 50), size=(100, 100))
assert frame.w == 100 scene.children.append(frame)
# Take verification screenshot # Verify
automation.screenshot("/tmp/test_output.png") assert frame.x == 50
assert frame.w == 100
print("PASS")
sys.exit(0) # Take verification screenshot
``` automation.screenshot("/tmp/test_output.png")
### LLM Agent Orchestration print("PASS")
sys.exit(0)
The `step()` function enables external agents to control simulation pacing: ```
```python ### LLM Agent Orchestration
import mcrfpy
from mcrfpy import automation The `step()` function enables external agents to control simulation pacing:
def agent_loop(): ```python
"""External agent controls simulation""" import mcrfpy
while not game_over(): from mcrfpy import automation
# Agent observes current state
automation.screenshot("/tmp/current_state.png") def agent_loop():
"""External agent controls simulation."""
# Agent decides action (external LLM call) while not game_over():
action = get_agent_action("/tmp/current_state.png") # Agent observes current state
automation.screenshot("/tmp/current_state.png")
# Execute action
execute_action(action) # Agent decides action
action = get_agent_action("/tmp/current_state.png")
# Advance simulation to see results
mcrfpy.step(0.1) # Execute action
``` execute_action(action)
### Batch Rendering # Advance simulation to see results
mcrfpy.step(0.1)
Generate multiple frames without real-time constraints: ```
```python ### Batch Rendering
import mcrfpy
from mcrfpy import automation Generate multiple frames without real-time constraints:
for i in range(100): ```python
# Update scene state import mcrfpy
update_scene(i) from mcrfpy import automation
# Advance animations for i in range(100):
mcrfpy.step(1/60) # 60 FPS equivalent update_scene(i)
mcrfpy.step(1/60) # 60 FPS equivalent
# Capture frame 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: 2025-12-01 - Added for #153*