new level structure

So, I bit the bullet and did somewhat large rewrite. As you might recall, levels in pyherc are lists of lists, with separate data structure for floor, walls, items, creatures and so on. While this works just fine, there are couple limitations that I wasn’t happy with. The biggest of them is that growing level size after initialization is painful and can only be done towards east and south. This in turn places some limitations on level generation.

When I blogged about level structure last time, I got an question if I had thought of using dictionaries for storing levels. Keys would be coordinate pairs and values would be tiles. This way I can have negative indexes and grow level when needed.

At the same time, I decided to separate functions that are used to manipulate levels from the data that represents levels. While this goes against common OO principles, I wanted to try it out, to see what kind of code I would end up with. The following function creates a new instance of a level (a dictionary really):

(defn new-level [model]
  "create a new level"
  {:model model
   :tiles {}
   :items []
   :characters []})

model is parent data structure and has functions needed for routing events, other entries are self explanatory (I hope).

Tiles are a bit larger, but not any more complex than levels:

(defn new-tile []
  "create a tile with default values"
  {:floor nil
   :wall nil
   :ornamentation nil
   :trap nil
   :tags []
   :items []
   :character nil
   :portal nil})

Most of the entries can only hold a single value, tags and items being exceptions. Tags are used to mark areas of interest (rooms, corridors and such) to help item placement and items list contains all the items that are located in this specific tile.

That’s essentially all the data structures that I need for levels. Rest is just a bunch of functions used to query and manipulate the data (I’m not bothering pretending that I have immutable data structures. We’re all consenting adults and can agree what is allowed and what is not).

For example, manipulating floor is done with following functions:

(defn get-tile [level location]
  "get tile at given location"
  (when (in location (:tiles level))
    (get (:tiles level) location)))

(defn get-or-create-tile [level location]
  "get tile at given location"
  (when (not (in location (:tiles level)))
    (assoc (:tiles level) location (new-tile)))
  (get (:tiles level) location))

(defn floor-tile [level location &optional [tile-id :no-tile]]
  "get/set floor tile at given location"
  (if (!= tile-id :no-tile)
    (do (let [[map-tile (get-or-create-tile level location)]]
          (assoc map-tile :floor tile-id)
          (:floor map-tile)))
    (do (let [[map-tile (get-tile level location)]]
          (when map-tile (:floor map-tile))))))

get-tile returns a tile in a given level and location. If there is no tile in given location (remember, the data structure is sparse and only contains tiles we’re interested in.), a nil is returned.

get-or-create-tile works in the same way, except it will create a new tile if one doesn’t already exist in given location.

floor-tile is where the actual work is done. It will always return id of glyph in given location. If optional 3rd parameter is given, then it changes the glyph before returning it. I’m using keyword :no-tile instead of nil, because sometimes I would like to set the floor to nil.

Retrieving a portal in given location is simple:

#d(defn get-portal [level location]
    "get portal at given location"
    (:portal (get-tile level location)))

I’m making an assumption that a tile exists in given location and thus not checking if get-tile returns valid object or nil.

Adding a trap (or making pretty much any modifications to level) has slightly different semantics. If there is no tile at the given location, a new one will be created with default values. After this, tile is modified:

#d(defn add-trap [level location trap]
    "add trap to level"
    (assoc (get-or-create-tile level location) :trap trap))

By the way, that #d before some of the functions is a reader macro that causes function to be decorated with log-debug decorator (that logs calls to this function and resulting return value):

(defreader d [expr] `(with-decorator log-debug ~expr))

So far, I’m pretty happy how the new level structure turned out. The transition is not complete and for example partitioning it to sections needs to change in the future. There’s some performance penalty involved, but nothing that can be detected while playing (only that some of the tests are taking more time than before).


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