ASM development log 0: The Game
Posted on by Idorobots
It's high time for more Another Syntactic Monstrosity examples, but first let's take a look at its development.
devLog
I spent the last few days on redesigning the parsing strategy, ATM the interpreter reads the raw input, lexes it using static, immutable grammar specs and then parses the token stream into an AST, all this hardcoded in D.This proves to be fast, but it's nearly non-tweakable. That's not what I intended ASM to be and it makes some neat features, such as overriding Tuple evaluator, impossible.The new parsing strategy reminds of CommonLisp reader and reader-macros:
- The raw input is passed to the lexer (written in D) that uses dynamic syntax table - a set of regular expressions describing the syntax (defined in ASM) to tokenize the input into a token stream.
- Next, the token stream is passed to the parser (written in ASM with default implementation in D), that uses ASM functions, correlated to the syntax table, to dispatch the token stream and translate it to basic, lispy S-expressions (see LR parser for details on how it's done).This will allow ASM to act like any other language, given there's the syntax table available, making it perfect for DSL programming.
For example:
(lambda (x y) (pow (+ x y) 2))
...might be written as:
[x y => (x + y)^2]
...and ASM will happily accept it. Awesome! There was little activity at the google.code Github page, because the language specs is changing constantly, and most of the tests are outside of the project. Furthermore, I'm keeping myself from coding much in ASM - simply there will be less code to break with newer specs versions. Once I'm happy with the specs I'll most certainly start a bigger project using ASM.
The Game
It's an implementation of a simple 'game' from the Land of Lisp book in ASM, you can get it from the project page.(The snippets use JavaScript syntax highlighting, so they're not too accurate.)## Places to go.
(var *nodes* '{(living-room (You are in the living-room. A wizard is snoring loudly on the couch.))
(garden (You are in a beautiful garden. There is a well in front of you.))
(attic (You are in the attic. There is a giant wielding a torch in the corner.))
# And so on...
})
## Paths to take.
(var *edges* '{(living-room (garden west door)
(attic upstairs ladder))
(garden (living-room east door))
(attic (living-room downstairs ladder))
# And so on...
})
## Items to steal.
(var *objects* '{whiskey bucket frog chain #...})
## Where to steal them.
(var *object-locations* '[(whiskey living-room)
(bucket living-room)
(frog garden)
(chain garden)
# Etc...
])
## Where we at, yo.
(var *location* 'living-room)
Here we have all the required data. I like the lispy naming convention, so my globals too will have ear-muffs (e.g. *neat*
). *nodes*
set contains the descriptions of all the different places we can visit, *edges*
set tells us where we can go. *objects*
set contains the list of all the items in the level, and *object-locations*
stores their locations. *objects-locations*
is a list, an alist to be exact, so we can push!
and pop!
from it in place and use assoc
to get the values. Lastly, the *location*
variable stores our current location.
(function describe-location [location nodes]
(second (assoc location nodes)))
(function describe-path [edge]
`(There is a $(third edge) going $(second edge) from here.))
(function describe-paths [location edges]
(apply append (map describe-path (rest (assoc location edges)))))
(function objects-at [loc objs obj-locs]
(objs [[obj] (equal? (second (assoc obj obj-locs))
loc)]))
(function describe-objects [loc objs obj-locs]
(apply append (map [[obj] `(You see a $obj on the floor.)]
(objects-at loc objs obj-locs))))
These functions describe various elements of the game world. describe-path
uses quasiquoting and embeds expressions in a tuple ($expression)
. describe-paths
describes all the edges
connected to a location
- it maps through the set of edges with describe-path
collecting the results. objects-at
function uses an anonymous function created with list evaluator ([[args]body])
to select some of the objects from objs
set. The "?" in equal?
is another naming convention. It's used for predicates and I stole it from Scheme.
Finally, describe-objects
describes all the stuff we can see on the floor in our location. These are all pure functional style, since they only use their parameters and no impure functions.
(function look []
(append (describe-location *location* *nodes*)
(describe-paths *location* *edges*)
(describe-objects *location* *objects* *object-locations*)))
(function walk [direction] {
(var next (first (select (rest (assoc *location* *edges*))
[[edge] (equal? (second edge) direction)])))
(if next {
(set! *location* (first next))
(look)
}
'(You can't go there.))
})
The look
function simply appends all the describe functions results, so we can apply write to them later. walk
looks kind of bad. That's because there's no find
function implemented in ASM, and I had to improvise with select
. It uses set evaluator for the body of the if, and expression comment to make it clear that '(You can't go there.)
is the else part. Note that walk
is not pure. It changes the state of *location*
.
(function pickup [object]
(if (member? object
(objects-at *location* *objects* *object-locations*))
(do (push! (tuple object 'inventory) *object-locations*)
`(You are now carrying the $object))
'(You can't get that.)))
(function inventory []
(join! 'inventory:
(objects-at 'inventory *objects* *object-locations*)))
These two functions manage our inventory and items interaction. Not much to see to be honest.
## Game REPL:
(function game-read []
(if (collection? (var command (read)))
(tuple (first command) `(quote $(second command)))
(tuple command)))
(function game-print [what] {
(map [[arg] (write arg \space)] what)
(write \newline)
})
(function game-repl [] {
(var accepted-commands '{look walk pickup inventory quit})
(if (equal? (first (var command (game-read)))
'quit)
(game-print '(Bye, bye.))
{(if (member? (first command) accepted-commands)
(game-print (eval command))
(game-print '(I don't know this command.)))
(game-repl)
})
})
(function new-game [] {
(set! *location* 'living-room)
(game-print (look))
(game-repl)
})
These few functions manage our player-game interaction. The game-read
function reads the input and makes sure it's well-formed syntactically. The game-print
function does the printing - basically maps the write
function to whatever is passed to it. Now, the game-repl
function reads the input from game-read
, validates it semantically and if it's fine, evaluates it and prints the result. Lastly there's the new-game
function, resetting the *location*
and loading the game.
Here's a little test-play:
$ ./asm
> (import 'examples.game)
fnord
> (new-game)
You are in the living-room. A wizard is snoring loudly on the couch. There is a
door going west from here. There is a ladder going upstairs from here. You see a
whiskey on the floor. You see a bucket on the floor.
walk upstairs
You are in the attic. There is a giant wielding a torch in the corner. There is a
ladder going downstairs from here.
jump window
I don't know this command.
walk downstairs
You are in the living-room. A wizard is snoring loudly on the couch. There is a
door going west from here. There is a ladder going upstairs from here. You see a
whiskey on the floor. You see a bucket on the floor.
pickup whiskey
You are now carrying the whiskey
inventory
inventory: whiskey
walk west
You are in a beautiful garden. There is a well in front of you. There is a door
going east from here. You see a frog on the floor. You see a chain on the floor.
walk china
You can't go there.
quit
Bye, bye.
>
2016-02-18: Adjusted some links & tags.