higher order functions and level generators

I did some cleaning of room generators this week and although it is not yet finished, I’m starting to see where I’m headed now.

To recap: level is divided into sections and they are linked together forming the basic topography. Rooms are generated in sections and connected to the edges of sections via corridors. So one section, one room.

Below is an example of what room generators previously looked like. CircularRoomGenerator can create a basic shape and TempleRoomGenerator can place something interesting in the middle of it. Notice how TempleRoomGenerator inherits CircularRoomGenerator and uses center-point attribute to decide where to place shrine and candles.

(defclass CircularRoomGenerator []
  "generator for circular rooms"
  [[--init-- (fn [self floor-tile corridor-tile level-types]
               "default constructor"
               (setv self.center-point nil)
               (setv self.floor-tile floor-tile)
               (setv self.corridor-tile corridor-tile)
               (setv self.level-types level-types)
               nil)]
   [generate-room (fn [self section]
                    "generate a new room"
                    (let [[center-x (// (section-width section) 2)]
                          [center-y (// (section-height section) 2)]
                          [center-point #t(center-x center-y)]
                          [radius (min [(- center-x 2) (- center-y 2)])]]
                      (for [x_loc (range (section-width section))]
                        (for [y_loc (range (section-height section))]
                          (when (<= (distance-between #t(x_loc y_loc) center-point) radius)
                            (section-floor section #t(x_loc y_loc) self.floor-tile "room"))))
                      (add-room-connection section #t(center-x (- center-y radius)) "up")
                      (add-room-connection section #t(center-x (+ center-y radius)) "down")
                      (add-room-connection section #t((- center-x radius) center-y) "left")
                      (add-room-connection section #t((+ center-x radius) center-y) "right")
                      (.add-corridors self section)
                      (setv self.center-point center-point)))]
   [add-corridors (fn [self section]
                    "add corridors"
                    (ap-each (section-connections section)
                             (let [[room-connection (match-section-to-room section it)]
                                   [corridor (CorridorGenerator room-connection
                                                                (.translate-to-section it)
                                                                nil
                                                                self.corridor-tile)]]
                               (.generate corridor))))]])

(defclass TempleRoomGenerator [CircularRoomGenerator]
  "generator for temple rooms"
  [[--init-- (fn [self floor-tile corridor-tile temple-tile level-types &optional candle-tile]
               "default constructor"
               (-> (super) (.--init-- floor-tile corridor-tile level-types))
               (setv self.temple-tile temple-tile)
               (setv self.candle-tile candle-tile)
               nil)]
   [generate-room (fn [self section]
                    "generate a new room"
                    (-> (super) (.generate-room section))
                    (let [[#t(x-loc y-loc) self.center-point]]
                      (section-ornamentation section #t(x-loc y-loc) self.temple-tile)
                      (when self.candle-tile
                        (section-ornamentation section #t((+ x-loc 1) y-loc) self.candle-tile)
                        (section-ornamentation section #t((- x-loc 1) y-loc) self.candle-tile))))]])

That’s quite a bit of code and makes sharing algorithms between generators harder than what I would like it to be.

After some tinkering around, the following is what I ended up with:

(defn new-room-generator [&rest creators]
  "create a room generator"
  (fn [section]
    (ap-each creators (it section))))

(defn circular-shape [floor-tile]
  "create a circular shape"
  (fn [section]
    (let [[center-x (// (section-width section) 2)]
          [center-y (// (section-height section) 2)]
          [center-point #t(center-x center-y)]
          [radius (min [(- center-x 2) (- center-y 2)])]]
      (for [x_loc (range (section-width section))]
        (for [y_loc (range (section-height section))]
          (when (<= (distance-between #t(x_loc y_loc) center-point) radius)
            (section-floor section #t(x_loc y_loc) floor-tile "room"))))
      (section-data section :center-point center-point)
      (add-room-connection section #t(center-x (- center-y radius)) "up")
      (add-room-connection section #t(center-x (+ center-y radius)) "down")
      (add-room-connection section #t((- center-x radius) center-y) "left")
      (add-room-connection section #t((+ center-x radius) center-y) "right"))))

(defn cache-creator [cache-tile item-selector character-selector]
  "create cache creator"
  (fn [section]
    "fill cache with items and characters"
    (section-floor section (section-data section :center-point) cache-tile)))

No classes, just functions. new-room-generator is a factory function, that will create me a new room generator function. The created function will simply call each and every function supplied to the factory function in turn and use them to generate a new room. circular-shape for example creates a circular room, while cache-creator is used to create a hidden cache in the location specified by center-point.

Wiring all that stuff by hand every time one wants to create a new room generator function probably gets a bit tedious. Enter configuration functions:

(defn temple [floor-tile corridor-tile cache-tile item-generator character-generator]
  (new-room-generator (circular-shape floor-tile)
                      (corridors corridor-tile)
                      (cache-creator cache-tile
                                     (tomes-and-potions-cache item-generator)
                                     (no-characters-cache character-generator))))

(defn plain-room [floor-tile corridor-tile ]
  (new-room-generator (square-shape floor-tile random)
                      (corridors corridor-tile)))

Now I can call temple and supply needed parameters (mainly tiles I want to use and generator functions that can create new monsters and items). The function takes care of extra wiring and returns a fully configured function that can be used to create a temple after temple.

plain-room is similar, but it creates plain square rooms without anything fancy. I could have a definition like:

(defn plain-circular-room [floor-tile corridor-tile ]
  (new-room-generator (circular-shape floor-tile)
                      (corridors corridor-tile)))

Otherwise the very same result, except the room is now circular (because of the circular-shape function). This means that I can now more easily share functionality and compose new rooms from pieces that I have created. The ultimate goal is to figure out how I can extract routines that decide where to place pillars or shrines into own functions. Now the functionality is in the functions that create basic shape and I don’t think that will work in the long run.

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