#lang racket ;; This module implements the client for the Hungry Henry game (provide lets-eat ;; String String[IP Address] -> Meal ;; launch single client and register at specified host ) (require "shared.rkt" 2htdp/universe 2htdp/image) ; ; ; ; ; ; ; ; ; ; ; ; ;;; ; ;; ; ;;; ; ; ; ; ; ; ; ;; ; ;; ; ; ; ; ;;;;; ; ; ; ; ; ; ; ; ; ; ;;;;; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ;;;; ; ; ; ; ; ; ; ;; ; ;; Image Constants (define FOOD-IMG (bitmap "graphics/cupcake.gif")) (define PLAYER-IMG (bitmap "graphics/hungry-henry.gif")) (define BASE (empty-scene WIDTH HEIGHT)) (define WAYPOINT-NODE (circle 3 'solid 'black)) ;; Color Constants (define PLAYER-COLOR "red") (define MY-COLOR "blue") (define WAYPOINT-COLOR "green") ;; Text Constants (define LOADING... "Waiting For Server") (define TEXT-SIZE 20) (define SCORE-SIZE 20) (define TEXT-COLOR "black") (define END-OPEN-TEXT "your score was: ") (define END-CLOSE-TEXT ", the winner was player ") (define LOADING-OPEN-TEXT "\nYou are ") (define SEPERATOR ": ") ;; PBAR constants (define PBAR-HEIGHT 35) (define PBAR-LOC (- HEIGHT PBAR-HEIGHT)) (define PBAR-COLOR "red") (define PBAR-TEXT (text "loading..." 20 "black")) ;; Message ID Constants (define UPDATE-LENGTH 3) (define SPLAYER-LENGTH 3) (define SBODY-LENGTH 2) (define END-LENGTH 2) (define SCORE-LIST-LENGTH 2) ;; Init Constants (define ZERO% 0) (define LOADING (text LOADING... 20 "black")) ;; ----------------------------------------------------------------------------- ;; State of Client (struct app (id img countdown) #:transparent) (struct entree (id players food) #:transparent) ;; Meal is one of ;; - Appetizer ;; - Entree ;; Appetizer = (app [or Id #f] Image Number∈[0,1]) ;; interpretation: ;; -- the first field is this players id, #f if it hasnt been sent yet ;; -- the second is the loading image ;; -- the third is the %%% of loading time passed, represents the loading state ;; Entree = (entree Id [Listof Feaster] [Listof Food]) ;; interpretation: ;; -- the first field is this player's id ;; -- the second field represents complete information about all players ;; -- the third field specifies the location of the cupcakes (define INITIAL (app #f LOADING ZERO%)) ; ; ; ; ; ; ; ; ;;; ;;; ; ;; ;; ; ; ; ; ; ;;;; ;;; ;; ;;; ; ; ; ; ; ; ; ; ;; ; ; ; ; ; ; ; ; ; ; ; ; ;; ; ;;;;;; ; ; ; ; ; ;; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ;; ; ; ; ; ;;; ;;; ;;;; ;; ;;;;;;; ;;; ;;; ; ; ; ; ; (define (lets-eat label server) (big-bang INITIAL (to-draw render-the-meal) (on-mouse set-waypoint) (on-receive handle-server-messages) (register server) (name label))) ;; Meal Message -> Meal ;; handles incomming messages (define (handle-server-messages meal msg) (cond [(app? meal) (handle-appetizer-message meal msg)] [(entree? meal) (handle-entree-message meal msg)])) ;; Meal Number Number MouseEvent -> Meal ;; handles what happends on a click (define (set-waypoint meal x y event) (if (and (entree? meal) (string=? event "button-down")) (make-package meal (list GOTO x y)) meal)) ;; Meal -> Image ;; deals with draw some kind of meal (define (render-the-meal meal) (cond [(app? meal) (render-appetizer meal)] [(entree? meal) (render-entree meal)])) ; ; ; ; ;;;; ; ; ; ; ; ; ; ;;; ;;;; ;;; ;;; ; ; ;;; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ;;;; ; ; ; ; ; ; ; ; ; ; ; ; ; ;;;;; ; ;;;;; ; ;; ;; ;;;;; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ;;;; ;;;; ;;;; ; ; ;;;; ; ; ; ;; ----------------------------------------------------------------------------- ;; Appetizer ;; Appetizer Message -> Meal ;; starts the game if the message is valid (define (handle-appetizer-message s msg) (cond [(id? msg) (app msg (app-img s) (app-countdown s))] [(time? msg) (app (app-id s) (app-img s) msg)] [(state? msg) (switch-to-entree s msg)] ;; fault tolerant [else s])) ;; Appetizer State -> Meal (define (switch-to-entree s m) (apply entree (app-id s) (rest m))) ;; ----------------------------------------------------------------------------- ;; Appetizer ;; Entree Message -> Meal ;; either updates the world or ends the game (define (handle-entree-message s msg) (cond [(state? msg) (update-entree s msg)] [(score? msg) (restart s msg)] [else s])) ;; Entree State -> Entree ;; creates a new entree based on the update mesg (define (update-entree s state-msg) (apply entree (entree-id s) (rest state-msg))) ;; Entree EndMessage -> Appetizer ;; Tranistion to start state (define (restart s end-msg) (define score-image (render-scores end-msg)) (app (entree-id s) (above LOADING score-image) ZERO%)) ;; ----------------------------------------------------------------------------- ;; predicates for recognizing network messages ;; Message -> Boolean ;; checks if message is a valid update message (define (state? msg) (and (list? msg) (= UPDATE-LENGTH (length msg)) (symbol? (first msg)) (list? (second msg)) (list? (third msg)) (symbol=? SERIALIZE (first msg)) (andmap player? (second msg)) (andmap body? (third msg)))) ;; Message -> Boolean ;; checks if message is a valid time message (define (time? msg) (and (real? msg) (<= 0 msg 1))) ;; Message -> Boolean ;; checks if is end game message (define (score? msg) (and (list? msg) (= END-LENGTH (length msg)) (symbol? (first msg)) (list? (second msg)) (symbol=? SCORE (first msg)) (score-list? (second msg)))) ;; List -> Boolean ;; is this a list binding names to scores? (define (score-list? l) (for/and ([s l]) (and (list? s) (= SCORE-LIST-LENGTH (length s)) (id? (first s)) (number? (second s))))) ; ; ; ; ; ; ; ; ;;;;;; ; ; ; ; ; ; ;; ;; ;;;; ;;; ;; ;;; ;; ;;; ;;; ;; ; ; ; ;;; ; ; ; ; ; ; ;; ; ; ;; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ;;;;;; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ;; ; ; ; ; ; ; ; ; ;; ; ;;;;;; ;;;;;; ;;;; ;; ; ; ;;;;;;; ;;; ;;; ;;; ; ; ; ; ;; ; ;;;; ; ; ;; ----------------------------------------------------------------------------- ;; Appetizer Drawing ;; Appetizer -> Image ;; tells the player that we're waiting for the server. shows id (define (render-appetizer app) (add-progress-bar (render-id+image app) (app-countdown app))) ;; Image Number∈[0,1] -> Image ;; draws the progress bar (define (add-progress-bar base count) (place-image (render-progress count) (/ WIDTH 2) PBAR-LOC base)) ;; Number∈[0,1] -> Image ;; draw a progress bar that is count percent complete (define (render-progress count) (overlay PBAR-TEXT (rectangle (* count WIDTH) PBAR-HEIGHT "solid" PBAR-COLOR))) ;; Appetizer -> Image ;; gets the text to display on the loading screen (define (render-id+image app) (define id (app-id app)) (define base-image (app-img app)) (overlay (cond [(boolean? id) base-image] [else (define s (string-append LOADING-OPEN-TEXT id)) (above base-image (text s TEXT-SIZE TEXT-COLOR))]) BASE)) ;; ----------------------------------------------------------------------------- ;; Entree Drawing ;; Entree -> Image ;; draws a Entree (define (render-entree entree) (define id (entree-id entree)) (define pl (entree-players entree)) (define fd (entree-food entree)) (add-path id pl (add-players id pl (add-food fd BASE)))) ;; [Listof Food] Image -> Image ;; draws all the food (define (add-food foods base-scene) (for/fold ([scn base-scene]) ([f foods]) (place-image FOOD-IMG (body-x f) (body-y f) scn))) ;; Id [Listof Feaster] Image -> Image ;; draws all players (define (add-players id lof base-scene) (for/fold ([scn base-scene]) ([feaster lof]) (place-image (render-avatar id feaster) (feaster-x feaster) (feaster-y feaster) scn))) ;; Id Feaster -> Image ;; gets an image for the player (define (render-avatar id player) (define size (body-size (player-body player))) (define color (if (id=? id (player-id player)) MY-COLOR PLAYER-COLOR)) (above (render-text (player-id player)) (overlay (render-player-score player) PLAYER-IMG (circle size 'outline color)))) ;; Feaster -> Image ;; Draw the players score (define (render-player-score player) (render-text (number->string (get-score (body-size (player-body player)))))) ;; Id [Listof Feaster] Image -> Image ;; draws the path of the player whose id is passed in (define (add-path id players base-scene) (define player (findf (lambda (x) (id=? id (player-id x))) players)) (if (boolean? player) base-scene (add-waypoint* player base-scene))) ;; Feaster Image -> Image ;; draws the list of way points to the scene (define (add-waypoint* player base-scene) (define loc (body-loc (player-body player))) (define ways (player-waypoints player)) (define-values (resulting-scene _) (for/fold ([scn base-scene][from loc]) ([to ways]) (values (add-waypoint from to scn) to))) resulting-scene) ;; Complex Complex Image -> Image ;; Add a waypoint to the scene at those coordinates (define (add-waypoint from to s) (define x-from (real-part from)) (define y-from (imag-part from)) (define x-to (real-part to)) (define y-to (imag-part to)) (define with-line (add-line s x-to y-to x-from y-from WAYPOINT-COLOR)) (place-image WAYPOINT-NODE x-to y-to with-line)) ;; ----------------------------------------------------------------------------- ;; render the end ;; Score -> Image ;; draws the end of the game (define (render-scores msg) (define scores (sort (second msg) < #:key second)) (for/fold ([img empty-image]) ([name-score scores]) (define txt (get-text name-score)) (above (render-text txt) img))) ;; (list ID Natural) -> string ;; builds a string for that winning pair (define (get-text name-score) (define-values (name score) (apply values name-score)) (string-append name SEPERATOR (number->string score))) ; ; ; ; ; ; ;;;;; ; ;; ; ; ; ;; ;; ;;; ;;; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ;; ; ;;;;;; ; ; ;; ; ; ; ; ; ; ; ; ; ; ; ;; ; ; ; ;;; ;;; ;;; ;; ;;; ;;; ; ; ; ; ; ;; String -> Image ;; draws the text (define (render-text txt) (text txt TEXT-SIZE TEXT-COLOR)) ;; player -> Number ;; Gets the X coord of a entrees (define (feaster-x feaster) (body-x (player-body feaster))) ;; player -> Number ;; Gets the Y coord of a entrees (define (feaster-y feaster) (body-y (player-body feaster))) ;; body -> Number ;; gets the X coord of a body (define (body-x body) (real-part (body-loc body))) ;; body -> Number ;; gets the Y coord of a body (define (body-y body) (imag-part (body-loc body))) ; ; ; ; ; ; ;;;;;;;;; ; ; ; ; ; ; ; ; ; ; ;;;; ;;;; ; ;;;;;;; ;;;; ; ; ; ; ; ; ; ; ;; ; ; ;; ; ; ; ; ; ; ; ; ; ;;;;;;;; ;;;;; ; ;;;;; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ;;;;; ;;;;; ;;;;;; ;;;; ;;;;;; ; ; ; ; ; (module+ test (require rackunit rackunit/text-ui) ;; testing main client (check-equal? (switch-to-entree (app "foo" 'blah 1) '(STATE () ())) (entree "foo" '()'())) (check-equal? (handle-server-messages (app #f 'ksajfhsdkjhfr 1) .5) (handle-appetizer-message (app #f 'ksajfhsdkjhfr 1) .5)) ;;dispatch-mouse (check-equal? (set-waypoint (app 1 LOADING 0) 1 1 "button-down") (app 1 LOADING 0)) (check-equal? (set-waypoint (app 1 LOADING 0) 1 1 "button-up") (app 1 LOADING 0)) (check-equal? (set-waypoint (app #f LOADING 0) 1 1 "button-down") (app #f LOADING 0)) (check-equal? (set-waypoint (app #f LOADING 0) 1 1 "button-up") (app #f LOADING 0)) (check-equal? (set-waypoint (entree "player1" (list (player "player1" (body 1 1+1i) empty)) empty) 1 1 "button-up") (entree "player1" (list (player "player1" (body 1 1+1i) empty)) empty)) (check-equal? (set-waypoint (entree "player1" (list (player "player1" (body 1 1+1i) empty)) empty) 1 1 "button-down") (make-package (entree "player1" (list (player "player1" (body 1 1+1i) empty)) empty) (list 'goto 1 1))) ;;render-the-meal ;; testing message receipt ;; app-may-start ;; entree-msg ;; update-msg? (check-true (state? `(,SERIALIZE (,(player "player1" (body 1+4i 234) `()) ,(player "player1" (body 3 3) `())) (,(body 1+i 2) ,(body 2 2))))) (check-true (state? `(,SERIALIZE (,(player "player1" (body 1+4i 234) `())) (,(body 1+i 2) ,(body 2 2))))) (check-true (state? `(,SERIALIZE () (,(body 1+i 2) ,(body 2 2))))) (check-true (state? `(,SERIALIZE (,(player "player1" (body 1+4i 234) `()) ,(player "player1" (body 3 3) `())) ()))) (check-false (state? `(,SERIALIZE (("player1" (1+4i 234) ()) ("player1" (3 3) ())) ((1+i 2) (2 2))))) (check-false (state? `(,SERIALIZE (("player1" (1+4i 234) ())) ((1+i 2) (2 2))))) (check-false (state? `(,SERIALIZE () ((1+i 2) (2 2))))) (check-false (state? `(,SERIALIZE (("player1" (1+4i 234) ()) ("player1" (3 3) ())) ()))) (check-true (state? `(,SERIALIZE () ()))) (check-false (state? `(,SERIALIZE (("player1" (1+4i 234) ()) ("player1" (3 3) ())) ((1+i 2) (2 2))))) (check-false (state? `(,SERIALIZE (("player1" (1+4i 234) ())) ((1+i 2) (2 2))))) (check-false (state? `(,SERIALIZE () ((1+i 2) (2 2))))) (check-false (state? `(,SERIALIZE (("player1" (1+4i 234) ()) ("player1" (3 3) ())) ()))) (check-false (state? '(u ((1 1+4i 234)) ((1+i 2) (2 2))))) (check-false (state? '(((1 1+4i 234)) ((1+i 2) (2 2))))) (check-false (state? '(u ((1 1+4i)) ((1+i 2) (2 2))))) (check-false (state? '(u ((1 1+4i 234)) ((1+i 2) (2 b))))) (check-false (state? '(u ((1 1+4i 234))))) (check-false (state? '(u ((1+i 2) (2 2))))) (check-false (state? '(((1+i 2) (2 2))))) (check-false (state? 4)) (check-false (state? 'f)) ;; score-list? (check-true (score-list? '(("s" 0) ("l" 0) ("sdf" 0)))) (check-true (score-list? empty)) (check-true (score-list? '(("s" 0) ("l" 0)))) (check-false (score-list? '(('s 0) ('l 0) ('sdf 0)))) (check-false (score-list? '((s 0) (l 0)))) (check-false (score-list? '((s) (l)))) (check-false (score-list? '((s 0) (l 0)))) ;; update-entree (check-equal? (update-entree (entree "player10" '() '()) `(s (,(player "player1" (body 10 10) `(3 4+9i)) ,(player "player10" (body 103 10+4i) `(3 5+78i))) (,(body 5 10) ,(body 30 30)))) (entree "player10" (list (player "player1" (body 10 10) (list 3 4+9i)) (player "player10" (body 103 10+4i) (list 3 5+78i))) (list (body 5 10) (body 30 30)))) ;; testing rendering the client ;; draw-app (check-equal? (render-appetizer (app #f LOADING 0)) (add-progress-bar (overlay LOADING BASE) 0)) ;; draw-entree ;; draw-players (check-equal? (add-players "player0" (list (player "player1" (body 40 23+34i) empty) (player "player0" (body 50 1+3i) empty)) BASE) (place-image (render-avatar "player0" (player "player0" (body 50 1+3i) empty)) 1 3 (place-image (render-avatar "player0" (player "player1" (body 40 23+34i) empty)) 23 34 BASE))) (check-equal? (add-players "player0" (list (player "player1" (body 40 23+34i) empty)) BASE) (place-image (render-avatar "player0" (player "player1" (body 40 23+34i) empty)) 23 34 BASE)) ;; draw-player ;; get-player-image (check-equal? (render-avatar "player0" (player "player0" (body 30 1+3i) empty)) (above (render-text "player0") (overlay (text (number->string (get-score 30)) 20 'black) PLAYER-IMG (circle 30 "outline" MY-COLOR)))) (check-equal? (render-avatar "player0" (player "player1" (body 30 1+3i) empty)) (above (render-text "player1") (overlay (text (number->string (get-score 30)) 20 'black) PLAYER-IMG (circle 30 "outline" PLAYER-COLOR)))) ;; draw-food (check-equal? (add-food (list (body 34 54+3i) (body 9 45+23i)) BASE) (place-image FOOD-IMG 45 23 (place-image FOOD-IMG 54 3 BASE))) (check-equal? (add-food (list (body 34 54+3i)) BASE) (place-image FOOD-IMG 54 3 BASE)) ;; testing auxiliary functions ;; player-x (check-equal? (feaster-x (player 20 (body 3 1+3i) empty)) 1) (check-equal? (feaster-x (player 20 (body 3 4+3i) empty)) 4) (check-equal? (feaster-x (player 20 (body 3 4+67i) empty)) 4) ;; player-y (check-equal? (feaster-y (player 20 (body 3 1+3i) empty)) 3) (check-equal? (feaster-y (player 20 (body 3 4+3i) empty)) 3) (check-equal? (feaster-y (player 20 (body 3 4+67i) empty)) 67) ;; body-x (check-equal? (body-x (body 20 1+2i)) 1) (check-equal? (body-x (body 20 4+2i)) 4) (check-equal? (body-x (body 20 3+2i)) 3) ;; body-y (check-equal? (body-y (body 20 4+1i)) 1) (check-equal? (body-y (body 20 1+4i)) 4) (check-equal? (body-y (body 20 3)) 0) "client: all tests run")