task 6 report - xopherdeep/do-it-for-the-xp GitHub Wiki

Task 6 report: the Eagle as JSON (board #1456)

Status: DONE_WITH_CONCERNS Branch: worktree-agent-a16bbf0f5729b7818, commit 1e8fd835 feat(tools): the Eagle's rooms and doors as JSON, matched from Zelda 1 Level 1 (one commit on top of origin/master 62e9e5fb).

What I built

  • tools/temples/classic/loz_eagle.py (new, 359 lines): reads vendor/LoZ_1.dot, matches its rooms onto the Eagle's 17 cells by a backtracking search, and returns the same JSON object the LTTP converter builds (id, source: "Zelda 1 Level 1 (Eagle)", floors, report). It has the same run() / applied / warnings / errors surface as convert.Conversion. The module docstring carries the rules (graph, match, doors, contents).
  • tools/temples/classic/overrides/wind-temple.json (new): names the one stairway (node 18 -> node 2).
  • tools/temples/classic/dungeons/wind-temple.json (new, generated; sha256 db19584c...a0f15, identical over three runs, one with PYTHONHASHSEED=7).
  • tools/temples/classic/convert.py (176 lines): wind-temple is routed to the Eagle module; the default run is now the eight LTTP temples plus wind-temple; main shares one emit() for writing and printing. Tables() is only built when an LTTP id is wanted.
  • tools/temples/classic/SOURCES.md: the LoZ_1.dot row now says loz_eagle.py reads it.
  • tests/unit/ClassicDungeonJson.spec.ts (338 lines): a Wind branch (the Wind Temple's JSON (the Eagle), 6 tests). The generic checks (sound maze, reachability, items once, key counts) run on the Wind JSON too; the LTTP-only Big Key / one-big-chest rule already skipped non-LTTP sources.

How the match was found

The dot has 19 nodes for 17 Eagle rooms, because two nodes are not rooms:

  1. s (node 7) is the way in from outside: its only edge goes to node 8. So "s = entrance" means node 8, the neighbour of s, is the entrance cell (5,2), not that s is a cell. Without this the graph has 18 room nodes for 17 cells and no match exists.
  2. Node 2 (I, no enemies) is the bow's basement, reached from the push-block room (node 18, tag p). The dot labels that edge l, the same as a shutter, so it cannot be told from a door by its label: it is named in overrides/wind-temple.json (stairways).

That leaves 17 room nodes and 18 doors against the 17 cells and their 18 orthogonal neighbour pairs, and the search finds exactly 2 placements (anchors: node 8 -> (5,2), b node 15 -> (1,4), t node 11 -> (1,5)). They differ only by swapping nodes 5 and 6 (the rooms west and east of the entrance, identical tags and doors), and both give the same JSON, so the converter accepts it and says so (2 placement(s) fit, 1 JSON). If two placements ever gave different JSON, the run stops ("N different layouts fit"). I checked the result against the map image: the locked, shut and open doors drawn between the rooms agree with the matched edges.

Match: entrance 8 -> (5,2); 4 -> (4,2); 3 -> (3,2); 9 -> (3,3) map; 10 -> (3,1); 13 -> (2,2) compass; 14 -> (2,1); 0 -> (2,0) (old man's room); 1 -> (2,3) boomerang; 17 -> (2,4); 15 -> (1,4) boss; 11 -> (1,5) Triforce; 12 -> (1,2); 16 -> (0,2); 18 -> (0,1) push-block room; 5/6 -> (5,1)/(5,3); 2 -> B1 basement. Map at (3,3) and compass at (2,2) agree with today's wind-temple.ts.

What is in the JSON

  • 1F: 6x6, exactly the 17 Eagle cells. B1: 1x1, the bow's basement. cellsPerFloor {B1: 1, 1F: 17}.
  • Doors (18 on 1F): 6 small-key (entrance-north, (3,1)-(2,1), (2,2)-(2,3), (0,2)-(0,1), (1,2)-(0,2), boss door (2,4)-(1,4)); 3 bomb ((2,1)-(2,0), (3,3)-(2,3), (3,2)-(2,2)); 2 shutter ((3,2)-(3,1), boss-Triforce (1,4)-(1,5)); 7 open. The boss door is k one way and l the other in the dot; small-key wins (LTTP ranking) and the run prints a note.
  • shutter flag on (3,2) and (1,5): a direction's label is read as the door in the wall of the room it leaves, so the flag goes on the cell that leaves by an l door (same rule as LTTP's "cell whose own door is the trap").
  • Keys: all 6 k rooms also have e, so all 6 are keyDrop: small ((0,2), (1,2), (2,4), (3,2), (5,1), (5,3)); no small-key chests. smallKeys 6, smallLocks 6. No Big Key anywhere.
  • i -> map (node 9, (3,3)) then compass (node 13, (2,2)), in graph order. I: the basement's is big-chest (B1), the boomerang room (2,3) is other.
  • enemies: 1 on each of the 12 e rooms (the dot has no counts), 0 elsewhere; boss cell 0. puzzle on the p rooms (0,1) and (1,2).
  • (0,1) has teleport -> B1 (0,0); the basement has teleport -> 1F (0,1). The Triforce room is identified by its room name Eagle Triforce at (1,5); the schema has no flag for it.
  • Report: mustClear 6 (the 6 key-drop rooms), side 6.

TDD evidence

RED (before any converter code; the Wind JSON did not exist):

$ chemx test tests/unit/ClassicDungeonJson.spec.ts
  ✖ Test Failures (6 failed out of 95)
    ✖ the Wind Temple's JSON (the Eagle) › is Zelda 1 Level 1: a 6x6 floor 1F with exactly the 17 Eagle cells, not the owner's four
      expect(received).toBeDefined()  ... Received: undefined
    ... (the other 5 Wind tests fail the same way: no wind-temple.json)

GREEN (after python3 tools/temples/classic/convert.py wind-temple, and again on the committed tree):

$ chemx test tests/unit/ClassicDungeonJson.spec.ts
  ✔ All tests passed (106 passed in 23571ms)

All eight LTTP dungeons byte-identical:

$ python3 tools/temples/classic/convert.py ; echo $?     # all nine
exit 0
$ git status --short tools/temples/classic/dungeons
?? tools/temples/classic/dungeons/wind-temple.json       # the only change

Failure paths (scratch script on altered copies of the dot; each stops and names the cause): an extra room hung on node 5 -> node 5 [e,k] fits no Eagle cell ... (18 rooms, 17 cells); the old man's room removed -> Eagle cell(s) with no node: [(2, 0)]; a second b node -> needs exactly one 'b' node, not [11, 15]; an extra door across the map -> node 5 [e,k] fits no Eagle cell; an unknown tag, an unknown door label, no stairway named (names node 18 and hints at overrides/wind-temple.json), a stairway target with doors of its own.

Self-review

  • Every file under 500 lines (loz_eagle.py 359, convert.py 176, spec 338). Python stdlib only; deterministic (sorted iteration, fixed search order, same bytes on three runs).
  • Only the spec was run (chemx test tests/unit/ClassicDungeonJson.spec.ts), plus the converter. No typecheck, lint or build, as instructed.
  • The TS spec uses named conditions and tuple types only; no object-literal parameter types.

Concerns (for the lead and Task 9)

  1. The basement is its own floor B1, so the JSON has 18 cells, 17 of them on 1F. The brief says "one floor 1F" and a teleport link must land on a real cell, so something has to hold the bow's big-chest. If you want the JSON to be 1F only, the basement can sit on an empty 1F square such as (0,0) with a one-line change (floor, at in build()), but then 1F holds 18 cells. Task 9's wording "the 17 Eagle cells hold ... big-chest -> relic" needs adjusting: the relic chest is in the B1 room, reached by the teleport at (0,1).
  2. (0,1) clashes with the owner's circle. Today's wind-temple.ts has a teleport circle at (0,1) paired with (5,5). The JSON's (0,1) teleport goes to the basement. The Task 9 override must decide which one the circle is (or move one).
  3. s is not a cell. I read "s = entrance" as the neighbour of s being the entrance cell (see "How the match was found"); there is no other consistent reading.
  4. Direction of door labels (which side of a pair owns an l door) is my reading (the room the edge leaves). It changes only which cell gets the shutter flag ((3,2) and (1,5); the other reading would flag (3,1) and the boss cell), and so mustClear would be 7/5 instead of 6/6.
  5. enemies is 1 per e room because the dot has no counts; p maps to puzzle (the brief does not mention it).
  6. convert.py with no argument now also writes wind-temple.json (it is the ninth).
⚠️ **GitHub.com Fallback** ⚠️