Update "Headless-Mode.-"

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

@ -10,8 +10,6 @@ McRogueFace supports headless operation for automated testing, CI pipelines, and
**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
--- ---
@ -53,12 +51,6 @@ dt = mcrfpy.step(None) # or mcrfpy.step()
print(f"Advanced {dt} seconds to next event") 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 ### What step() Does
1. Advances internal `simulation_time` by the specified duration 1. Advances internal `simulation_time` by the specified duration
@ -70,7 +62,7 @@ print(f"Advanced {dt} seconds to next event")
## Timers in Headless Mode ## Timers in Headless Mode
Timers work with simulation time in headless mode: Timers work with simulation time. Timer callbacks receive `(timer_object, runtime_ms)`:
```python ```python
import mcrfpy import mcrfpy
@ -78,13 +70,13 @@ import sys
fired = [False] fired = [False]
def on_timer(runtime): def on_timer(timer, runtime):
"""Timer callback receives simulation time in milliseconds""" """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")
# Set timer for 500ms # Create timer for 500ms interval
mcrfpy.setTimer("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
@ -92,14 +84,56 @@ mcrfpy.step(0.6) # 600ms - timer will fire
if fired[0]: if fired[0]:
print("Success!") print("Success!")
mcrfpy.delTimer("my_timer") t.stop() # Clean up timer
``` ```
### Timer Behavior ### Timer API
- Timers use `simulation_time` in headless mode (not wall-clock time) ```python
- Timer callbacks receive the current simulation time in milliseconds t = mcrfpy.Timer("name", callback, interval_ms)
- Multiple timers can fire in a single `step()` call if intervals overlap
# Control methods
t.stop() # Stop the timer
t.pause() # Pause (can resume later)
t.resume() # Resume after pause
t.restart() # Restart from beginning
# Properties
t.name # Timer name (str)
t.interval # Interval in ms (int)
t.active # Is timer running? (bool)
t.paused # Is timer paused? (bool)
t.stopped # Is timer stopped? (bool)
t.remaining # Time until next fire (float)
t.once # Fire only once? (bool)
t.callback # The callback function
```
---
## Animations with step()
Animations update when `step()` is called:
```python
import mcrfpy
scene = mcrfpy.Scene("test")
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
scene.children.append(frame)
mcrfpy.current_scene = scene
# Start animation: move x from 0 to 500 over 2 seconds
frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_IN_OUT)
# 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)
```
--- ---
@ -111,15 +145,13 @@ In headless mode, `automation.screenshot()` is synchronous - it renders the curr
import mcrfpy import mcrfpy
from mcrfpy import automation from mcrfpy import automation
mcrfpy.createScene("test") scene = mcrfpy.Scene("test")
ui = mcrfpy.sceneUI("test") ui = scene.children
# Add a red frame 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))
frame.fill_color = mcrfpy.Color(255, 0, 0)
ui.append(frame) ui.append(frame)
mcrfpy.current_scene = scene
mcrfpy.setScene("test")
# 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")
@ -127,7 +159,7 @@ 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 (not red!) # Next screenshot shows green
automation.screenshot("/tmp/green_frame.png") automation.screenshot("/tmp/green_frame.png")
``` ```
@ -142,36 +174,11 @@ 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(dt): def capture(timer, runtime):
automation.screenshot("output.png") automation.screenshot("output.png")
sys.exit(0) sys.exit(0)
mcrfpy.setTimer("capture", capture, 100) capture_timer = mcrfpy.Timer("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)
``` ```
--- ---
@ -181,20 +188,17 @@ print(f"Frame x: {frame.x}") # 500 (complete)
### Automated Testing ### Automated Testing
```python ```python
#!/usr/bin/env python3 """Headless test example."""
"""Headless test example"""
import mcrfpy import mcrfpy
from mcrfpy import automation from mcrfpy import automation
import sys import sys
# Setup # Setup
mcrfpy.createScene("test") scene = mcrfpy.Scene("test")
mcrfpy.setScene("test") mcrfpy.current_scene = scene
# Test logic
ui = mcrfpy.sceneUI("test")
frame = mcrfpy.Frame(pos=(50, 50), size=(100, 100)) frame = mcrfpy.Frame(pos=(50, 50), size=(100, 100))
ui.append(frame) scene.children.append(frame)
# Verify # Verify
assert frame.x == 50 assert frame.x == 50
@ -216,12 +220,12 @@ 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 (external LLM call) # Agent decides action
action = get_agent_action("/tmp/current_state.png") action = get_agent_action("/tmp/current_state.png")
# Execute action # Execute action
@ -240,13 +244,8 @@ import mcrfpy
from mcrfpy import automation from mcrfpy import automation
for i in range(100): for i in range(100):
# Update scene state
update_scene(i) update_scene(i)
# Advance animations
mcrfpy.step(1/60) # 60 FPS equivalent 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")
``` ```
@ -267,4 +266,4 @@ for i in range(100):
--- ---
*Last updated: 2025-12-01 - Added for #153* *Last updated: 2026-02-07*