Adventures in Clojure: The Fact Base

Adventures in Clojure: The Fact Base August 9, 2014

clojure When you’re writing a game based around a game world of some kind, be it a text adventure like Zork, an action adventure like The Legend of Zelda, or any number of other games, there’s stuff your game needs to remember. A lot of it has to do with the player: where is he, how much health does he have, does he have the Iron Mitts of Cooking that let him invade and conquer the Iron Chef Temple of Cuisine?

But on top that, there are a lot of just plain facts to remember. Where has he been? What areas have been unlocked? What puzzles have been solved, what are still vexing him, and which ones have yet to be discovered?

You’d like to keep all of this information in one place, so as to make it easy to save it with the rest of the game data; and you’d like it to be easy to update and query.

My solution was to define the notion of a fact. A fact is a vector of values, usually keywords, that together indicate something true about the game world. Here’s my initial set of facts:

(def facts 
  (atom #{
   [:sewer :clogged]
   [:tv :broken]
}))

The :tv is an object, the player’s television. It’s broken at the start of the game; the player needs to fix it. And there’s another problem; down the street, the sewer’s clogged. It’s not yet clear whether the keyword :sewer refers to any explicit entity; our player may never lay eyes on the sewer, but he might unleash a flood that breaks through the clog (thus removing this fact from the fact base) and thereby making that annoying smell go away.

I’ve used an atom to contain the facts; another possibility would be to represent the full set of game data in a single map value that gets updated with each move the player makes, as several commenters suggested on my last post. They may well be right; but since my programming time has all been devoted to Quill in recent weeks, I’m presenting the game code as it is rather than as it might be.

The facts atom contains a set of facts; a set is a data structure that mimics a mathematical set, and can contain at most one of any given value. You can do all of the usual set operations on them, but the most basic operations are to put a value into the set, and to remove it from the set. For this, Clojure defines the conj (conjoin) function, and the disj (disjoin) function. Each takes a set and a value, and returns a set that either contains or does not contain the value.

Since the set is in an atom, we use swap! in conjunction with conj and disj to add and remove facts:

(defn add-fact! [f]
  (swap! facts conj f))

(defn remove-fact! [f]
  (swap! facts disj f))

The exclamation point indicates that these functions mutate global state.

Next, we need a query: is a putative fact actually a fact? Is the :sewer still :clogged? A value f is a fact if @facts contains? it; otherwise it is not a fact. Thus, I could define my query fact? like this:

(defn fact? [f] (contains? @facts f))

But I found it useful to extend the notion of a fact a little. If the first element of the fact vector is :not, then I want fact? to return true if the remaining elements of the vector are not in the fact base. In other words:

=> (fact? [:sewer :clogged])
true
=> (fact? [:not :sewer :clogged])
false
=> (fact? [:fred :dead])
false
=> (fact? [:not :fred :dead])
true

In this way, both facts and the negations of facts have exactly the same form, a simple vector. The following version of fact? gives the desired behavior:

(defn fact? [f]
  (if (not= (first f) :not)
    (contains? @facts f)
    (not (contains? @facts (rest f)))))

For extra credit, I could make it recursive; and I probably should.

Finally, for convenience I’ve got if-fact, where (if-fact f x y) is equivalent to (if (fact? f) x y). It looks like this:

(defn if-fact
  ([f x]   (if-fact f x nil))
  ([f x y] (if (fact? f) x y)))

Browse Our Archives