;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; teh partygame client ;; ;; hint: compile whole files with C-c M-k ;; (require 'sb-bsd-sockets) ;we want to use sbcl's builtin sockets ;; ;; some global variables ;; (defparameter *port* 1111) (defparameter *socket* nil) ; change this to your username. the default is to generate a random ; one at each compilation. (defparameter *username* (format nil "foo~a" (random 10000))) (defparameter *password* "bar") ; in this list we will keep track of the players we know. ; we just save their id, but you may want to change this ; you could define a class for the player and use a hashmap ; id -> player instead of this list, for example (defparameter *known-players* ()) (defparameter *my-player-id* nil) ; the place where we currently think the party is at. ; we just believe everything we are told. you will want to change this (defparameter *party-place* nil) (defparameter *remaining-actions* 0) (defparameter *command-lookup-table* ; this is a list of lists. the inner lists have two elements: a keyword that names the command and the code it corresponds to. ; we use it as a lookup table to translate commands into numerical codes to be sent to the server and vice versa. '((:err-place 1 ); invalid place parameter. (:err-player 2 ); invalid player parameter. (:err-need-login 3 ); login needed. (:err-night 4 ); cannot be used at night. (:err-no-action 5 ); no actions left. (:err-logged-in 6 ); logged in twice. (:err-game-running 7 ); cannot login. game is already running. (:err-login 8 ); invalid username/password. (:err-nan 9 ); numeric value required. (:err-unknown 10 ); unknown command. (:err-invalid 11 ); invalid parameters. (:err-unreachable 12 ); is unreachable. (:enter 20 ); <"name"> entered the game. (:left 21 ); <"name"> left the game. (:place-enter 22 ); enters (:place-leave 23 ); left (:party-info 24 ); <0|1> tells you about a party at (:player-info 25 ); <0|1> tells you about player (:player-list 30 ); rows of player-name follow. (:player-name 31 ); <"name"> follows player-list (:player-scores 32 ); rows of player-score follow. (:player-score 33 ); follows player-scores. scores for player. (:player-ids 34 ); rows of player-id follow (:player-id 35 ); <"name"> follows player-ids. mapping between id and "name". (:wait-more 40 ); waiting for at least players. (:starts-in 41 ); game starts in seconds. (:starting 42 ); game starts. (:my-player-id 43 ); your in this game. (:day-start 50 ); day started (:day-ending 51 ); day ends in seconds. (:day-ends 52 ); day ended (:night-start 53 ); nights starts (:night-ending 54 ); night ends in seconds. (:night-ends 55 ); night ended. (:game-ends 56 ); game ends. (:party-place 60 ); the party is at (:player-place 61 ); player is at (:score-none 62 ); no scores :-( (:score-lame 63 ); too few players to party :-( (:score-woohoo 64 ); scored! (:action-info 65 ); actions left. (:new-contact 66 ); you now know about (:lost-contact 67 ); you forgot about (:logged-in 70 ); login succesful (:welcome 71 ))); connected! ;; the following two functions translate code numbers to commands and vice versa, using the *command-lookup-table* ;; try (code->command 70) in the REPL if you are still confused (defun code->command (code) (first (find code *command-lookup-table* :test (lambda (test-code table-entry) (eql (second table-entry) test-code))))) (defun command->code (command) (second (find command *command-lookup-table* :test (lambda (test-command table-entry) (eql (first table-entry) test-command))))) ; we exchanged cellphone numbers with someone :) (defun add-contact (id) (setf *known-players* (cons id *known-players*))) ; we forgot about someone (defun remove-contact (id) (setf *known-players* (remove id *known-players*))) ; send a string and a newline to the server (defun send (message) (let ((message (format nil "~a~%" message))) (format t "<= ~a" message) (sb-bsd-sockets:socket-send *socket* message (length message)))) (defun tell-about-party (id location) (send (format nil "party ~a ~a" id location))) (defun tell-about-no-party (id location) (send (format nil "no_party ~a ~a" id location))) (defun goto-location (location) (send (format nil "goto ~a" location))) (defun goto-random-location () (goto-location (random 10))) ; we call this function every time (defun action () (format t "####action!~%") (if (and *party-place* (> (length *known-players*) 0)) (tell-about-party (nth (random (length *known-players*)) *known-players*) *party-place*) (goto-random-location))) ; takes a line of input from the server and acts accordingly. ; the input is a list of strings (defun handle-input (input) (when (or (< (length input) 2) (< (length input) (+ 2 (parse-integer (second input))))) (warn "malformed input from server") (return-from handle-input)) (let* ((command (code->command (parse-integer (first input)))) ;lookup the first word (num-args (parse-integer (second input))) ;the second word is number of args (args (subseq input 2 (+ 2 num-args)))) ;the rest (case command (:welcome (send (format nil "login ~a ~a" *username* *password*))) (:my-player-id (setf *my-player-id* (first args))) (:new-contact (add-contact (first args))) (:lost-contact (remove-contact (first args))) (:starting (setf *known-players* ())) (:party-place (setf *party-place* (first args))) (:action-info (when (> (setf *remaining-actions* (parse-integer (first args))) 0) (action))) (:day-start (setf *party-place* nil)) (:game-ends (setf *known-players* ()))))) ; starts the client. ; we open a socket and parse the input. we then call handle-input ; each time we receive a full line. (defun start (&optional (ip-address #(127 0 0 1))) (setf *socket* (sb-bsd-sockets:make-inet-socket :stream :tcp)) ;create socket (let ((stream (sb-bsd-sockets:socket-make-stream *socket*)) ;stream is the socket's output (current-word "" ) ;(re)set everything (current-line "" ) (current-tokens () ) (comment-mode nil)) (sb-bsd-sockets:socket-connect *socket* ip-address *port*) ;connect (loop do (let* ((char (read-char stream)) ; read single chars from the stream (char-as-string (string char))) (setf current-line (concatenate 'string current-line char-as-string)) ;save the current line as it was sent by te server (case char (#\Space (when (not (string= current-word "")) ;start a new word with space if the word isn't empty (setf current-tokens (append current-tokens (list current-word)) current-word ""))) (#\Newline (progn ;reset everything on newline, process input (format t "-> ~a" current-line) (handle-input (append current-tokens (list current-word))) (setf current-tokens () current-word "" current-line "" comment-mode nil))) (#\: (setf comment-mode t)) ; colon starts a comment (otherwise (unless comment-mode ;append everything else to the current word (setf current-word (concatenate 'string current-word char-as-string)))))))))