Writing a house's plan from a request in GDScript
A new recipe turns a short request into a full building plan, with a roof solver for rectangles and Ls, and it matches the art kit's six hand-drawn plans.
The game can now write the plan of a building from a short request. I give it a kind (cottage or house), a footprint, a number of storeys, a roof shape, a pitch and a seed, and it hands back every wall bay, frame, door, corner, slope, ridge and hip, plus sockets for lamps, signs and chimneys. To prove it works, it has to make the six plans the art kit’s sheets were drawn from by hand, and it does. This is part one. Rectangles and Ls with gable or hip roofs work, but T, U and H shapes and the later kinds don’t yet, and nothing is placed in the world.

Why does a building need a recipe?
I wanted a generator that can build buildings and proof that it does, but I didn’t want them in the world yet. The art kit already builds single walls from numbers. This is the step above it, where something decides what a whole building is made of. The proof is that the recipe has to reproduce plans that were written by hand.
What goes into a request?
A request has a kind, a footprint, storeys, a roof (gable or hip), a pitch of 30, 45 or 60 degrees, a look, a seed and a state. The footprint comes as wings (boxes, in Blender’s axes), as cells (a list of grid squares) or as squares (a width and a depth). Everything sits on a 3 m grid. A cottage has one storey and 2 to 10 squares. A house has two or three storeys and 4 to 16 squares. Ground storeys are 4 m and upper ones are 3 m. A request the recipe can’t make comes back with a reason and empty lists.
BuildingRecipe.plan(request) is pure and deterministic. With no seed it returns the flat study building, with its eight numbers at 0 and no sockets. With a seed, the numbers and the sockets are drawn from named random streams called “numbers”, “tint” and “sockets”, so asking for one doesn’t change another.
Walls come as a member per bay and storey. A ground wall gets a plinth step, a string course, plinth courses, frames around its openings and elbows at the corners. The recipe also writes the doors, the walkable squares and a far box that the far level of detail uses.
Openings follow four rules, and they’re data in content/buildings/kinds.json, not code. The front’s longest run takes the door in its middle bay, and the other front bays get windows. A wall under a gable stays blank. Other walls get a window every other bay. Long back walls get double windows on the ground storey. Those four rules were enough to reproduce both hand plans.
How does the roof get solved?
I used the dual grid, the corner idea from Townscaper. Instead of reasoning about the outline of the footprint, the solver decides things about the corners of the grid. It cuts the footprint into wings two squares across and carries an L’s arm into the main wing as far as its ridge. Each wing is roofed as a prism, the minimum of its planes, and the whole roof is the maximum of the wings.
Then each square becomes a slope, a hip or a valley, and each edge becomes an eave, a gable, a ridge or plain. Each grid corner takes one junction from a table: eave end, foot, eave corner, inside eave, ridge butt, ridge T, ridge L, ridge cross, hip end or hip peak. Because every corner looks itself up in the same table, an L doesn’t need a special case beyond its wings. The tests run the whole table in all four turns.
Anything else is refused with a reason in plain words. Two footprints that meet only at a corner come back as “touch only at a corner: two buildings, not one”. A footprint of 8 corners comes back as “only the rectangle and the L are roofed yet”.
How do you prove a generator matches hand-drawn plans?
The art kit has six plans written by hand: a 3 by 2 two-storey house and an L, each at 30, 45 and 60 degrees. The test makes all six again from their own request blocks and compares them. The compare is of the lines as a set, not in order, because the two hand plans were written in two different hand orders, so no one order from the recipe could match both.
The file is written byte for byte in the kit’s format, with its key order, positions in millimeters and Python’s JSON float spacing. So the kit reads the game’s plans unchanged, and the file sizes can be compared too. They’re equal: 21562 and 21562 bytes for one pair, 31555 and 31555 for the other.
The axes were the trap. The plan file is in the game’s axes, with +Y up and z as minus the plan’s y. The recipe works in plan coordinates, with x along the front and y going back, so one function converts, and the kit converts back when it reads. Only request.wings is in Blender’s axes.
The recipe also writes some queries the kit has no member for yet, like the hip pieces, which I’m having built in the kit now.
Also done
tests/building_recipehas 2159 checks and none fail. They make 200 requests twice and check the bytes match, and they check 190 plans for closed storey edges, one door reachable from outside, nothing past the footprint and a roof that covers every square once.tests/building_roofhas 877 checks: 25 corner cases in 4 turns, 156 rectangles and Ls solved and 11 refusals.