#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")