Update Headless Mode
parent
449fd3bc63
commit
48a9ed0f7f
1 changed files with 268 additions and 268 deletions
|
|
@ -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*
|
||||||
Loading…
Add table
Add a link
Reference in a new issue