Manipulating levels in Adderall

So, I have a goal: I wish to be able to reason and manipulate levels in Adderall (a miniKanren implementation in Hy). First step is to start with floors and walls. Here is our simple level:

{:wall-data {(, 5 5) :rock-wall
             (, 6 5) :rock-wall
             (, 7 5) :tile-wall}
 :floor-data {(, 5 6) :dirt-floor
              (, 6 6) :rock-floor
              (, 7 6) :rock-floor}}

Since I don’t yet know how to manipulate dictionaries, I’ll transform this into list of lists:

[[:wall-data [(, (, 5 5) :rock-wall)
              (, (, 6 5) :rock-wall)
              (, (, 7 5) :tile-wall)]]
 [:floor-data [(, (, 5 6) :dirt-floor)
               (, (, 6 6) :rock-floor)
               (, (, 7 6) :rock-floor)]]]

In order to manipulate walls and floors, we need to have access to them in the datastructure. For walls, I wrote the following:

(defn wall-dataᵒ [level wall-data]
  (fresh [x]
    (≡ x [:wall-data wall-data])
    (memberᵒ x level)))

First, a fresh variable x is introduced. The x is unified with a list that has first item as :wall-data (a keyword) and rest is wall-data parameter. The final step is to say that x should be found in level (again supplied as a parameter). Floor-data is retrieved in the same way:

(defn floor-dataᵒ [level floor-data]
  (fresh [x]
    (≡ x [:floor-data floor-data])
    (memberᵒ x level)))

Now that we have access to correct part of the datastructure, we can start manipulating it:

(defn wallᵒ [level location id]
  (fresh [data tile]
    (wall-dataᵒ level data)
    (≡ tile (, location id))
    (memberᵒ tile data)))

wallᵒ takes three parameters, first is the overall datastructure, second is location we are interested in and third one is the ID of tile in that location.

Two fresh variables are introduced: data and tile. wall-dataᵒ is then used to drill down to correct position of the data structure and associate data with wall-data information. Then we unify tile with a tupple that consists of location and id and say that this tupple should be part of the data. Again, floor is handled in the same way:

(defn floorᵒ [level location id]
  (fresh [data tile]
    (floor-dataᵒ level data)
    (≡ tile (, location id))
    (memberᵒ tile data)))

Now we have the tools that we need at this point. But how are we going to use them? Lets first create a variable and assign some data into it:

(setv level-data 
    [(, (, 5 5) :rock-wall)
     (, (, 6 5) :rock-wall)
     (, (, 7 5) :tile-wall)]]
    [(, (, 5 6) :dirt-floor)
     (, (, 6 6) :rock-floor)
     (, (, 7 6) :rock-floor)]]])

Easy way to use these functions is to query what is in given location:

=> (run* [q]
     (wallᵒ level-data (, 6 5) q))

Looks simple enough. At (6 5) there is :rock-wall.

How about asking for all locations that have :rock-floor?

=> (run* [q]
     (floorᵒ level-data q :rock-floor))
[(6, 6), (7, 6)]

Similar query than before, but with q at different position.

To check if a given location has a specific type of floor, we can use following two examples:

=> (run* [q]
     (floorᵒ level-data (, 5 6) :dirt-floor)
     (≡ true q))

=> (run* [q]
     (floorᵒ level-data (, 6 6) :dirt-floor)
     (≡ true q))

The first goal associated level-data, given location and given floor type together. If this association succeeds, q is unified with true in the second line. If association fails, q is not unified and an empty list is returned as a result.

What about retrieving locations of the floor tiles and their IDs?

=> (run* [q]
     (fresh [location id]
       (floorᵒ level-data location id)
       (≡ (, location id) q)))
[((5, 6), :dirt-floor), ((6, 6), :rock-floor), ((7, 6), :rock-floor)]

We first introduce two fresh variables and associate them with level-data using floorᵒ. Then we unify q with a tupple that consists of location and id (we can always return just a one variable, thus we need to perform this extra step). Because we used run*, all found results are returned. We could have limited the amount to 2 by doing run 2 [q], instead of run* [q].

A little more complex (and pretty useless at this point) is shown next:

=> (run 1 [q]
    (wallᵒ q (, 1 1) :rock-wall)
    (floorᵒ q (, 1 2) :dirt-floor)
    (floorᵒ q (, 1 3) :dirt-floor)) 
[([:wall-data ([[1 1] :rock-wall] . <'_.0'>)]
  [:floor-data ([[1 2] :dirt-floor] [[1 3] :dirt-floor] . <'_.1'>)] . <'_.2'>)]

“if we have :rock-wall at (1 1) and :dirt-floor at (1 2) and (1 3), what would the level look like?” Here we are interested only in one answer (thus run 1 [q]). Since the lists we are dealing with could have more entries in addition to the ones that we specified, there are fresh variables shown: <‘_.0’>, <‘_.1’>, <‘_.2’>. These could be anything, they don’t affect to the validity of the result.

All this is still at very basic level. We have created goals that define overall data structure of levels and specific data structures of walls and floors. With these 4 goals we can query, check and create levels. More interesting this will be after we add monsters and traps into mix and are able to specify their spatial relations (there is a monster standing in between a wall and a pit). At that point we would have simple tools that can be used to aid placing things during level generation or that can be used by character AIs to reason their surroundings.

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