Little level dsl tricks

I have been tinkering with how levels are configured and made some minor changes. I’m trying to keep things relatively simple, but at the same time I want to have configuration code to look more like a configuration file than a function definition.

Below is shown a configuration for area called “first gate”. This is the area where the game starts. Two requires at the top are used to signal that I want to use certain macros. (level-config-dsl) is just a cute way to have pile of imports without cluttering the code. After that follows a list of levels, with just single entry. That entry first contains name and description of the level and rest is just configuring how it connects to other levels and how it is going to be generated. I made a post earlier explaining how the level generation works.

(require pyherc.macros)
(require pyherc.config.dsl.level)


(level "first gate"
#s("The first gate is where your long journey begins. This is perfect"
"area for gathering some supplies before venturing further in the"
(connections (unique-stairs "first gate" "lower caverns"
"grey stairs" "room" certainly))
(layout (regular-grid #t(20 20) #t(10 10))
(regular-grid #t(20 10) #t(10 10))
(regular-grid #t(10 20) #t(10 10)))
(room-list (circular-room "ground_tile3" "ground_soil4")
(circular-band-room "ground_wood4" "ground_soil4"
(square-room "ground_tile3" "ground_soil4")
(square-room "ground_soil4" "ground_soil4")
(circular-room-with-candles "ground_wood4"
(touch-up (wall-builder "wall_rubble6")
(floor-builder "ground_soil4")
(floor-builder "ground_soil3")
(floor-builder "ground_tile3")
(floor-builder "ground_tile4")
(floor-builder "ground_wood4")
(wall-cracker "wall_rubble6" unlikely)
(support-beams "wall_rubble6" "wooden beams" unlikely)
(wall-torches "wall_rubble6" almost-certainly-not))
(item-lists (option (item-by-type 2 3 "weapon")
(item-by-type 2 3 "armour")
(item-by-type 2 4 "potion")
(item-by-type 1 4 "food")
(item-by-type 1 1 "hint")
(item-by-type 0 2 "boots")))))

From point of view of the game, level configuration is done with functions that have specific signature and are located under specific package. This allows them to be discovered at the start up. Definition of the macro, that creates that function for us is as follows:

(defmacro level-list [&rest levels]
`(defn init-level [rng item-generator creature-generator level-size context]

Very basic macro, that just creates the function definition with correct signature and stuffs all the parameters given to it inside of the function as code. All those parameters are expected to be level-macros, but currently there’s no check for that.

Level-macro is where things get interesting. System expects a return value of dictionary, with certain keys and values. I wanted to be able to specify different elements in any given order (except for the name and description) and possibly leave out some of them. Not all elements are required in every level. For example, first gate doesn’t have any monsters present, so I didn’t want to specify just an empty list there (same functionality could be achieved with a simple function and keywords).

(defmacro level [level-name description &rest elements]
"create new instance of level config"
(let [[room-generators nil]
[partitioners nil]
[decorators '[]]
[items '[]]
[characters '[]]
[portal-config '[]]]
(ap-each elements
(cond [(= 'room-list (first it)) (set-branch room-generators it)]
[(= 'layout (first it)) (set-branch partitioners it)]
[(= 'touch-up (first it)) (set-branch decorators it)]
[(= 'item-lists (first it)) (set-branch items it)]
[(= 'creature-lists (first it)) (set-branch characters it)]
[(= 'connections (first it)) (set-branch portal-config it)]
[true (macro-error it "unknown config element")]))
(if-not room-generators (macro-error nil "room-list not defined"))
(if-not partitioners (macro-error nil "layout not defined"))
`{:level-name ~level-name
:description ~description
:room-generators ~room-generators
:partitioners ~partitioners
:decorators ~decorators
:items ~items
:characters ~characters
:portal-config ~portal-config}))

(defmacro set-branch [branch code]
`(if-not ~branch
(setv ~branch ~code)
(macro-error ~code "duplicate config element")))

First macro initializes some local variables and starts looping through code supplied to it. It picks up and stores code to correct variable, depending on the config element being used. Duplicate entries cause an error (raised by set-branch macro). Then there are two checks for mandatory elements (room-generators and partitioners), that are always required for generating level. Last step is to create a dictionary containing the code and emit that as code.

Most of the inner elements are very simple. They just wrap code given to them into an array and could actually be implemented as functions, instead of macros.

(defmacro room-list [&rest rooms]

Macros for items and creatures are specified slightly differently.

(defmacro item-lists [&rest items]
`(ap-map (ItemAdder item-generator it rng) [~@items]))

(defmacro creature-lists [&rest creatures]
`(ap-map (CreatureAdder creature-generator it rng) [~@creatures]))

In order the system to work correctly, ItemAdder and CreatureAdder instances need to be supplied with some services (factories for creating items, creatures and random number generators). Such details don’t belong to configuration file, so they are passed in from ambient context (notice that rng for example isn’t ~rng. Values for these are supplied by system when init-level function is being invoked.)

Item and creature lists use option element. This is just a macro that wraps its contests inside of a list. System can then randomly pick one of these lists and generate content based on that. This allows me to have item or creature packs. A level might have 3 rats and 3 spiders or 1 giant spider.

That’s about the current state of the configuration system. I have a feeling that it will continue to evolve a little bit still. One thing I’m not very happy with is that for example item-list needs to use option element, even when there is only one possible list of items to generate. Later I’m planning to change this to work in a way that option element is needed only when there are more than one possible item list.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s