Quest: Warrior Dragon · Autonomous Agent Governance

AI Playthrough

The robos-jrpg engine plays the whole game — King Lorik's throne room to the Dragonlord — with nothing a human player does not have: real controller inputs and what is on the screen. Every milestone becomes a video, a save checkpoint and a screenshot, and a GNOME tray icon lets you follow a two-hour run without watching it.

Why

Reachable is not the same as beatable

The BFS proof of viability walks every connected (map, col, row) state and proves every chest, stair, door and the Dragonlord's altar can be reached. It says nothing about whether a hero with the gold, levels, spells and herbs the game actually hands out can get there alive, afford the Broad Sword, survive Charlock's basement, or beat the Dragonlord's second form. Those are economy, combat-formula and inventory questions, and the only honest way to answer them is to play the game.

So the game plays itself. PlayGameBrain.gd is a "play the game" mode of the in-engine AIEngine.gd: it presses move_up/down/left/right, ui_accept and cmd_menu, and reads state through the same HTTP IPC web service the E2E tests use (GameControlServer.gd, GET /api/v1/state). No teleports, no stat edits, no peeking at hidden RNG. If it cannot finish, the game has a bug.

70Milestones in the registry (every chest, purchase, quest item, level gate, boss)
~2 hWall-clock for a full run at time-scale 8
~800Random battles per run
L21–22Hero level when the Dragonlord falls, with 0–1 deaths
Throne room: King Lorik tells how the Dragonlord stole the Ball of Light
#1 — King Lorik's throne room. The run starts where a new game starts.
A Red Slime battle on the way to level 3
#10 — the first level gate (level 3): 24 battles outside Tantegel.
Swamp Cave: Princess Gwaelin asks to be taken back to her father
#41 — Princess Gwaelin rescued in the Swamp Cave at level 11.
Metal Scorpion battle while grinding for level 15
#51 — level 15 (REPEL): a 19-minute grind before the Staff of Rain.

Architecture

Driver, IPC web service, brain, dashboard

Four pieces, three of which only talk HTTP and JSON. The Python driver is "the user": it boots Godot on an Xvfb display, enables the mode with one IPC command and from then on only observes. The brain lives inside the engine and is the only thing that presses buttons. The dashboard reads a live.json the driver rewrites once a second and asks the game for a screenshot every 1.5 s.

flowchart LR
  subgraph Driver["tests/e2e/play_full_game.py (Python)"]
    D1[boot Godot on Xvfb]
    D2[start_play_game]
    D3[poll /api/v1/state + get_milestones]
    D4[assert invariants
stall-timeout watchdog] D5[ffmpeg per-milestone video
export_save_state checkpoint
screenshot] D6[live.json every 1 s
playthrough.json] end subgraph Godot["Godot 4 · games/quest-warrior-dragon"] W["GameControlServer.gd
GET /api/v1/state
POST /api/v1/command"] B["PlayGameBrain.gd
(AIEngine play-the-game mode)"] R["RoutePlanner.gd"] M["data/v1/milestones.json
70 ordered objectives"] G["Game scenes · CombatEngine · PlayerState"] B -->|real inputs| G G -->|state| B B --> R M --> B W <--> B W <--> G end subgraph Dash["RobOS Game Playthrough app (packages/game-playthrough)"] T["GNOME tray icon
progress ring + menu"] V["Dashboard window
live state · screenshots · milestone cards"] end D2 -->|POST command| W D3 -->|HTTP| W D6 -->|reports/playthrough/live.json| V D5 -->|videos/NN-id.mp4
checkpoints/NN-id.json| V V -->|capture_screenshot / stop_play_game| W V --> T

Milestone registry

tools/gen_milestones.py derives data/v1/milestones.json from the chest, shop, quest and level-curve registries: every weapon, armor and shield purchase, every chest, each quest item, level gates before dangerous zones, the Dragonlord and the ending. Each entry says where to stand, which command to use, the min_level and gold needed, and a done_when predicate the brain checks against game state.

The brain

PlayGameBrain.gd always seeks the next unfinished milestone. RoutePlanner.gd plans the multi-map route (stairs, doors, bridges, the Rainbow Bridge) over the real walkability grids; the brain walks it one tile at a time with the same stride inputs the demo AI uses.

Survival policy

Kill-before-die battle maths from combat_formulas.json; zone-aware heal thresholds (survive two worst-case hits from the local encounter pool); tiered HEAL / herb / HEALMORE; inn trips only when out of heal MP and herbs; herb restock from every item shop; MP conservation inside Charlock and HEALMORE-only healing against the Dragonlord. NES pack rules hold: eight tool slots, six per stack, no dropping.

Deadlock watchdog (brain)

A wall-clock progress watchdog fires after 90 s with no change in milestone, battles, level, gold, map, tile, herbs or HP. It drops the current objective and plan, blacklists an unreachable supply shop, and after three stalls on one milestone skips it if optional or fails the run.

Deadlock watchdog (driver)

--stall-timeout (default 600 s) ends a run whose done-count, battles, level, gold and EXP stop changing with status stalled, a screenshots/stall.png and the tail of the brain's event log, so a hung run never burns the night.

Invariants

The driver asserts things a real player could never do: the hero is never standing on water or mountains, HP is never negative, and no input loop repeats forever. Violations are recorded in playthrough.json and shown in the dashboard.

The driver

One video and one checkpoint per milestone

A single two-hour recording is nearly useless for review. In --stepped mode the driver cuts the ffmpeg capture at every milestone boundary into reports/playthrough/videos/NN-<id>.mp4, asks the game to export_save_state into checkpoints/NN-<id>.json and takes screenshots/NN-<id>.png. Trivial milestones (a chest two tiles away) are one-second clips; level gates and expensive purchases are the five-minute grinds. Because every milestone has a checkpoint, --only N loads checkpoints/(N-1)-*.json with load_save_state and replays just that milestone, so a fix can be re-verified in seconds instead of two hours.

Filmstrip: one frame from each of the 74 milestone videos of the winning run, with each clip's length
One frame from each of the 74 milestone videos of the winning run, with its length. Milestones are uneven: most chests take a second or two, while #54, digging up Erdrick's Token, took 22 minutes because the brain had to reach level 17 first.
cd games/quest-warrior-dragon

python3 tests/e2e/play_full_game.py                       # whole game, headless, ~2 h at the default time scale
python3 tests/e2e/play_full_game.py --until 18            # stop after milestone 18 (Copper Sword)
DW_HEADED=1 python3 tests/e2e/play_full_game.py           # watch it on the desktop
python3 tests/e2e/play_full_game.py --record              # + playthrough.mp4 and a 12x time-lapse
python3 tests/e2e/play_full_game.py --stepped             # one video + checkpoint + screenshot per milestone
python3 tests/e2e/play_full_game.py --stepped --only 42   # replay milestone 42 from checkpoints/41-*.json
python3 tests/e2e/play_full_game.py --stepped --from 42   # resume from checkpoint 41 to the ending
python3 tests/e2e/play_full_game.py --stall-timeout 300   # driver-side deadlock detector (default 600 s)
python3 tests/e2e/play_full_game.py --dashboard off       # stepped/recorded runs start the tray + dashboard by default

# Regenerate the milestone registry after changing shops, chests or quests
python3 tools/gen_milestones.py
ArtifactPath under tests/e2e/reports/playthrough/Written
Run reportplaythrough.jsonStatus, battles, deaths, elapsed, violations, per-milestone timings; at the end of the run
Live feedlive.jsonOnce a second: status, done count, stats, battle, objective, recent AI inputs, event log, milestone list
Milestone videovideos/NN-<id>.mp4At each milestone boundary in --stepped mode (_current.mp4 while recording)
Checkpointcheckpoints/NN-<id>.jsonFull save state via export_save_state: level, HP/MP, gold, equipment, items, opened chests, unlocked doors, quest flags, map and tile
Screenshotscreenshots/NN-<id>.png, stall.png, final.pngAt each milestone, on a stall, and when the run ends
Brecconary weapon shop window: What dost thou want? Bamboo Pole 10G
Frame from 13-buy-copper_sword: the brain navigates a real shop menu.
Drakee battle: ERDRICK attacks Drakee
Frame from 15-chest-garinham-1f-garc1 (17 min): grinding Drakees before Garinham's first chest.
Thou hast done well! ERDRICK earns 1 XP and 1 GP
Frame from 10-level-3: the reward text the brain waits through with ui_accept.
Battle spell list with HURT selected against a Druin
Frame from 42-return-gwaelin: HURT picked from the battle spell list against a Druin.

Following a run

The tray icon and the live dashboard

A two-hour autonomous run has a visibility problem: you want to know at a glance whether it is still alive, how far along it is, and what it is doing, without a terminal scrolling JSON. The RobOS Game Playthrough app (packages/game-playthrough) is a small Electron app that stepped and recorded runs start automatically (--dashboard on|off|auto). It stays open after the run; quit it from the tray menu.

GNOME top bar with the Game Playthrough tray icon and its context menu after the winning run
The tray icon's progress ring and context menu, rendered from the winning run's live.json with the menu the app builds.

Tray icon

A 32-px progress ring drawn on a canvas and pushed to the tray whenever status or done-count changes: cyan while running, green when done, grey while waiting, red on a crash or stall. Its context menu, rebuilt every 3 s, shows:

  • One summary line: status · done/total milestones · level · HP · gold · battles · deaths
  • The current milestone (▶ #47 Buy Magic Armor at Rimuldar) and the brain's current objective ("grind for level 12 before …")
  • Show dashboard, Open latest milestone video, Open videos folder, Open reports folder
  • Stop run (sends stop_play_game over IPC; enabled only while the driver is alive) and Quit dashboard

Click the icon to toggle the window; the tooltip carries the same summary line.

Live view

Game Playthrough live view after the winning run: 74 / 74 milestones and the closing credits
The live view at the end of the winning run: 74 / 74 milestones in 2:33:59, 2,621 battles, 6 deaths, and the closing credits on screen.

Header

Status pill (RUNNING, DONE, STALLED, CRASHED), elapsed time and time scale, a progress bar over the 74 milestones, and stat chips: level, HP, MP, gold, EXP, ATK/DEF, battles, deaths, scene and tile. The "Now:" line names the current milestone and the brain's sub-objective.

Live game

A screenshot refreshed every 1.5 s through capture_screenshot (it writes live.png in the reports folder), with the scene and map key beside the heading.

Battle

Current monster and its HP, monster ATK/DEF, hero HP/MP and whether STOPSPELL landed, straight from /api/v1/state's battle block.

Equipment & pack

Weapon, armor, shield and the full pack with stacks collapsed (Herb ×5), so you can see the NES eight-slot rule and herb restocks working.

AI inputs

The last ~20 inputs the brain pressed, including stride targets (move_up(stride:9->100,16)) and the current stride target tile.

Brain event log

The brain's own event log (L11 cast: hurt vs scorpion (hp 44/59 mp 12), field heals, inn trips, watchdog stalls, skipped milestones) plus any invariant violations the driver recorded.

Milestone cards

The left column lists all 70 milestones with their type and whether a checkpoint exists; "follow run" keeps the current one in view. Every finished milestone is a clickable card that plays its video, shows the save checkpoint taken right after it (location, level, EXP, gold, HP/MP, ATK/DEF, equipment) and the milestone screenshot. Reveal video opens the file in the file manager; Replay command copies the exact play_full_game.py --stepped --only N command line.

Milestone card #1 Hear King Lorik's quest with its video and the checkpoint: ThroneRoom, level 1
#1 Hear King Lorik's quest — a four-second clip and the first checkpoint, level 1.
Milestone card #15 Grind + Open chest garc1 in Garinham with its video and checkpoint: level 8, 586 gold
#15 Grind + Open chest garc1 in Garinham — 17 minutes of grinding to level 7 and gold for keys; checkpoint at level 8 with 586 gold.
Milestone card #31 Grind + Reach level 10 with its video and checkpoint: WorldMap (6,60), level 10, 617 gold
#31 Grind + Reach level 10 (STOPSPELL) — 143 s on the overworld; checkpoint at WorldMap (6,60), level 10, 617 gold.
Milestone card #41 Rescue Princess Gwaelin with a Wolf battle frame and checkpoint: SwampCave (3,22), level 11
#41 Rescue Princess Gwaelin — the Swamp Cave clip; checkpoint at SwampCave (3,22), level 11.
Milestone card #39 Grind + Buy Broad Sword at Rimuldar with its video and checkpoint: broad_sword, 746 gold
#39 Grind + Buy Broad Sword at Rimuldar (1500G) — 494 s of grinding for the gold; the checkpoint confirms broad_sword equipped with 746 gold left.
# Run the dashboard on its own against any reports folder / game port
cd packages/game-playthrough && npm install
npx electron --no-sandbox . --reports ../../games/quest-warrior-dragon/tests/e2e/reports/playthrough --port 19260

# Debug aids for headless agents
npx electron --no-sandbox . --snapshot out.png               # write a PNG of the window after 5 s
npx electron --no-sandbox . --select 44 --snapshot out.png   # open milestone 44's card first

What it found

Every bug the brain hit was a game bug

The rule in games/quest-warrior-dragon/AGENTS.md is "fix the game, not the brain", and it held. Teaching the AI to finish the game surfaced defects that neither the BFS audit nor the chained Cucumber checkpoints could see, because they only show up when a hero with the real economy and real stats tries to progress:

  • Charlock 1F exit gate unwalkable — reachable on the BFS grid, but the AI could never leave the castle after Erdrick's Sword.
  • Kol NPC blocking the item-shop doorway — a wandering villager parked in a one-tile door and deadlocked the herb restock; the watchdog caught it, the fix capped shop retries and moved the NPC.
  • Consumables not stacking — herbs filled all eight tool slots, so chests with quest items were skipped under the NES "no dropping" rule.
  • Missing level-up curves and incorrect flee factors and spell resistances — the hero either could not reach a level gate or kept losing to monsters that should have run.
  • Boss fights not syncing the pack, duplicate chests, a Cantlin item shop with no stock, and Wings doing nothing on the overworld (the brain uses them to fly home from Charlock when its MP commit bar is short).
  • Magic Armor tie rule — Magic Armor and Erdrick's Armor compared equal, so the brain kept re-buying; fixed in the equipment comparison, together with a death-adaptive grind policy (each death outside grinding raises the current milestone's required level by one).
The ITEMS window in Erdrick's Cave: Torch x2, Leather Armor, Small Shield, Copper Sword
#14 — the ITEMS window in Erdrick's Cave: one row per kind, Torch ×2.
Kol village with the command menu open at a chest
#26 — Kol, where an NPC in the item-shop door once deadlocked the run.
Erdrick's Cave B2 in darkness with a one-tile torch radius
#14 — Erdrick's Cave B2 in the dark, lit by a torch.
Cantlin, the walled town, at its first chest
#56 — Cantlin, behind the wall the Golem guards.

Results

What a passing run looks like

With the game data fixed, the brain clears all 74 milestones and beats the Dragonlord at level 21 in a single Charlock expedition. The run that produced every image on this page took 2 h 34 min at time-scale 8, with 2,621 battles and 6 deaths, and you can watch all of it on YouTube. The playthrough.json report, the per-milestone videos and checkpoints are the proof of work; the dashboard is how you read it.