Birdoggydog's Builds

Laying dungeon monsters by room kind and drawing sheets

Room kinds are now rows of a table, a population block lays monsters and chests from rules, and a Python tool draws a 9-room dungeon sheet in about a second.

Three more of the dungeon review’s ideas are built. A room’s kind is now a row of a table that says what goes in it, a dungeon can have its monsters, chests, plates and lamps laid from rules about those kinds, and a Python tool draws a dungeon’s floor sheets from a plain description. The Breakout Site plays exactly as before, and every existing dungeon imports byte for byte the same.

Why did dungeons still cost so much to fill?

The review had found that the Breakout Site’s 58 monsters were 58 hand-written rows, about 2 M tokens of agent work, and that drawing a dungeon’s sheets by hand on the phone designer page cost about 2.7 M tokens each. I’d already decided to leave the Breakout Site’s monsters alone, so I wanted the rules proven on a new dungeon. That’s the Rendering Annex, a test dungeon with 9 rooms on 2 floors that isn’t reachable from a new game.

How do room kinds drive everything in a room?

59 seconds: the two generated floors, then each room of the annex walked with a caption saying its kind and what the rules put in it, ending with the boss alone in the vat room.

A room’s kind was already read from the words of its note, but only to pick furniture. I moved those 15 word rows out of content/dressing.json into a new content/room_kinds.json, in the same order, and added a 16th kind, a ward. Each row now also has a share of the floor’s monsters, a chance of a chest and what it holds, a chance of a pressure plate, a share of lamp energy and an amount of floor meat. The vat hall, for example, has no furniture, 1.5 times the usual monsters and lamps at 60 percent.

A sheet names a kind the way it always did, with a word in a note, or now with rooms.<x,y>.kind in the content file, which wins over the note. Old sheets read the same because the lookup is the dresser’s old algorithm, moved into a class called RoomKinds.

How does a population block lay a room?

A dungeon whose content file has a population block gets it read once, after the rooms are found, in DungeonPlan._work_out(). A dungeon with no block, like the Breakout Site, isn’t touched.

"population": {
  "seed": 11,
  "floors": {"1": {"meat_sack": 3, "meat_boy": 1}, "2": {"meat_boy": 2, "meat_beast": 1}},
  "squares": 10, "least": 1, "most": 3,
  "empty": ["Lower Landing"],
  "boss": {"monster": "meat_monster", "room": "Rendering Floor"}
}

For each room, a random generator seeded from the dungeon, the room and the block’s seed decides a chest and a plate with the kind’s chances. The monster count is the room’s area times the kind’s share divided by squares, clamped between least and most. Each monster is a pack of one, put on the square farthest from the others in that room, with a type picked by the floor’s weights from the ones whose body fits there. The boss gets a room to itself.

The placement rules are the ones my monster test already held the hand-placed site to, now the generator’s checks. A monster never stands on a thing or on a door, never in the start room or anywhere that can see it, and never within three squares of the start or a lift. It only goes where its full height fits under the ceiling, so a flyer needs a tall room. A hand-placed thing always wins: a room with a pack, chest or trap of the sheet’s gets none of that type from the rules.

Rule things are ordinary things of the plan marked "from": "rule", so they’re checked, reported and built like hand-placed ones, and a refinement can skip one.

The first version put three packs where they couldn’t see the way in, so the import warned about them. I changed the rule to prefer squares that see more of the way in, and the annex’s warnings went to none. The numbers in the kind rows are guesses I made by eye, and I haven’t tuned them.

How is a sheet drawn from a description?

A contact sheet of eight frames. The first shows two floor plans of numbered rooms joined by corridors, with the start, doors and monsters marked. The others are first-person views of rooms with furniture and red meat growth on the walls.

The two generated floors top left, then the cell, kitchen, lab, office and vat rooms as walked in the game.

I wrote the generator, tools/sheet_draw.py, in Python and not GDScript because a session can run it from the shell in about a second with no engine start-up. The description is JSON: rooms with a name, size, kind, height and note, links of four sorts (door, corridor, narrow, crawl), lifts between floors and a population block that names rooms by name. The annex’s description is 2.3 KB.

The layout is a tree grown from the floor’s first room, which is the start room on floor 1 and the room the lift arrives in below. Each linked room goes on one side of the one before, with the way across straight, and the sides and offsets are tried in a seeded order. The gap is lengthened until nothing touches. A link that would close a loop is routed around through rock with a breadth-first search over squares that touch no other room, or dropped with a warning if there isn’t one. Floor 1’s start is a corridor out to the edge of the sheet, and a lift is a one-square nook in a room’s wall.

It writes the designer page’s format (tiles, things, heights), so I can edit the result on the phone, and with --content it writes the game’s content file in the same layout as the importer, so bringing the sheet in again writes the same bytes.

The annex came out with 16 monsters, 3 chests and 1 plate, and the boss alone in the vat room on the lower floor. A new dungeon now costs rooms, links, lifts and one population block, and only the NPCs, keys and locks, story items, fixtures and named packs are still placed by hand.

How it was checked

tests/dungeon_rules passes 35 checks, including the Breakout Site being unchanged, the same seed laying the same and a different seed laying differently, a hand-placed thing beating a rule, and a walk through all 9 rooms. The dungeon part of the suite, 22 tests, passes. Three tests failed along the way, all because of the annex file, and each was fixed as above, in the test, or in the generator’s output layout.

Still open: the kind numbers need tuning, and the description is JSON that I or an agent has to write from the page’s free text by hand.