3 Headless Mode
John McCardle edited this page 2026-02-07 23:49:00 +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

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")

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. Timer callbacks receive (timer_object, runtime_ms):

import mcrfpy
import sys

fired = [False]

def on_timer(timer, runtime):
    """Timer callback receives (timer, runtime_ms)."""
    fired[0] = True
    print(f"Timer fired at {runtime}ms")

# Create timer for 500ms interval
t = mcrfpy.Timer("my_timer", on_timer, 500)

# Advance past the timer interval
mcrfpy.step(0.6)  # 600ms - timer will fire

if fired[0]:
    print("Success!")

t.stop()  # Clean up timer

Timer API

t = mcrfpy.Timer("name", callback, interval_ms)

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

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)

Synchronous Screenshots

In headless mode, automation.screenshot() is synchronous - it renders the current scene state before capturing:

import mcrfpy
from mcrfpy import automation

scene = mcrfpy.Scene("test")
ui = scene.children

frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200),
                      fill_color=mcrfpy.Color(255, 0, 0))
ui.append(frame)
mcrfpy.current_scene = scene

# 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
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(timer, runtime):
    automation.screenshot("output.png")
    sys.exit(0)

capture_timer = mcrfpy.Timer("capture", capture, 100)

Use Cases

Automated Testing

"""Headless test example."""
import mcrfpy
from mcrfpy import automation
import sys

# Setup
scene = mcrfpy.Scene("test")
mcrfpy.current_scene = scene

frame = mcrfpy.Frame(pos=(50, 50), size=(100, 100))
scene.children.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
        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(i)
    mcrfpy.step(1/60)  # 60 FPS equivalent
    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: 2026-02-07