Add "Headless Mode"
parent
f0d8599fe1
commit
47d8592685
|
|
@ -0,0 +1,270 @@
|
|||
# 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 advancement
|
||||
- `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
|
||||
|
||||
Launch McRogueFace with the `--headless` flag:
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
1. Advances internal `simulation_time` by the specified duration
|
||||
2. Updates all active animations
|
||||
3. Fires any timers whose intervals have elapsed
|
||||
4. Does NOT render (call `screenshot()` to trigger render)
|
||||
|
||||
---
|
||||
|
||||
## Timers in Headless Mode
|
||||
|
||||
Timers work with simulation time in headless mode:
|
||||
|
||||
```python
|
||||
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_time` in 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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
# 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:
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
#!/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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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 time
|
||||
- `dt` (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*
|
||||
Loading…
Reference in New Issue