We have only one more thing to do now and our game will be complete: Add some special actions to the game that the player has to do to win in the game. The first command will let the player weld the chain to the bucket in the attic:
(def chain-welded false) (defn weld [subject object] (cond (and (= location 'attic) (= subject 'chain) (= object 'bucket) (have? 'chain) (have? 'bucket) (not chain-welded)) (do (def chain-welded true) '(the chain is now securely welded to the bucket -)) :else '(you cannot weld like that -)))
So first we created a new global variable that lets us tell whether we've done this action already. Next, we create a weld function that makes sure all the right conditions are in place for welding and lets us weld.
Let's try our new command:
(weld 'chain 'bucket)
user=> (weld 'chain 'bucket) (you cannot weld like that -)
Oops... we're don't have a bucket or chain, do we? ...and there's no welding machine around... oh well...
Now let's create a command for dunking the chain and bucket in the well:
(def bucket-filled false) (defn dunk [subject object] (cond (and (= location 'garden) (= subject 'bucket) (= object 'well) (have? 'bucket) chain-welded) (do (def bucket-filled true) '(the bucket is now full of water)) :else '(you cannot dunk like that -)))
Now if you paid attention, you probably noticed that this
command looks a lot like the
command... Both commands need to check the location,
subject, and object - But there's enough making them
different enough so that we can't combine the
similarities into a single function. Too bad...
...but since this is Lisp, we can do more than just write functions, we can cast SPELs! Let's create the following SPEL:
(defspel game-action [command subj obj place & args] `(defspel ~command [subject# object#] `(spel-print (cond (and (= location '~'~place) (= '~subject# '~'~subj) (= '~object# '~'~obj) (have? '~'~subj)) ~@'~args :else '(i cannot ~'~command like that -)))))
Notice how ridiculously complex this SPEL is - It has more weird quotes, backquotes, commas and other weird symbols than you can shake a list at. More than that it is a SPEL that actually cast ANOTHER SPEL! Even experienced Lisp programmers would have to put some thought into create a monstrosity like this (and in fact they would consider this SPEL to be inelegant and would go through some extra esoteric steps to make it better-behaved that we won't worry about here...)
The point of this SPEL is to show you just how sophisticated and clever you can get with these SPELs. Also, the ugliness doesn't really matter much if we only have to write it once and then can use it to make hundreds of commands for a bigger adventure game.
Let's use our new SPEL to replace our ugly
(game-action weld chain bucket attic (cond (and (have? 'bucket) (def chain-welded true)) '(the chain is now securely welded to the bucket -) :else '(you do not have a bucket -)))
Look at how much easier it is to understand this
command - The
game-action SPEL lets us write exactly
what we want to say without a lot of fat - It's almost
like we've created our own computer language just for
creating game commands. Creating your own pseudo-language
with SPELs is called
Domain Specific Language Programming, a very powerful
way to program very quickly and elegantly.
(weld chain bucket)
user=> (weld chain bucket) (you do not have a chain -)
...we still aren't in the right situation to do any welding, but the command is doing its job!
Next, let's rewrite the
dunk command as well:
(game-action dunk bucket well garden (cond chain-welded (do (def bucket-filled true) '(the bucket is now full of water)) :else '(the water level is too low to reach -)))
Notice how the
weld command had to
check whether we
have? the subject,
but that the dunk command skips that step - Our
new game-action SPEL makes the code easy to write
And now our last code for splashing water on the wizard:
(game-action splash bucket wizard living-room (cond (not bucket-filled) '(the bucket has nothing in it -) (have? 'frog) '(the wizard awakens and sees that you stole his frog - he is so upset he banishes you to the netherworlds - you lose! the end -) :else '(the wizard awakens from his slumber and greets you warmly - he hands you the magic low-carb donut - you win! the end -)))
You have now written a complete text adventure game!
Click HERE for a complete walkthrough of the game,
Click HERE for a copy of the source code you can copy & paste into your Clojure prompt in a single step.
In order to make this tutorial as simple as possible, many details about how Lisp works have been glossed or fudged over, so let's look at what those details are...