Temple Generation Contract - xopherdeep/do-it-for-the-xp GitHub Wiki
Temple Generation Contract
Status: draft 1, 2026-10-10. Owner: the game design. Implementers: src/lib/engine/dungeons/generator/.
A temple is a Zelda-style dungeon the hero clears while doing chores. Today every temple is hand-authored
(src/lib/engine/temples/*.ts). This contract says what a generated temple must look like, which rules make it
impossible to get stuck, and how a temple resets. The hand-authored temples must satisfy the same rules, so the
solver doubles as a lint for them.
1. Vocabulary
| Term | Meaning |
|---|---|
| Temple | Dungeon from src/lib/engine/core/dungeons/types.ts: id, name, entrance [floorIndex,row,col], maze { floor: char[][] }, rooms { char: Room }. Generated temples use the same shape so registerCustomTemples accepts them. |
| Room | One cell. type is one of the roomTypes.ts kinds. locked { north,south,east,west } marks locked door sides. content holds the chest, beast or shop. |
| Door | A passable edge between two non-wall cells. A locked door needs a small key; the boss door needs the master key. |
| Small key | Consumable. Found in key chests (K___) or dropped by a monster room. One key opens one locked door, after which the door stays open. |
| Master key (boss key) | Not consumable within a run. Opens the boss door only. Found behind the mini-boss or in a chest placed after the mini-boss on the critical path. |
| Critical path | The ordered rooms the solver must pass: entrance → (keys/locks) → mini-boss → master key → boss door → boss → Pegasus chest → gate. |
| Key item | Permanent equipment the temple hands out (map, compass, the temple's tool such as Lantern/Hook Shot/Bombs, the Pegasus). Kept across resets. |
| Blood moon | The temple's reset. Beasts return, chests with consumables refill, small keys and the master key are forgotten, doors re-lock. Key items and the "cleared" badge stay. |
| Theme | What the temple stands for in the family: a room of the house or a category of chores (Kitchen, Yard, Homework). Monster rooms of the temple draw quests from that theme. |
2. Rules (R-numbers are the test names)
- R1 Connected. Every non-wall, non-void room on every floor is reachable from the entrance when all doors are open.
- R2 Keys before locks. Walking from the entrance and opening doors only with keys already collected, the hero can always reach another key or the next critical room. Formally: the solver (section 4) finds a path to the gate.
- Walls and cracked walls (R2 note). A side drawn as a wall (
room.barriers, on either end of the way) is no door to the solver. A side drawn bombable is a door the solver opens only once the hero has reached a Bomb source inside the temple (isBombSourceinroomGrants.ts: a shop room, or a chest or fight-room drop holding a Bomb), so a temple is still finishable without Bombs brought from outside; a cracked side into a boss room stays a Boss door. The engine'sdoorKindand the solver's graph both askbarrierBetween(authoredDoors.ts).
- Walls and cracked walls (R2 note). A side drawn as a wall (
- R3 Key count. The number of small keys placed in the temple ≥ the number of locked doors on the critical path. Extra locked side doors are allowed only if the keys for them are reachable without spending a critical key (the solver checks the worst order: it may spend keys on side doors first, so the generator must place one spare key per optional lock or place optional locks after the surplus).
- R4 Boss key placement. The master key is in a chest (a loot room whose chest is a Dungeon Item chest,
chest: 'dungeon') or a Key room whosekeyIdismaster, reachable only after the mini-boss room. It is never behind the boss door. It is never in the mini-boss room's own chest fields: a fight room has no chest in play (the engine makes chests for loot rooms only), so such a key would never be handed out; a fight room hands a key out only through its drops (content.drops, counted byroomGrants.ts; play hands drops out from Classic Dungeon Temples plan Task 3). The audit and the solver count only what play grants (roomGrants.ts, shared withDungeonManager.initializeChests): a key written into a room that grants none is not a key. - R5 Auto-fights. Entering a mini-boss or boss room starts the fight at once (
content.lockOnEnter: true, the room seals, the fight is the room's bound quest). Winning opens the doors. Losing returns the hero to the entrance with the room uncleared. - R6 Boss door. Exactly one boss room per temple, with exactly one door from the rest of the temple. Every door into the boss room is the boss door, whatever the room's flags say (the engine's rule,
isBossDoor): only the master key opens it, small keys cannot.locked.master: trueon the boss room is kept for reading, not read: the generator emits it and the Temple Creator's Boss Lock writes it, but neither the engine nor the solver looks at it. The Pegasus chest and the gate lie beyond the boss room. - R7 Pegasus. Exactly one Pegasus chest per temple, after the boss. Collecting it marks the temple cleared and unlocks the next region's travel.
- R8 Floors. Stairs come in pairs (
Uon floor n matchesDon floor n+1 at the same row/col). Every teleport circle leads somewhere: the author's target if it names one, else the next circle on its floor (wrapping), else, alone on its floor, the temple's next circle (teleportTargetFor, one rule for the engine and the solver); a circle with no other circle anywhere leads nowhere, which the solver reports. Circles are not pairs, so a ride one way need not come back. A pit drops to the same row/col one floor down, which must be a non-wall room; a pit may instead name its landing (targetLevel/targetCoords), honoured only when that floor exists and lies strictly below the pit (pitDropOfinfloorOrder.ts, one rule for the engine and the solver), so an old own-floor seed falls the old way. A flight of stairs may be locked (content.locked: 'small' | 'master'on the room it climbs FROM): its first climb in a run spends a small key, or needs the Boss Key without spending it; the landing climbs back freely; the solver links that stairs door with the same lock and the Temple Creator's counts add a small-locked climb as one lock. - R9 No dead keys. Every small key is reachable before the boss door (a key behind the boss door is a generation error).
- R10 Size.
rows × colsper floor in 4..9, floors in 1..3; room count per floor ≥ 6; at most 40 % combat rooms; one shop at most; one save room per floor at most; ≥ 1 heal or camp room per temple. - R11 Determinism.
generateTemple(seed, options)is a pure function of its arguments. Same seed, same temple. The seed is stored on the temple (seed) so a family can share one. - R12 Hand-authored temples pass. The solver runs over
src/lib/engine/temples/*.tsin a jest spec; any failure is a bug in the layout, not in the solver.
3. Generator (seeded)
- Plan the skeleton. Pick floors, rows, cols from the options or the seed. Carve a spanning tree on each floor with a drunk-walk/DFS from the entrance (the maze look), then add 10–20 % loops so the map isn't a pure tree.
- Lay the critical path. Choose the boss room as the farthest leaf from the entrance (by tree distance, across floors). Choose the mini-boss as a room on the entrance→boss path at 55–75 % of the distance. The Pegasus chest is a leaf adjacent to the boss room, created if needed. The gate is the entrance (the hero walks out) unless the options ask for a separate
GATE. - Lock the boss door. The boss room gets
locked.master(a note, see R6: the engine bars every door into a boss room whether or not the flag is there). - Place the master key. Always in a loot room (a Dungeon Item chest
{ chest: 'dungeon', dungeon: 'master-key' }) of its own past the mini-boss on the critical path: a leaf whose tree-ancestor is beyond the mini-boss and before the boss, else any free room past the mini-boss. The mini-boss room itself holds no chest (R4); choosing the mini-boss therefore needs a room past it. The oldbossKeyChestoption is ignored (the shipped temples still record it in their header). - Place small locks and keys. For k =
options.locks(1..3): pick an edge on the critical path between entrance and mini-boss; lock it; put a key chest in the subtree before that edge (never behind it). Repeat so each lock has its own key earlier than it. Optional side locks: lock a side-branch edge and add one extra key chest before it (R3). - Dress the rest. Walk the remaining rooms: 25–35 % monster (
q__q/Q__Q, bound to quests of the theme), 10 % loot (consumables), 5 % trap, 5 % puzzle, one shop, heal/camp near the mini-boss, save rooms near stairs, map and compass chests in the first third. - Verify. Run the solver. If it fails (it shouldn't), re-run step 5 with the next sub-seed, at most 8 times, then throw: a thrown generator is a test failure, never a shipped temple.
Options: { theme, floors?, size?: "small"|"medium"|"large", locks?: 0..3, element?: TempleElement, quests?: QuestRef[] }.
4. Solver (the airtight part)
State = (floor, row, col, smallKeys, hasMasterKey, clearedMiniBoss, openedDoors). Breadth-first search from the
entrance. Transitions: move through an open door; move through a locked door if smallKeys > 0 (spend one, mark
the door opened for every state); move through the boss door if hasMasterKey; entering a key chest adds a key;
entering the master-key chest sets hasMasterKey (only if its room is reachable, which with R4 implies the
mini-boss was passed); stairs, teleports and pits move the hero. Result: solvable and the critical order found.
The solver explores the worst key order too: locked side doors are tried first so a layout that only works when
the hero is clever is rejected (R3).
The solver is also the map-hint engine: it can tell the compass which locked door the hero can open right now.
5. Reset (blood moon)
resetTemple(progress, templeMeta) returns new progress with: clearedRooms emptied, openedDoors emptied,
small keys 0, master key false, chests of consumables marked unopened; keyItems (map, compass, the temple tool,
the Pegasus) untouched; timesCleared kept. The reset fires when the Game Master schedules it (weekly by
default) or when the theme's quests roll over; the screen shows "The blood moon rises over the Wind Temple."
6. Themes
A temple binds to a theme (themeId): a house room or a chore category. Monster rooms draw their fight from the
theme's open quests; when the theme has none, the room is a free fight paying AP only (the existing rule). The
theme also picks the floor art, the music (BGM.temples) and the element.
7. Acceptance
tests/unit/TempleSolver.spec.ts: R1–R9 on fixtures (one solvable, one with a key behind its lock, one with the master key behind the boss door, one with an unreachable room). R12 over every authored temple.tests/unit/TempleGenerator.spec.ts: 200 seeds × small/medium/large all solvable; determinism; option bounds.- The Temple Creator in the Admin console gains a "Generate" button (later) that calls the generator and shows the solver's critical path on the preview.