diff --git a/Headless-Mode.md b/Headless-Mode.md new file mode 100644 index 0000000..dd3f030 --- /dev/null +++ b/Headless-Mode.md @@ -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* \ No newline at end of file