Skip to content

feat(spec5): town-overlay nav — mapflags-aware readPos + minimap + 30-iter action log#14

Closed
ArturSkowronski wants to merge 610 commits into
bfirsh:masterfrom
ArturSkowronski:ff1-buy-and-equip-coneria
Closed

feat(spec5): town-overlay nav — mapflags-aware readPos + minimap + 30-iter action log#14
ArturSkowronski wants to merge 610 commits into
bfirsh:masterfrom
ArturSkowronski:ff1-buy-and-equip-coneria

Conversation

@ArturSkowronski
Copy link
Copy Markdown

Summary

Two layers of work on FF1 Coneria weapon-shop entry (Spec 5 boot phase):

V3 architecture (pre-session, was uncommitted): RAM-triggered boot phase, vision advisor loop driven by Gemini 2.5 Pro, castle short-circuit, sub-goal hierarchy in AdvisorAgent prompt. Removes the v1 EnterConeriaWeaponShop hardcoded sweep.

Empirical fixes (run #1-7 this session):

Empirical trajectory (7 runs)

Run Discovery End-state
#1 Old prompt → Gemini reasons from training data ("town is south of castle"), all cardinals blocked Fail 4 iters
#2 "Fail party not visible" escape hatch → Gemini bails iter 0 Fail 0 iters
#3 mapflags=1 + worldX/Y frozen = town overlay layer (Disch disasm bit 0) Fail 4 iters
#5 readPos fix → movement detected. Gemini myopia: 40 iters circling INN OOB 40 iters
#6 Minimap → broke through to weapon-shop view, but local pathfinding loop Cap 80 iters
#7 Action log 30 + detour prompt → reached weapon shop, opened BUY menu (iter 22-23) Done rejected (mapId=0)

Open architectural debt (next PR)

FF1 NES shops are NPC dialog overlays inside town overlay (mapflags=1, mapId=0), NOT sub-maps. The runOutfitBootPhase inSubShop check (curMapId != 0) is structurally wrong — Gemini visually reached the weapon-shop BUY menu in run #7 but Done was rejected because mapId stayed 0. Next iteration needs:

  1. Shop-UI detection via vision (BUY/SELL/EXIT menu recognition) or RAM (screenState / menuCursor register).
  2. BuyAtShop precondition relaxed to allow mapId=0 && mapflags=1.

Test plan

  • ./gradlew :knes-agent:compileKotlin clean
  • Unit tests: 334 / 3 baseline failures (Coneria8VisualDiff, ConeriaTownEmpiricalDiscovery, ExploreOverworldFrontier — unchanged from prior baseline) / 7 gated skipped
  • Empirical run Frame-synced input queue, MCP/REST game actions, and UI cleanup #7: boot phase reached BUY/SELL/EXIT menu before Done-rejected (~$0.55 budget, fundamental architectural bug exposed)
  • Architectural fix for shop-UI detection (next PR)

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.
V2.2/V2.3/V2.3.1 — Indoors phase + deterministic findPath + town/castle classification
…ust neighbour

V2.4 evidence run revealed false-positive exits flagged inside Coneria town:
internal walls have impassable tiles directly south of passable floor, but the
playable area continues further south. Tightening the check to require the
ENTIRE column south to be impassable filters out internal walls — only outer
south boundary of the playable area qualifies as an implicit exit.
…ypes

Histogram across mapIds 0..30 revealed that:
- Each interior uses its own outside-of-map padding byte: towns mostly 0x39;
  castle/dungeon variants 0x3c, 0x3f, 0x47.
- Walls span 0x30..0x37 (V2.4 only knew 0x30/0x32-0x35 — Coneria town subset).
- 0x31 is consistently floor across maps (must NOT be classified as wall
  even though it falls in the wall range).
- Anything else in 0x00..0x7F is reasonably treated as passable floor /
  decoration / NPC sprite — over-permissive is self-correcting via the
  ExitInterior idle-detection (non-moving step gets fog-marked blocked).

This unblocks mapId=24 (sub-interior reached after walking south through
Coneria town in V2.4.1 evidence run).
V2.4.2 evidence run revealed agent reaches Battle, wins, but gets stuck in
PostBattle (screenState=0x63) — XP/gold results screen requires per-stage A
taps to dismiss. The 10-tap final flush wasn't enough.

Now battleFightAll explicitly loops single A taps with frame buffers between,
checking screenState each iteration, until both BATTLE and POST_BATTLE are
cleared (max 30 taps). Also widens canExecute to cover PostBattle phase so
the executor can call battleFightAll from either state.
… need same as overworld)

V2.4.3 evidence run revealed exitInterior fails despite findPath returning a
17-step path: party never physically moves between iterations because 16
frames isn't enough to consume one indoor tile of motion. The idle-detection
then mis-marked the target tile as blocked; after ~10 iterations all neighbours
were blocked and BFS returned no-path.

WalkOverworldTo bumped 16→24 in V2.3 for the same reason. Apply the same to
ExitInterior.
V2.4.4 evidence run showed party still not moving with 24 frames indoors.
findPath returns valid path but exitInterior loop's button presses don't
register party movement. FF1 interior walk timing differs from overworld;
bump to 48 to provide margin. If still failing, the issue is structural
(input mechanism, not timing) and warrants a different approach.
…ture; V2.4.6 next

State snapshot for next-session pickup. Captures:
- V2.4.x progress: 6 fixes shipped (south-edge, multi-map classifier,
  PostBattle dismiss, frames bumps).
- What's working: ROM decoder, phase classifier with mapId, advisor ASCII
  for indoors, overworld navigation.
- Current blocker: ExitInterior fails when party spawns near map edge —
  16x16 viewport doesn't include the south-edge exit at localY~14-15.
- V2.4.6 plan: BFS the full 64x64 InteriorMap instead of the 16x16
  viewport. Less invasive than refactoring Pathfinder interface — add
  InteriorMap.readFullMapView() returning a 64x64 ViewportMap, pass to
  pathfinder; advisor ASCII rendering stays at 16x16.
Wire 5 interior-scan trace events through InteriorExplorer + InteriorScanner
via a `(String) -> Unit` traceSink param (typealias InteriorTraceSink, defined
independently in both runtime/ and skills/ packages to avoid coupling).

Events emitted:
- interior_scan_triggered (explorer, with frame_detector mode)
- interior_scan_candidates (explorer)
- interior_scan_confirmed  (scanner)
- interior_scan_rejected   (scanner; covers pass2-rejected + invalid-coords)
- interior_scan_error      (scanner)

AgentSession.discoverWeaponShop wires `trace.record` as the sink for both
explorer + scanner. Default `traceSink = {}` keeps existing tests/call sites
unaffected. New test asserts the 3 Found-path events fire.

Spec 4 §7 (interior_explore_outcome already wired by Task 11).
…oser cap

Three empirically-driven fixes after 2026-05-08 cold-start run revealed:

1. Opus_4 → Opus_4_5 (claude-opus-4-0 returns 500; koog 0.6.1 max is
   claude-opus-4-5-20251101). ModelRouter + test updated.

2. GeminiVisionConsult.scanInteriorCandidates: stub → real Gemini Pro
   call. With KNES_VISION=gemini-pro the runtime vision is Gemini, but
   only Pass 2 had real impl — Pass 1 was the empty-list stub from
   Task 2, causing 3-of-3 empty scans → pass1-degraded bailout in
   walkSteps=2. Per user: "pixele najlepiej w gemini".

3. pass1-degraded cap 3 → 10. Pixel-hash fallback triggers a scan per
   walk step (1-byte FNV-1a delta), so 3 consecutive empty Pass 1s
   accumulate before party can explore. 10 lets explorer cover meaningful
   ground before bailout.

Test counts unchanged (333 total, 3 baseline failures, 7 skipped).
Run 5 (commit ce9a23b) succeeded structurally — Gemini Pass 1+2 confirmed
EXIT_TILE in Coneria mapId=8 — but bailed at walk-stuck-after-4-steps.
WalkInteriorVision STUCK is common in Coneria narrow corridors; cap=3
fires before party can re-route.

Bumping consecutiveStuck cap 3→8 + explorer capSteps 50→100 gives
explorer room to navigate past initial stuck states. Test updated.
Empirical run 6 showed explorer used only $0.06 of $3 budget before
walk-stuck cap=8 fired at step 9. Most budget burned in main loop
LLM (Sonnet nav + Opus advisor) after explorer bailed, not in explorer.

Bumping all caps radically lets explorer use more of its budget:
- consecutiveStuck: 8 → 30 (LLM nav often stucks; tolerate longer)
- consecutiveScanEmpty: 10 → 30 (Pass 1 empty in transit corridors OK)
- capSteps: 100 → 200 (full Coneria interior coverage budget)

Tests updated.
Empirical runs 5+6+7 showed 8-30 consecutive STUCK from Sonnet 4.6
navigator in Coneria narrow corridors, regardless of explorer cap.
Per user feedback: Opus advisor should drive interior navigation.

Bumping default model to claude-opus-4-5-20251101 (koog enum doesn't
expose 4.7 yet). Cost goes up ~5x per nav call, but explorer was using
only $0.05 of $3 budget — there's headroom to spend on better nav.
Spec 4: FF1 interior self-discovery landmark scan
Closes Spec 4 empirical gap (run 9 "WrongClass" failures): party stood
in town overlay (mapId=8) when BuyAtShop fired, hence taps opened game
menu not BUY menu. New skill issues hardcoded N+W tap pattern from
Coneria spawn (~smPlayer(12,35)) toward weapon shop building entrance,
detects mapId change as boundary signal, then walks N to face the
shopkeeper. Updates the cached NPC_SHOPKEEPER landmark with the
discovered sub-shop mapId so BuyAtShop's preflight matches.

Wired into runOutfitBootPhase between cachedShop hit and per-char
buy loop, gated on currentMapId==8 (skipped on warm-start where party
already inside).

Coneria-specific POC; generic interior nav is Spec 6+ territory.
…p timing

Run 10 v1 hit west wall at smPlayer(7,30) without finding door. v2:
- Walk N until smPlayerY stops decreasing (= south wall of building)
- Sweep horizontal offsets ±1..±5 along wall, tap N at each
- Detect mapId change after each tap (wall-hit-aligned, sweep-N-at-offset)
- After entry, walk N×6 to face shopkeeper
- Tap timing: pressFrames 6→12, gapFrames 6→8 (FF1 needs full hold for 1-tile move)
Run 12: party reached mapId=24 sub-shop at (12,14) but BuyAtShop
WrongClass'd because keeper was further N. First Down cursor tap
in BuyAtShop walked party south out of shop, dropping mapId back
to 8 mid-purchase. v3 walks N until smPlayerY stops decreasing
(keeper blocks), so party is reliably facing the keeper.
Run 13: party reached mapId=24 sub-shop at (12,18) but blocked by wall
before keeper, BuyAtShop tap-A loop never opened BUY menu (12× WrongClass).

After EnterShop succeeds, capture screenshot and run Pass 1 (Gemini)
to locate shopkeeper sprite. Walk party so it stands at (sx, sy+1) —
directly south of keeper, facing N. Final N-tap ensures facing remains
correct after horizontal moves. Then BuyAtShop fires with correct
positioning.
Run 14: Gemini Pass 1 saw count=0 in mapId=24, confirming we entered
the wrong building. Reorder sweepOffsets to prefer ±3, ±4 first so we
hit a different door. Also dump post-enter screenshot to /tmp when
shopkeeper isn't visible — gives manual inspection point.
Run 15: reordered sweep failed to enter (party stranded at 9,18 mapId=8).
Restore original sweep order (preferring small offsets first) so we
reproduce mapId=24 entry. Always dump post-enter screenshot to disk so
we can manually inspect which building we landed in.
Run 16 dump revealed mapId=24 was Coneria CASTLE entrance hall, not
weapon shop. Walking N×16 from spawn passes through plaza into castle
gate. Limit maxNorthTaps to 7 (mid-plaza) + sweep offsets ±8 to find
shop doors which are below castle gate.
Hardcoded sweep landed in castle entrance (run 16). Replace with
Opus 4.5 advisor that sees screenshot + Coneria layout context (per
Mike's RPG Center map) and recommends single-step actions.

Pipeline:
1. Loop up to 20 iters while mapId=8
2. Each iter: capture screenshot → Opus advise → execute one tap
3. Loop terminates on mapId change OR Done/Fail advice
4. Update cached weapon-shop landmark with discovered mapId

Cost: ~$0.05-0.15 per Opus call × 20 iters worst-case = up to $3 per run.
Within $3 cost-cap budget if advisor finds shop quickly.
Run 20 revealed: KNES_VISION=gemini-pro routes outfitVision to
GeminiVisionConsult, which has only a Fail-stub for adviseShopApproach.
Add a separate outfitAdvisor: HaikuConsult? defaulting to a fresh
AnthropicHaikuConsult instance — its real impl uses Opus 4.5 directly.
runOutfitBootPhase prefers outfitAdvisor if set, falls back to
outfitVision otherwise.
Run 21: Opus reasoning was solid but recommended Up 20x straight.
Party walked from (12,35) to (12,18), hit wall, advisor didn't notice.

Track prevSx/prevSy across iters; when position unchanged, append
'BLOCKED — try Left/Right' to advisor context. Bumps maxAdvisorIters
to 25 to give more search time.
Run 22: advisor switched Left after blocked but entered castle again.
Two improvements:
1. SYSTEM_ADVISOR prompt now includes empirically-observed coordinate
   layout: castle gate at smPlayer(10-12, ~17), weapon shop X~8-9,
   armor shop X~5-7. Explicit warning to AVOID castle gate.
2. After mapId change, run Pass 1 scan to verify shopkeeper visible.
   If not (wrong building), tap Down ×15 to exit, continue advisor loop.
   Give up after 3 wrong-building entries.
…andoff

23 paid runs, ~$63 empirical validation. Spec 4 vision pipeline
validated and merged (PR #119). Spec 5 multi-mapId nav implemented
in two variants (hardcoded sweep + Opus 4.5 advisor) but neither
reliably navigates Coneria plaza to weapon shop door — both land
in castle gate (mapId=24) instead.

Three forward paths documented: ROM pathfinder, FF1 disasm vendoring,
or manually-recorded gameplay path.
…-iter action log

V3 architecture (pre-session, was uncommitted):
  - RAM-triggered boot phase (mapId=0 && char1_str>0) replaces FfPhase trigger
  - Pre-boot deterministic pressStartUntilOverworld
  - Vision advisor loop in runOutfitBootPhase (Gemini 2.5 Pro thinking mode)
  - Castle short-circuit when curMapId in {8, 24}
  - SUB-GOAL HIERARCHY in AdvisorAgent.systemPrompt (T not C; outfit→grind→bridge→shrine)
  - SYSTEM_ADVISOR rewritten: locate-party first, ban training-data game geography
  - Removed v1 EnterConeriaWeaponShop (hardcoded sweep)
  - Coneria8DoorDumpTest diagnostic for ROM-decoded mapId=8 layout

Empirical fixes (this session, run #3-7 evidence):
  - readPos branches on mapflags bit 0, not mapId — town overlay (mapId=0+mf=1)
    reads smPlayerX/Y instead of frozen worldX/Y. Fixes "all 4 cardinals
    blocked" false-Fail seen in run #1, #3.
  - walk_settle 30→120 frames so town transition + sprite render stabilizes
    before advisor takes over (run #2 Fail "party not visible" was mid-render).
  - 3-regime advisor context: OVERWORLD / TOWN_OVERLAY / SUB_MAP, with regime-
    appropriate position semantics ("worldX/Y" vs "smPlayer x,y").
  - Mandatory double-check retry when advisor bails with party-visibility doubt
    (defensive; not exercised post-settle bump).
  - V5.38 minimap: visited-tile set + per-tile blocked-cardinals, rendered as
    13×13 ASCII grid in advisor context. Run #5#6 evidence: with no minimap
    Gemini circled INN for 40 iters; with minimap reached weapon shop in 22.
  - Action log 6→30 + counter-intuitive detour prompt ("walk away from goal
    to break out of pocket"). Run #6#7 evidence: with 6 entries Gemini
    oscillated (13,8)↔(13,9)↔(14,8) for 38 iters; with 30 reached weapon shop
    door tile and opened the BUY/SELL/EXIT menu in run #7 iter 22-23.

Diagnostic dump: each boot-advisor iter persists screenshot to
/tmp/spec5-boot-iter-NN-midM-mfF-posPX_PY-smSX_SY-wWX_WY.png so the position
regime + RAM-vs-engine state can be cross-verified post-mortem.

Open architectural debt (next session): FF1 NES shops are NPC dialog overlays
in the town overlay (mapflags=1, mapId=0), NOT sub-maps. The runOutfitBootPhase
inSubShop check (`curMapId != 0`) is structurally wrong — Gemini visually
reached the weapon shop dialog in run #7 but Done was rejected because mapId
stayed 0. Shop entry detection needs to switch to vision (BUY/SELL/EXIT menu
recognition) or RAM (screenState/menuCursor); BuyAtShop precondition needs to
allow mapId=0 + mapflags=1.
@ArturSkowronski
Copy link
Copy Markdown
Author

Misrouted to upstream — opening in fork repo instead.

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