Drawing dungeon maps from a description on a phone page
A phone designer where I write what a place is and get back a drawn map and a list of what the game lacks, plus a work list refiled as a tree in three writes.
The phone page I design Blobber’s dungeons on works the other way around now. I write what a place is and what happens there, and an agent draws the map and attaches a manifest of everything the dungeon needs, with each line marked “have” or “to build”. I tried it first on the game’s starting dungeon, and it came back as four floors joined by lifts. The work list page also turned into a tree of areas and features with tags and goals. Both happened on 10 October.
Flipping the designer from drawing to describing
Digging out every room with my finger was slow. I wanted to describe a dungeon and get a map back, along with a list of the props and features the game doesn’t have yet. I also wanted to save dungeons, see a list of them, and see what is and isn’t built for each one.
The page is a single HTML file, published as a private web page, and the only extra thing it can do is use a small shared document database. Each dungeon map is saved as one document:
{ id, name, w, h,
tiles: [row strings] '#' rock '.' floor '~' shallow water ',' narrow passage ':' crawl tunnel
heights: [row strings] 'l' low 2.5 m 'n' normal 4 m 't' tall 8 m 'c' cavern 14 m
things: [{t, x, y, uid, ...}] t: entrance exit door key chest trap pack npc item note
notes, status, rev, updatedAt,
manifest: [{name, kind, why, have}], draftNote }
The canvas is drawn with the 2D context. One finger uses the current tool, and two fingers pan and zoom. Every edit bumps rev, saves the map to the browser’s local storage right away and to the shared store 1.2 seconds later, so a map survives a dropped connection.
A map has a status. draft, ready and built were already there. The two new ones are described (I’ve written the description and asked for a drawing) and drafted (the drawing is done).
How does a drawing get back to the phone without being overwritten?
The agent reads the store directly through a tool and doesn’t go through the page. At the start of a session it looks for maps with status == "described" and draws them. Then it writes back the tiles, things, heights, manifest and a note, with status: "drafted" and a rev higher than the map had.
The page had always pushed its local copy up when it started. That would overwrite a drawing made while my phone was away. I fixed that with a revision rule on the page. On start, if the store’s copy of the open map has a higher rev than the local one, the page loads the store’s copy and doesn’t save. In the live snapshot listener, if I’m not in the middle of an edit and the store’s rev is higher, the page reloads the map in place, and it keeps my pan and zoom if the size hasn’t changed. The agent’s writes are pinned to the document version it last read, so its write fails if I edited the map in the meantime.
Drawing four floors from a script
My description of the starting dungeon asked for several floors. Each one is a glimpse of another faction, overrun so only a small part can be explored, and they’re joined by lifts, not ramps or stairs. The party starts naked in a vat and leaves with weak gear, and the last floor opens into a tower.
The designer and the game’s importer both handle one level per map, so the answer was four maps. The floor I’d drawn myself was kept, with three changes. The start moved into the vat, my old way out became the lift up, and two notes mark glimpses of the wider complex.
The three new floors were drawn by a short Python script. It has a grid of rock, a rect() function that digs floor, water or a passage and sets a ceiling height, and an add() function that places a thing with a fresh id. A checker then asserts there’s exactly one entrance, flood-fills from it and requires that every dug square can be reached. It rejects a thing placed in rock, two things on one square, and a chest, key, character, entrance or exit sitting in a passage.
- Floor 1, cyberpunk, mine: 36 by 36, 650 dug squares, 59 things. The birthing chamber and its vat, five experiment cells, labs, bunks, a meat vat chamber under a cavern ceiling, a security hall.
- Floor 2, steampunk: 24 by 24, 193 squares. A two-story pump gallery and a boiler hall seen past meat.
- Floor 3, eldritch: 24 by 24, 154 squares. A low wet decontamination hall and a crawl tunnel ending at a grate that looks onto a server shrine.
- Floor 4, high fantasy: 24 by 24, 182 squares. A guard hall and the foot of a tower whose machinery calls lightning down, with the way out.
Each floor has a larger hall drawn beyond a square with a note on it: “meat choke: seen, not entered”. The space is on the map so it can be built and seen. The choke is a line in the manifest, because the game doesn’t have anything yet that seals a passage but lets the party see past it.
Four separate maps showed up as four dungeons in the list, so I had the page learn that a dungeon can have several floors. A floor’s map has of (the id of the first map) and level (2, 3 and so on). The list groups them under the first map’s name, and a strip under the title switches between floors.
What does “have” mean on a manifest?
Before writing the manifest, the agent read the importer’s documentation and the content files, so “have” was checked and not a guess. The importer makes each square 3 m. It works out rooms as the largest rectangles with one ceiling height. From the things on the map it places doors (plain, keyed, coded, pickable), chests, four traps, monster packs, keys, item pickups and talking characters. The only way in is a ramp cut from the surface.
Floor 1’s manifest has 29 lines: 8 “have” and 21 “to build”. The other floors have 6, 8 and 6 lines. The “to build” list includes several floors in one dungeon, a lift, starting inside with no gear, a console that unlocks doors from another room, windows and grates, the meat choke, and walls, lights and furniture for each faction. Furniture and a few item sets were marked “to build” without checking each one, and the draft says so.
The page also lists everything that’s still to build across all dungeons. It takes every manifest line that isn’t a “have”, keys it by its lower-cased name, collects which dungeons want it, and sorts by how many do.
Filing 590 work items into a tree with three writes
The work list page was one long ledger of 590 items, in seven sections by type. I was starting to lose track of threads of development and how far along each one was. It’s now 12 areas with 60 features inside them, and the items sit under each feature. Every level collapses and has its own tally. Tags cut across the tree, so picking “animation” and “combat” narrows it to combat animation. A second view lists goals, and a goal’s progress is the share of items done under its features.
The page stores each item as its own document. Adding a feature and tags to every one would have taken 590 pinned writes. So the first filing went in as a single document, a 34.6 KB map from item id to feature and tags:
feature = item.feature ?? filing[id].f
tags = item.tags if it is a list, else filing[id].t
When I move an item on the page, the filing gets written onto the item itself, and that takes priority. The tree of areas and features is a second single document, so I can add a feature without republishing the page.
Each area and feature is a <details> element. A feature’s items are only built the first time it’s opened, and each item’s node is cached by id. That way a half-typed answer survives the re-layout that happens on every database snapshot.
The first filing was a table written feature by feature from the 590 titles, and checked so that no item is filed twice or left out. Then 14 keyword rules over each item’s text added the cross-cutting tags. It was done from titles and keywords and hasn’t been checked item by item.
Next
Floors and lifts in the game’s importer, so I can walk the four maps.