Organizing Things with Records
Making a Plan
As we've said, our game is going to need the following:
- objects
- places
- place data (description, exits)
- object locations
- player location
We need to create a data structure to hold all of that, so that it can be passed to functions which need one or more bits of that data. As we mentioned when we talked about Non-Global State, The data structure we're going to use is the LFE record. A record is a simple data structure that lets us associate keys and values.
Let's attack this problem in pieces; we can start with the big picture, and then fill that in.
Game State
Let's create the over-arching record definition for our game state:
(defrecord state
objects
places
player-location
goals)
We've just defined a record called state
that has four fields: objects
, places
, player-location
, and goals
.
Objects
For each object in the game, we need to know its name and location:
(defrecord object
name
location)
Let's create some objects now, improving upon our initial "objects" exploration:
> (set objects
(list (make-object name 'whiskey-bottle location 'living-room)
(make-object name 'bucket location 'living-room)
(make-object name 'frog location 'garden)
(make-object name 'chain location 'garden)))
(#(object whiskey-bottle living-room)
#(object bucket living-room)
#(object frog garden)
#(object chain garden))
You are probably wondering where that mysterious make-object
function came from. When you create a record in LFE, LFE creates several functions dynamically, just for use with your record: their names start with or have as part of their own names, the record name you used. For example, when you created the state
and object
records, LFE created the make-state
and make-object
functions (among several others -- more later).
Places and Exits
Now that we've defined some objects in our world, we're on our way towards describing our world. But there's more to go, still. Our next goal is to create a record for our places:
(defrecord place
name
description
exits)
Great! Now we can define our places ... almost. What's the "exit" business? Well, if we're going to move about from place to place, we need to know what direction to go in, the object that lets us pass from one location to the next, and the final destination. Let's create another record for this data:
(defrecord exit
direction
object
destination)
Now we're ready to create our places!
> (set living-room
(make-place
name 'living-room
description (++ "You are in the living-room of a wizard's house. "
"There is a wizard snoring loudly on the couch.")
exits (list
(make-exit
direction "west"
object "door"
destination 'garden)
(make-exit
direction "upstairs"
object "stairway"
destination 'attic))))
#(place living-room
"You are in the living-room of a wizard's house. There is a wizard snoring loudly on the couch."
(#(exit "west" "door" garden) #(exit "upstairs" "stairway" attic)))
As you can see above, we have records being created inside records: the living-room
record has two exits in it, and we just created those exit
records when created the living room's place
record.
Something else new: the ++
function. This is the function for combining two lists in LFE, and since strings and lists are actually the same exact data type, it's also what you use to concatenate strings.
Three more to go!
> (set garden
(make-place
name 'garden
description (++ "You are in a beautiful garden. "
"There is a well in front of you.")
exits (list
(make-exit
direction "east"
object "door"
destination 'living-room))))
#(place garden
"You are in a beautiful garden. There is a well in front of you."
(#(exit "east" "door" living-room)))
> (set attic
(make-place
name 'attic
description (++ "You are in the attic of the wizard's house. "
"There is a giant welding torch in the corner.")
exits (list
(make-exit
direction "downstairs"
object "stairway"
destination 'living-room))))
#(place attic
"You are in the attic of the wizard's house. There is a giant welding torch in the corner."
(#(exit "downstairs" "stairway" living-room)))
> (set netherworld
(make-place
name 'netherworld
description (++ "Everything is misty and vague. "
"You seem to be in the netherworld.\n"
"There are no exits.\n"
"You could be here for a long, long time ...")
exits '()))
This may seem like a lot of overhead, but it means that things will be much
cleaner and less susceptible to bugs: each item of data is well-defined, with functions that create the data, access the data, and update the data -- both the "magical" record functions mentioned above as well as functions defined in the Erlang standard library (e.g., the proplists
and orddict
modules).
Furthermore, this is a common practice used in many real-world Erlang and LFE applications: records are passed as inputs to functions and returned as (often updated) outputs, which in turn are fed into other functions.
Goals
We're going to have a couple of puzzles in our game and a final task to accomplish, once these puzzles are solved. Let's define the goal record:
(defrecord goal
name
achieved?)
Now the goals:
> (set goals
(list (make-goal name 'weld-chain achieved? 'false)
(make-goal name 'dunk-bucket achieved? 'false)
(make-goal name 'splash-wizard achieved? 'false)))
(#(goal weld-chain false)
#(goal dunk-bucket false)
#(goal splash-wizard false))
Now that we have our records, let's put them together!