Skip to content

V2.2/V2.3/V2.3.1 — Indoors phase + deterministic findPath + town/castle classification#8

Closed
ArturSkowronski wants to merge 363 commits into
bfirsh:masterfrom
ArturSkowronski:ff1-agent-v2
Closed

V2.2/V2.3/V2.3.1 — Indoors phase + deterministic findPath + town/castle classification#8
ArturSkowronski wants to merge 363 commits into
bfirsh:masterfrom
ArturSkowronski:ff1-agent-v2

Conversation

@ArturSkowronski
Copy link
Copy Markdown

Summary

Three stacked feature increments to the FF1 Koog agent. The narrative below is the
full debugging journey
— what we thought the bug was, what the live runs revealed,
how the design evolved.

  • V2.2 — Indoors phase scaffold + exitBuilding skill (1 commit, was already on this branch)
  • V2.3 — Deterministic findPath BFS + ROM-decoded overworld map + ASCII map for advisor (12 commits)
  • V2.3.1 — Town/castle phase classifier fix (2 commits)

Specs: docs/superpowers/specs/2026-05-02-ff1-koog-agent-v2-3-design.md
Plan: docs/superpowers/plans/2026-05-02-ff1-koog-agent-v2-3.md
Evidence: docs/superpowers/runs/2026-05-02-v2-3-deadend-escape/,
docs/superpowers/runs/2026-05-02-v2-3-1-town-fix/


The bug we set out to fix (V2.1 evidence)

Per PR #97 trace: the agent reached Overworld(146, 158) after party creation, walked
6 tiles north to (146, 152), then ran out of budget over 22 turns trying to walk
further north. The model self-diagnosed "stuck inside Coneria castle" — wrong: real
RAM said locationType=0 (overworld). The actual cause was terrain blocking: north
of (146, 152) is mountain. The greedy walkOverworldTo skill had no obstacle awareness,
just kept pressing UP forever.

V2.2 — wrong fix (already on this branch as commit 29aa653)

Initial hypothesis: model was right that party was indoors. Added Indoors phase
detection (locationType==0xD1) + exitBuilding skill (walks SOUTH out of castles).

A re-run showed: the stuck position was Overworld, not Indoors. V2.2 was honest
infrastructure for future castle/town entries, but didn't fix V2.1's actual bug.
Kept on the branch as "valid for later".

V2.3 — research, then design pivot

Research input

Web research on Gemini Plays Pokemon revealed three findings that shaped V2.3:

  1. Tile-grid text representation matched or beat raw screenshots for spatial reasoning.
  2. pathfinder was their main tool — sub-agent in blank context with structured map.
  3. RAM ground-truth wins over model memory.

We brainstormed four V2.3 paths (A: screenshots, B: hardcoded waypoints, C: obstacle-
aware walk, D: advisor-only screenshots) and converged on D'+C — advisor gets ASCII
map + fog-of-war, executor gets a deterministic pathfinder.

Plan revision: pathfinder becomes deterministic

User question "could we give the agent a deterministic tool for path detection in the
viewport?" reframed the design. findPath(targetX, targetY) exposed as @tool, no LLM
tokens consumed, BFS over 16x16 viewport. API stable across versions; only the
SearchSpace enum widens (VIEWPORT today, FOG / FULL_MAP / A* / LLM-pathfinder
later).

First implementation attempt: read PPU nametable

Plan: read 16x16 tile IDs around party from PPU nametable, classify via empirical
tile-classifications/ff1-overworld.json (built by a research test that dumps hex
grids alongside PNG screenshots).

Implemented (T1-T5). Hit a wall at T6:

  • Research test boot sequence didn't progress past title (used controller.setButtons
    • advanceFrames directly; FF1 needs the InputQueue + tap pattern via
      EmulatorToolset).
  • Switched to toolset-driven boot. Now boots correctly — screenshot shows Coneria
    overworld.
  • BUT: NT0 hex dump = menu/HUD garbage, not terrain. Switched to dumping all 4 NTs.
    NT1 has overworld content, but it's the same content before and after walking 4
    tiles north
    (worldY 0x9E -> 0x9A). Conclusion: PPU nametable is not a reliable
    terrain source for FF1
    . Mid-frame writes overwrite NT after rendering; PPUSCROLL
    shifts the visible window; we sample post-frame which catches scratch state.

Design pivot: parse FF1 ROM map directly

FF1 stores its 256x256 overworld as RLE-compressed pointer-table-indexed rows in PRG
bank 1 (file offset 0x4010). Tile bytes are already game-classified (grass/forest/
mountain/water/town/castle/...) per the wiki. Parser is ~50 LOC.

Replaced NametableReader with OverworldMap.fromRom(File) + OverworldTileClassifier.
The interface contract ViewportSource (a fun-interface with readViewport(xy)) lets
OverworldMap and any future map source plug in interchangeably.

5/5 OverworldMap tests pass against the real FF1 ROM. Coneria castle and town tiles
are findable in the bounding box around spawn.

Live run #1 (V2.3 raw)

Out-of-budget after 13 turns. But: party advanced spawn -> (145, 152) in 1 turn,
which is exactly the V2.1 deadend tile. Pathfinder + BFS worked.

Two issues surfaced:

  1. maxIterations=10 on Koog agent was too tight for V2.3's larger tool surface
    (findPath chained with walkOverworldTo). Bumped to 20.
  2. findPath consistently returned BLOCKED at (145, 152) even though the
    OverworldMap showed plenty of grass. Cause discovered in the trace's RAM dump:
    worldX=145, worldY=152, localX=5, localY=28, locationType=0. Party walked INTO
    Coneria town
    ; the overworld coord stayed sticky while local-map navigation took
    over. RamObserver only treated locationType==0xD1 as Indoors -> false-classified
    as Overworld -> walkOverworldTo futilely retried world coords that didn't change.

V2.3.1 — phase classifier fix

RamObserver.classify: phase = Indoors when partyCreated AND
(locationType==0xD1 OR localX/Y != 0). Captures both castle interiors AND town
outdoor maps.

ExitBuilding: terminate condition changed from locationType==0 (towns already
have that, would return ok=true immediately without moving) to
locationType==0 AND localX==0 AND localY==0.

Live run #2 (V2.3.1)

Phase transitions now correct: TitleOrMenu -> Overworld(146, 158) -> Indoors(localX=5, localY=29). Executor calls exitBuilding and battleFightAll appropriately. Party
fights 5 random encounters inside Coneria, advances localY 29 -> 17 (12 tiles of
interior progress).

exitBuilding doesn't fully exit because FF1 castle interiors have multi-segment
geometry (stairs, doors, sub-maps) — a simple "walk SOUTH" loop bumps into walls and
gets reset by battles. This is the next layer of debt (V2.4: ROM-decode castle
interior maps the way V2.3 decoded the overworld), out of this PR's scope.


What this PR ships

  • OverworldMap: parses FF1 RLE pointer table into a 256x256 byte grid
  • ViewportPathfinder: pure-function deterministic BFS over 16x16 viewport
  • FogOfWar: per-run accumulator of seen tiles + confirmed-blocked tiles
  • AsciiMapRenderer: textual viewport for advisor input
  • findPath @tool: deterministic, zero-LLM-token pathfinder exposed to executor
  • WalkOverworldTo refactor: thin shim over findPath; marks non-moving steps as blocked
  • AdvisorAgent prompt augmented with ASCII map + fog stats on Overworld phase
  • RamObserver.observeFull returns Observation(phase, ram, viewportMap?)
  • V2.3.1 fixes: town outdoor map detection + ExitBuilding termination

Test plan

  • All unit tests pass (~40+ tests across pathfinding/perception)
  • OverworldMap decodes real FF1 ROM (5/5 tests)
  • Live run V2.3 raw: V2.1 deadend escaped, evidenced in trace
  • Live run V2.3.1: Indoors phase recognised, executor uses correct skills, 5 battles fought
  • Manual review of trace.jsonl files in evidence runs

Known limitations / V2.4 scope

  • Castle interior navigation: exitBuilding walks SOUTH but FF1 castles have stairs/
    doors that don't form a straight line. V2.4 should decode castle interior maps from
    ROM (analogous to V2.3 overworld decode) and pathfind through them.
  • worldX/worldY to map-coord offset: minor calibration may be needed; pathfinder's
    BFS starting at party-tile may classify the start tile as impassable due to a
    1-tile offset between RAM coords and map indexing.
  • iteration cap may need to be raised again if advisor is chained more frequently.

ArturSkowronski and others added 30 commits April 11, 2025 13:52
Initialize JetBrains Junie 🚀
chore: update Compose 1.8.2 and Gradle wrapper versions
FF1 agent V2 — first autonomous run (boot→overworld), OutOfBudget before Garland
…ecutor input/result, no truncation) + FF1-aware prompts
FF1 agent V2.1 — FF1-aware prompts + ~/.knes traces; agent reasons but stuck in Coneria castle
…g-of-war

Addresses the (146, 152) blocked-by-terrain deadend evidenced in PR #97 by
adding a deterministic 16x16 BFS pathfinder, a fog-of-war accumulator, and
ASCII map rendering for the advisor — keeping the executor's overall control
flow unchanged.

Informed by Gemini Plays Pokemon research: textual tile grids match or beat
raw screenshots for spatial reasoning, so V2.3 starts text-only and defers
screenshot augmentation to V2.4.
…es NametableReader

PPU nametable approach didn't work: FF1 uses scrolling + mid-frame writes, so
NT0/NT1 dumps don't reflect the actual visible terrain at any meaningful sample
point. Pivoted to FF1 ROM map data: pointer table at file offset 0x4010, RLE
rows decoded into a 256x256 ByteArray. Tile bytes are ALREADY game-classified
(grass/forest/mountain/water/town/castle/bridge), so OverworldTileClassifier is
a hardcoded lookup.

Removed: NametableReader, NametableReaderLiveTest, TileClassifierResearchTest.
Added: OverworldMap, OverworldTileClassifier, OverworldMapTest (5 tests pass).

Format reference:
https://datacrystal.tcrf.net/wiki/Final_Fantasy/World_map_data
V2.3 live run revealed party walks INTO Coneria town at overworld tile (145,152)
but RAM says locationType=0 (not 0xD1) while localX/Y populate to non-zero. Our
classifier treated this as Overworld and walkOverworldTo futilely retried world
coords that don't change.

Fix is two-fold:
1) RamObserver: phase = Indoors when partyCreated AND (locationType==0xD1 OR
   localX/Y != 0). Captures both castle interiors AND town outdoor maps.
2) ExitBuilding: terminate on (locationType==0 AND localX==0 AND localY==0)
   rather than just locationType==0 — towns already have locationType=0 entering,
   old condition returned ok=true immediately without moving.

Now the executor will see Indoors → call exitBuilding → walk SOUTH → reach
overworld → resume northward navigation toward Garland's bridge.
@ArturSkowronski
Copy link
Copy Markdown
Author

Wrong repo target; reopening on ArturSkowronski/kNES

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant