1 Headless Mode
John McCardle edited this page 2025-12-02 02:59:09 +00:00

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:

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:

# 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:

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:

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:

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:

# 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:

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

#!/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:

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:

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