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
 |