Rooms part 2

It's been a while! I've had to temporary put this project to one side due to time constraints but I'm now back. I'm aiming to make at least one post per week to try and force a habit. At some point in the future I'll even have nailed down the day I make these posts each week but for now lets take baby steps.

This week I've been working on improvements to the zone system including making the graph weighted, locks and directions


Locks are a mechanism to restrict access to a room unless the player meets a specific requirement. I've stolen the term lock from the paper Generating_Missions_and_Spaces_for_Adaptable_Play_Experiences which I intended to implement so that I can procedurally generate dungeons using L-Systems. Currently I have two different kind of locks based on if the player has an item or a flag.

To kick things off I've had to design the rough layout of the player structure:

1(def player {

2 :name "wibble"

3 :class :mage

4 :ability {:name :resilient :description "Takes less damage when health is below 30%"}

5 :stats {:health 100

6 :critical-strike 5

7 :critical-multiplier 1.5

8 :attack-speed 0.8

9 :armour 15

10 :resistance 15

11 :damage 10

12 :magic-damage 10

13 :inventory-slots 30}

14 :flags []

15 :inventory [`(:rope 1) `(:healing-potion 10) `(:gold 1000)]

16 })

I've made each item in the inventory be a tuple so that I can support stackable items. Flags are truthy values associated with the player. For instance if the player decides to choose chaotic-good as an alignment I would add that as a value to the flags vector.

To help implement locks in my game I've added in two new helper functions.

1(defn has-item?

2 "returns true if the player has the item in the inventory otherwise false"

3 [item player]

4 (true? (some #(= (first %) item) (player :inventory)))

5 )


7(defn has-flag?

8 "returns true if the player has the specified flag set otherwise false"

9 [flag player]

10 (true? (some #(= % flag) (player :flags)))

11 )

I've used the true? function because some will either return true or nil which I think is a gotcha.

linkRedefining the vertex

The verticies now need to store the direction, weight and the lock so I've needed to update the add-exit function

1(defn add-exit

2 "adds an exit from one room to another unidirectional"

3 ([zone source-room target-room direction cost] (add-exit zone source-room target-room direction cost []))

4 ([zone source-room target-room direction cost locks]

5 (-> zone ;first lets ensure that zone has both rooms

6 (add-room-if-absent target-room)

7 (add-room-if-absent source-room)

8 (update-in [source-room] #(conj % {:target target-room :direction direction :cost cost :locks locks})) ;append to source's adjacency vector

9 )

10 ))

We can now build the following zone with the following code


1 (def zone (-> {}

2 (add-exit :a :b :south 1)

3 (add-exit :b :a :north 1)

4 (add-exit :b :e :south-east 4)

5 (add-exit :a :c :east 2 [(partial has-item? :candle)])

6 (add-exit :c :d :south 1)

7 (add-exit :d :e :south 1)))


9 => {

10 :b [{:target :a, :direction :north, :cost 1, :locks []}

11 {:target :e, :direction :south-east, :cost 4, :locks []}],

12 :a [{:target :b, :direction :south, :cost 1, :locks []}

13 {:target :c, :direction :east, :cost 2, :locks [#someFnAddress]}],

14 :e [],

15 :c [{:target :d, :direction :south, :cost 1, :locks []}],

16 :d [{:target :e, :direction :south, :cost 1, :locks []}]

17 }

linkUpdating Dijkstra

Updating Dijkstra to take into account weights and locks was trivial we just had to make some minor adjustments to the calculation of costs and exits

1(defn exits

2 "Lists the exits of a room"

3 [zone player room]

4 (map :target (filter #(has-access? player (% :locks)) (room-edges zone room))))


6(defn build-graph

7 [zone player source-room]

8 (let [rooms (keys zone)]

9 (loop [results (assoc-in (zipmap rooms (repeat {:cost ##Inf :prev nil})) [source-room :cost] 0)

10 unvisited (set rooms)]

11 (if (empty? unvisited)

12 results

13 (let [current (first (sort-by (comp :cost second) (select-keys results unvisited)))

14 current-room (first current)

15 current-cost (get (second current) :cost)

16 neighbours (exits zone player current-room)

17 calculate-new-cost (fn [zone current-room target-room] (+ current-cost ((room-edges zone current-room target-room) :cost)))]

18 (recur

19 (reduce (fn [x x1]

20 (as-> x1 v

21 (results v)

22 {x1 (if (< (calculate-new-cost zone current-room x1) (v :cost))

23 (assoc v :cost (calculate-new-cost zone current-room x1) :prev current-room) v)}

24 (merge x v))

25 )

26 results neighbours)

27 (disj unvisited current-room))

28 )

29 )))

30 )

running this code gives us the following output (the player doesn't have a candle item)

1(build-graph zone player :a)


3{:b {:cost 1, :prev :a},

4 :a {:cost 0, :prev nil},

5 :e {:cost 5, :prev :b},

6 :c {:cost ##Inf, :prev nil},

7 :d {:cost ##Inf, :prev nil}}

linkNext Steps

This week I stumbled upon this article Building an E-Commerce Marketplace Middleware in Clojure so I will look at getting clojure to load my room data from a database next.

LocksRedefining the vertexUpdating DijkstraNext Steps

Home Rooms part 2 Rooms part 1 This blog's purpose