234 lines
8.1 KiB
Common Lisp
234 lines
8.1 KiB
Common Lisp
(defparameter *nodes* '((living-room (you are in a 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 welding torch in the corner.))))
|
|
|
|
(defun describe-location (location nodes)
|
|
(cadr (assoc location nodes)))
|
|
|
|
(defparameter *edges* '((living-room (garden west door)
|
|
(attic upstairs ladder))
|
|
(garden (living-room east door))
|
|
(attic (living-room downstairs ladder))))
|
|
|
|
(defun describe-path (edge)
|
|
`(there is a ,(caddr edge) going ,(cadr edge) from here.))
|
|
|
|
(defun describe-paths (location edges)
|
|
(apply #'append (mapcar #'describe-path (cdr (assoc location edges)))))
|
|
|
|
(defparameter *objects* '(whiskey bucket frog chain))
|
|
|
|
(defparameter *object-locations* '((whiskey living-room)
|
|
(bucket living-room)
|
|
(chain garden)
|
|
(frog garden)))
|
|
|
|
(defun objects-at (loc objs obj-locs)
|
|
(labels ((at-loc-p (obj)
|
|
(eq (cadr (assoc obj obj-locs)) loc)))
|
|
(remove-if-not #'at-loc-p objs)))
|
|
|
|
(defun describe-objects (loc objs obj-loc)
|
|
(labels ((describe-obj (obj)
|
|
`(you see a ,obj on the floor.)))
|
|
(apply #'append (mapcar #'describe-obj (objects-at loc objs obj-loc)))))
|
|
|
|
(defparameter *location* 'living-room)
|
|
|
|
(defun look ()
|
|
(append (describe-location *location* *nodes*)
|
|
(describe-paths *location* *edges*)
|
|
(describe-objects *location* *objects* *object-locations*)))
|
|
|
|
(defun walk (direction)
|
|
(let ((next (find direction
|
|
(cdr (assoc *location* *edges*))
|
|
:key #'cadr)))
|
|
(if next
|
|
(progn (setf *location* (car next))
|
|
(look))
|
|
'(you cannot go that way.))))
|
|
|
|
(defun pickup (object)
|
|
(cond ((member object
|
|
(objects-at *location* *objects* *object-locations*))
|
|
(push (list object 'body) *object-locations*)
|
|
`(you are now carring the ,object))
|
|
(t '(you cannot get that.))))
|
|
|
|
(defun inventory ()
|
|
(cons 'items- (objects-at 'body *objects* *object-locations*)))
|
|
|
|
;; Let's now define a repl
|
|
|
|
;;(defun game-repl ()
|
|
;; (loop (print (eval (read)))))
|
|
|
|
;; Well, that's the definition of a repl isn't it?
|
|
;; It reads, then evals, then prints, and loops... This is easy.
|
|
|
|
;; Time to redefine the repl to work proper for us. Also, why loop?
|
|
|
|
(defun game-repl ()
|
|
(let ((cmd (game-read)))
|
|
(unless (eq (car cmd) 'quit)
|
|
(game-print (game-eval cmd))
|
|
(game-repl))))
|
|
|
|
(defun game-read ()
|
|
(let ((cmd (read-from-string
|
|
(concatenate 'string "(" (read-line) ")"))))
|
|
(flet ((quote-it (x)
|
|
(list 'quote x)))
|
|
(cons (car cmd) (mapcar #'quote-it (cdr cmd))))))
|
|
|
|
;; (read-from-string) works like (read) but lets us read a syntax expression
|
|
;; or any other basic Lisp data type from a string instead
|
|
;; of directly from the console
|
|
|
|
;; The string we use for this is a tweaked version of a string
|
|
;; we get from read-line
|
|
|
|
;; We tweak it by adding quotes around it using (concatenate)
|
|
;; which can be used for cating strings together as well as
|
|
;; some parens
|
|
|
|
;; Then we define a local function called (quote-it) which we can use to
|
|
;; (quote) any arguments the player has in a command
|
|
;; (remember that the single quote is shorthand for (quote )
|
|
|
|
;; Let's test
|
|
|
|
;; (game-read)
|
|
;; walk east
|
|
;; >> (WALK 'EAST)
|
|
|
|
;; Looks like it adds the parens and quotes it
|
|
|
|
;; Note that our read loop isn't perfect. If a player threw
|
|
;; in an extra paren, it would go to hell
|
|
|
|
(defparameter *allowed-commands* '(look walk pickup inventory))
|
|
|
|
(defun game-eval (sexp)
|
|
(if (member (car sexp) *allowed-commands*)
|
|
(eval sexp)
|
|
'(i do not know that command.)))
|
|
|
|
;; The first line checks to see if the command the user gives is in our
|
|
;; list of allowed commands.
|
|
|
|
;; If we didn't do this, it would be easy to call Lisp commands
|
|
;; from our custom repl from beyond the scope of this game
|
|
|
|
;; Our eval command doesn't protect against hacking 100%, but it's a start
|
|
|
|
;; Our game-print function will convert our symbol-based writing
|
|
;; into properly capitalized text
|
|
;; by having this function available, we can store
|
|
;; the text in our game engine in the most comfortable format possible
|
|
;; lists of symbols
|
|
|
|
;; this makes it easier to manipulate text
|
|
|
|
(defun tweak-text (lst caps lit)
|
|
(when lst
|
|
(let ((item (car lst))
|
|
(rest (cdr lst)))
|
|
(cond ((eql item #\space) (cons item (tweak-text rest caps lit)))
|
|
((member item '(#\! #\? #\.)) (cons item (tweak-text rest t lit)))
|
|
((eql item #\") (tweak-text rest caps (not lit)))
|
|
(lit (cons item (tweak-text rest nil lit)))
|
|
(caps (cons (char-upcase item) (tweak-text rest nil lit)))
|
|
(t (cons (char-downcase item) (tweak-text rest nil nil)))))))
|
|
|
|
(defun game-print (lst)
|
|
(princ (coerce (tweak-text (coerce (string-trim "() "
|
|
(prin1-to-string lst))
|
|
'list)
|
|
t
|
|
nil)
|
|
'string))
|
|
(fresh-line))
|
|
|
|
;; So this is a bit more complex
|
|
|
|
;; The first important part of code xecuted is in game-pring
|
|
;; it converts the symbol list (containing the text whose layout we want to fix
|
|
;; into a string with prin1-to-string
|
|
|
|
;; The "to-string" part means this function doesn't show the result on the screen
|
|
;; but just returns it as a string
|
|
;; The "1" means it will stay on a single line
|
|
|
|
;; Next game-print converts the string to a list of characters with (coerce)
|
|
;; By coercing our string into a list, we can reduce the bigger goal
|
|
;; of the function into a list-processing problem
|
|
|
|
;; In this case we're creaing a list of chars making up the text we want to fix
|
|
|
|
;; No we send the data to the list-eater function (tweak-text)
|
|
;; Some of the arguments used in (game-print) are printed on their own line
|
|
;; for clarity
|
|
|
|
;; to make it easier to see which argments are meant for which commands by
|
|
;; looking at indentation
|
|
|
|
;; for instance, the "t" and "nil" arguments belong to tweak-text
|
|
|
|
;; (tweak-text) looks at each character in the list and modifies it as needed
|
|
;; At the top of this function we define item and rest
|
|
;; which we get by chewing off an item from the front of the sentence
|
|
;; we're tweaking
|
|
|
|
;; Then the tweak-text function uses a cond to check the character at the
|
|
;; top of the list for different conditions
|
|
|
|
;; The first checks whether the character is a space character
|
|
;; if so, leave it alone and go to the next char
|
|
;; if the char is a period, question mark, or exclamation point
|
|
;; we turn on the (cap) parameter for the rest of the string
|
|
;; (by using the value "t" as an argument in the recursive call) to
|
|
;; indicate that the next symbol is at the beginning of a sentence
|
|
;; and needs to be a capital letter
|
|
|
|
;; We also track whether we've encountered a quotation mark
|
|
;; we do this because, infrequently, a symbol list is not
|
|
;; adequate for encoding English text
|
|
|
|
;; Examples include having a comma as commas are not allowed
|
|
;; in standard common lisp symbols
|
|
|
|
;; or product names with non-standard capitalization
|
|
|
|
;; In these cases we can fall back on using text strings
|
|
|
|
;; Example:
|
|
;; (game-print '(not only does this sentence have a "comma," it also mentions the "iPad."))
|
|
;; >> Not only does this sentence have a comma, it also mentions the iPad.
|
|
|
|
;; We tell the function to treat the capitlization as shown literally by
|
|
;; turning off the lit variable in the recursive call
|
|
;; As long as this value is set, (tweak-text) prevents
|
|
;; the capitalization rules from being reached
|
|
|
|
;; Next (tweak-text) checks to see whether the next char is supposed to be
|
|
;; capitalized. If it is, we use the char-upcase function to change the
|
|
;; current character to uppercase (if it isn't already) before processing
|
|
;; the next item in the list
|
|
|
|
;; If none of the other conditons were met, we know that the current char
|
|
;; should be lowercase and we can convert it using the char-downcase function
|
|
|
|
;; After tweak-text is finished correcting the text in the list,
|
|
;; (game-print) coerces it back into a proper string and princs it
|
|
|
|
;; (fresh-line) makes sure the next item appearing on the screen will start
|
|
;; on a fresh line
|
|
|
|
;; Note: (read) and (eval) should be mostly avoided for production code
|
|
;; Someone will be able to think up a way to break into it
|