The Engine Plays Quest: Warrior Dragon: A Milestone-Driven AI Playthrough with Per-Milestone Videos, Checkpoints and a Tray Icon
Proving every tile was reachable did not prove the game could be won. So we taught the robos-jrpg engine to play Quest: Warrior Dragon from King Lorik's throne room to the Dragonlord with real inputs only, cut the two-hour run into one video and one save checkpoint per milestone, added deadlock watchdogs on both sides of the IPC boundary, and put the whole thing in a GNOME tray icon so you can follow it without watching it.
The BFS proof of viability was the first verification layer for the Quest: Warrior Dragon port: a headless walk over every connected (map, col, row) state that shows every chest, door, staircase and the Dragonlord's altar is reachable from the throne room. It caught real routing bugs. It also said nothing about whether the game is beatable. Can a level-1 hero with 120 gold earn the 1500 for a Broad Sword before the Swamp Cave kills him? Does the pack ever fill up so that a quest chest has to be skipped? Does Charlock's basement let you out again after Erdrick's Sword? Those are economy and combat-formula questions, and the only honest test is to play.
A brain that only wants the next milestone
PlayGameBrain.gd is a "play the game" mode of the in-engine AIEngine.gd. It has exactly the affordances a human has: move_up/down/left/right, ui_accept and cmd_menu, and the state the game exposes through its IPC web service (GameControlServer.gd, GET /api/v1/state). No teleports, no stat edits, no reading hidden RNG. What makes it finish the game is not cleverness but a registry: tools/gen_milestones.py derives data/v1/milestones.json, an ordered list of 70 objectives, from the chest, shop, quest and level-curve data: every weapon, armor and shield purchase, every chest, each quest item, a level gate before each dangerous zone, the Dragonlord and the ending. Each entry says where to stand, which command to use, what level and gold are needed, and a done_when predicate. The brain always seeks the next unfinished one.
- Routing:
RoutePlanner.gdplans multi-map routes over the real walkability grids (stairs, doors, bridges, the Rainbow Bridge); the brain walks them with the same stride inputs the demo AI uses. - Survival: 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 restocks at every item shop, MP conservation inside Charlock and HEALMORE-only healing against the Dragonlord. - NES rules: eight tool slots, six per stack, no dropping; a full pack skips a non-gold chest rather than cheating around it.
- Death-adaptive grinding: every death outside a grind raises the current milestone's required level by one, so a run that gets unlucky becomes more careful instead of repeating the mistake.
ui_accept and the cursor keys.
The visibility problem of a two-hour run
At time-scale 8 a full run takes about two hours and roughly 800 random battles. Nobody watches that, and a single two-hour MP4 is nearly useless when you want to know why milestone 42 took ten minutes. So the Python driver (tests/e2e/play_full_game.py) got a --stepped mode: it cuts the ffmpeg capture at every milestone boundary into videos/NN-<id>.mp4, asks the game to export_save_state into checkpoints/NN-<id>.json, and saves screenshots/NN-<id>.png. Because every milestone has a checkpoint, --only N loads checkpoint N-1 and replays just that milestone, so a fix to the Kol item shop can be verified in twenty seconds instead of ninety minutes. --from N resumes a run from a checkpoint to the ending.
Then the tray icon. Stepped and recorded runs now start tools/playthrough-dashboard, a small Electron app that reads the live.json the driver rewrites once a second. In the GNOME top bar it draws a 32-pixel progress ring (cyan running, green done, red crashed or stalled) whose context menu, rebuilt every three seconds, shows status, done/total milestones, level, HP, gold, battles, deaths, the current milestone and the brain's sub-objective, plus Open latest milestone video and Stop run (which sends stop_play_game over IPC). Click it and the dashboard window opens.
Every finished milestone in the list is a card. Click it and the dashboard plays that milestone's video, shows the checkpoint saved right after it (location, level, EXP, gold, HP/MP, ATK/DEF, equipment), the milestone screenshot, and a Replay command button that copies the exact --stepped --only N command line. Reviewing a run became: skim the filmstrip, click the milestone that looks slow, watch sixty seconds, read the checkpoint.
broad_sword equipped with 746 gold left.Two watchdogs, one on each side of the wire
Autonomous runs do not fail loudly; they stall. The brain got a wall-clock progress watchdog: 90 seconds with no change in milestone, battle count, level, gold, map, tile, herbs or HP and it drops the current objective and plan, blacklists an unreachable supply shop, and after three stalls on the same milestone skips it if optional or fails the run. The driver got --stall-timeout (600 s by default): when the done-count, battles, level, gold and EXP all stop moving, it ends the run with status stalled, a stall.png and the tail of the brain's event log. The first thing the pair caught was a wandering villager in Kol who parked himself in the one-tile item-shop doorway and deadlocked the herb restock forever.
Every bug it hit was a game bug
The rule in the game's AGENTS.md is "fix the game, not the brain", and it held. The playthrough found the Charlock 1F exit gate unwalkable (reachable on the BFS grid, impassable in play), boss fights not syncing the pack, duplicate chests, missing level-up curves, incorrect enemy flee factors and spell resistances, a Cantlin item shop with no stock, Wings that did nothing on the overworld, consumables that refused to stack, and a Magic Armor / Erdrick's Armor tie that made the brain re-buy armor it already outclassed. None of these were visible to the BFS audit or to the chained Cucumber checkpoints, because they only appear when a hero with the real economy and real stats tries to progress.
Results, and how to run it
With the game data fixed, the brain clears all 70 milestones and beats the Dragonlord at level 21–22 in one or two Charlock expeditions with at most a single death; the screenshots in this post are from a later run that cleared all 74 milestones with 2,621 battles and 6 deaths. The playthrough.json report, the per-milestone videos and the checkpoints are the proof of work. To try it: python3 games/quest-warrior-dragon/tests/e2e/play_full_game.py --stepped runs the whole game headless with the tray icon and dashboard; --until 18 stops after the Copper Sword, --only 42 replays one milestone, DW_HEADED=1 shows the game on your desktop. The dashboard also runs on its own against any reports folder (npx electron --no-sandbox . --reports ... --port ...) and has --snapshot out.png and --select N flags so headless agents can screenshot it, which is how the dashboard images in this post were taken.