Cleaning up level generation

So, after a 5 month break, I got a feeling that I want to write some Hy code. First step was to fix level generation that was a bit broken, but that took only one evening. After that, I wanted to clean up the code a bit.

So, at that point I had a LevelGenerator class with a following method:

def generate_level(self, portal):
    level = new_level(self.model)
    partitioner = self.rng.choice(self.partitioners)
    connector = RandomConnector(self.rng)
    sections = connector.connect_sections(partitioner(level))
    for section in sections:
        generator = self.rng.choice(self.room_generators)
        generator(section)
    for adder in self.portal_adders:
        adder.add_portal(level)

    if portal is not None:
        rooms = list(get_locations_by_tag(level, 'room'))
        if len(rooms) > 0:
            new_portal = Portal(icons=(portal.other_end_icon, None),
                                level_generator_name=None)
            location = self.rng.choice(rooms)
            add_portal(level, location, new_portal, portal)

    for adder in self.creature_adder:
        adder.add_creatures(level)
    for adder in self.item_adder:
        adder.add_items(level)
    for decorator in self.decorator:
        decorator.decorate_level(level)
    return level

Nothing too complicated here. Create a new Level, chop it into partitions, connect them, draw some rooms and sprinkle with details.

The same code translated into Hy looked like this:

(defn new-level-generator [model partitioners room-generators decorators
                           portal-adders item-adders creature-adders
                           rng level-context]
  "create a new level generator function"
  (fn [portal]
    (let [[level (new-level model)]
          [partitioner (.choice rng partitioners)]
          [connector (RandomConnector rng)]
          [sections (.connect-sections connector (partitioner level))]]
      (ap-each sections ((.choice rng room-generators) it))
      (ap-each portal-adders (.add-portal it level))
      (when portal (let [[rooms (list (get-location-by-tag level "room"))]]
        (when rooms (let [[new-portal (Portal #t(portal.other-end-icon nil)
                                      nil)]]
          (add-portal level (.choice rng rooms) new-portal portal)))))
      (ap-each creature-adders (.add-creatures it level))
      (ap-each item-adders (.add-items it level))
      (ap-each decorators (.decorate-level it level))
      level)))

Again, nothing too fancy and a pretty straight forward conversion. There’s some repetition there though: multiple (ap-each foo (.bar it level) calls that loop through a list of builder objects and call a method to build level. I wanted to clean this up and flex my macro-writing skills.

First step was to unify their interface. Easiest way was to add new method:

def __call__(self, level):
    self.add_creatures(level)

This turns the builder object into callable, so I can call it like a function:

(ap-each creature-adders (it level))
(ap-each item-adders (it level))
(ap-each decorators (it level))

Next step was to get rid of the repeating (ap-each foo (it level)) for every type of builder:

(defmacro run-generators-for [level &rest generators]
  `(do ~@(map (fn [x] `(ap-each ~x (it ~level))) generators)))

run-generators-for macro needs two or more parameters. First parameter is the level being generated, rest of the parameters are lists of level builder objects (or functions). It will then create that ap-each code for each and every of them:

(run-generators-for level foo bar baz)
-->
(do
  (ap-each foo (it level))
  (ap-each bar (it level))
  (ap-each baz (it level)))

I reordered some of the code a bit and the end result was following:

(defn new-level-generator [model partitioners room-generators decorators
                           portal-adders item-adders creature-adders
                           rng level-context]
  "create a new level generator function"
  (fn [portal]
    (let [[level (new-level model)]
          [partitioner (.choice rng partitioners)]
          [connector (RandomConnector rng)]
          [sections (.connect-sections connector (partitioner level))]]
      (ap-each sections ((.choice rng room-generators) it))
      (run-generators-for level
                          portal-adders
                          creature-adders
                          item-adders
                          decorators)
      (when portal
        (let [[rooms (list (get-locations-by-tag level "room"))]]
          (when rooms (add-portal level
                                  (.choice rng rooms)
                                  (Portal #t(portal.other-end-icon nil) nil)
                                  portal))))
      level)))

Later on I will probably turn those level builder objects into functions, but this piece of code doesn’t need to know anything about it.

Advertisements

Leave a Reply

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

WordPress.com Logo

You are commenting using your WordPress.com 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 )

Google+ photo

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

Connecting to %s