Mini Runner adds an LLM agent, replay tools, and diagnostics around the preserved Lode Runner runtime. The legacy engine still runs the game; the wrapper and backend only observe state, choose short actions, and save the result.
public/game/*: legacy gameplay, rendering, menus, input, editor, demo recording, and demo playback.src/*: Vite wrapper frontend, recording/playback rail, browser AI loop, and host styles.app.py: Flask API for recordings, traces, model calls, and local JSON stores.agent/*: candidate-agent backend analysis, prompting, model calls, traces, and loop filtering.dash.pyandloader.py: read-only Streamlit and pandas trace dashboard.scripts/*: direct sanity checks, real-browser agent evaluator, and a read-only trace analytics notebook.
- Vite serves
index.html. index.htmlprovides the root<canvas id="canvas">.src/app.jsinserts<base href="/game/">, loads ordered legacy scripts from/game, loadslodeRunner.agentHooks.jsafterlodeRunner.main.js, then callswindow.init().- The legacy runtime creates additional canvases and icon layers on
document.body.
Important legacy files:
lodeRunner.main.js: initialization, canvas sizing, state machine, map build, andmainTick().lodeRunner.runner.js: runner movement, digging, gold pickup, collisions, and exit ladder behavior.lodeRunner.guard.js: guard movement, chase logic, trapping, gold carrying, and respawn.lodeRunner.demo.js: demo recording and playback.lodeRunner.menu.jsandlodeRunner.iconClass.js: menus, side icons, mode transitions, and UI dialogs.lodeRunner.storage.js: local game settings, scores, custom levels, and editor state.
The legacy runtime is load-order dependent and uses shared globals.
Tile maps use fixed 28x16 ASCII grids:
- space /
.empty #diggable brick@solid non-diggable blockHladder-ropeXtrap or dug holeSexit ladder$gold0guard&runner
Example game data:
[
" S ",
" S ",
"#######H####### S ",
" H----------S ",
" H ##H #######H##",
" H ##H H ",
" H ##H H ",
"##H##### ########H#######",
" H H ",
" H H ",
"#########H##########H ",
" H H ",
" H----------H ",
" H###### #######H",
" H H",
"############################"
]Legacy demo record:
"demo": { "action": [], "level": 1, "ai": 4, "time": 90, "state": 1, "godMode": 0, "goldDrop": [], "bornPos": [] }demo.action is a flat array of [tick, keyCode, tick, keyCode, ...] pairs.
The Vite frontend is the bridge between the legacy runtime and the backend. It owns the AI and playback controls, starts and advances the game through the hook, sends snapshots, and stores the finished recording. It does not implement game physics or guard behavior. See Recording and playback for the UI and LLM agent for the request lifecycle.
The frontend provides:
- recording persistence and selected-run playback;
- top debug overlay and playback pause/step controls;
- god-mode and fullscreen convenience buttons;
- browser-side AI solve loop;
- agent traces and opt-in raw model I/O debug logging through the Flask backend.
Legacy snapshot → deterministic analysis → legal candidate generation/scoring → LLM selects
candidateId → generic validation fallback → legacy keyCode/ticks execution →
recording and trace persistence.
- src/agent.js starts the configured game context through public/game/lodeRunner.agentHooks.js.
- The hook starts the legacy game in Training/Modern playback context, stops the normal ticker, and exposes manual
snapshot()/step()control. - The browser sends
playData,level,snapshot, boundedhistory,runId, and optional model selection to/api/agent/next-action. - app.py validates the request and calls
plan_next_action(). - agent/service.py resolves the model and orchestrates candidate planning.
- agent/candidates.py, agent/reasoning_tools.py, and agent/loop_tools.py analyze the snapshot/history, remove confirmed loop actions, and produce ranked eligible candidates.
- agent/prompt.py asks the LLM to choose one candidate by ID.
- The backend validates the selected candidate, applies one generic fallback for malformed or unsafe selection, and returns one bounded legacy action.
- The browser steps the legacy runtime and repeats until success, failure, cancellation, the configured legacy playback-time limit, or the configured step limit.
- src/agent.js saves the final successful or failed demo through the recording API.
app.py exposes the local API used by the frontend. The agent backend accepts a snapshot and
recent history, builds legal choices, asks the model to select one, validates the result, and saves
trace data. It does not simulate the game: the legacy runtime remains the authority for movement,
collisions, and terminal states.
See Backend specification for APIs, configuration, and stored data; Candidate design for candidate rules; and LLM agent for the decision flow.
- agent/config.py: constants, allowed keycodes, model normalization, default model lookup.
- agent/service.py: request validation,
aisuiteclient wrapper, one model call, candidate selection, and generic validation fallback. - agent/candidates.py: normalized analysis and candidate generation/ranking.
- agent/reasoning_tools.py: deterministic snapshot helpers for movement, guard pressure, digging, route access, and progress facts.
- agent/loop_tools.py: compact stationary, horizontal, and vertical cycle detection plus candidate suppression.
- agent/prompt.py: compact candidate-selection prompt.
- agent/traces.py: compact trace serialization for
state, candidates, selection, validation, action, and loop-monitor evidence. - agent/errors.py: request/config/execution error types.
- agent/logging_utils.py: low-noise Python logging setup.
The architecture keeps each layer responsible for one part of a decision:
- Legacy runtime: starts the game, applies keys, and decides what physically happens.
- Frontend: controls the run, captures snapshots, calls the backend, applies returned actions, and saves recordings.
- Backend: turns live state into legal candidates, validates the selected candidate, and writes traces.
- Model: selects from the supplied candidates; it cannot send raw keys or invent a route.
- Tooling: reads saved recordings and traces without changing a run.
The main boundary is the planner request: the frontend sends a snapshot and bounded history; the backend returns one key/tick action. The following snapshot is the evidence of what that action actually did. This avoids a second game simulator and keeps the legacy engine authoritative.
The main risk is drift between legacy state, browser snapshots, candidate analysis, and recorded ticks. Hooks, compact traces, loop evidence, and trace-aligned playback make that boundary inspectable.