From a567de9a4841eac57247ea2caf71c90bfd9b73a4 Mon Sep 17 00:00:00 2001 From: Matthias Felleisen Date: Fri, 21 Dec 2012 21:10:47 -0500 Subject: [PATCH] distributed server game --- collects/realm/chapter13/client.rkt | 148 ++++++++++++++++++++++ collects/realm/chapter13/readme.txt | 30 +++++ collects/realm/chapter13/run.rkt | 57 +++++++++ collects/realm/chapter13/server.rkt | 182 ++++++++++++++++++++++++++++ collects/realm/chapter13/shared.rkt | 63 ++++++++++ 5 files changed, 480 insertions(+) create mode 100644 collects/realm/chapter13/client.rkt create mode 100644 collects/realm/chapter13/readme.txt create mode 100644 collects/realm/chapter13/run.rkt create mode 100644 collects/realm/chapter13/server.rkt create mode 100644 collects/realm/chapter13/shared.rkt diff --git a/collects/realm/chapter13/client.rkt b/collects/realm/chapter13/client.rkt new file mode 100644 index 0000000000..90a7e5101c --- /dev/null +++ b/collects/realm/chapter13/client.rkt @@ -0,0 +1,148 @@ +#lang racket + +;; the client for distributed Guess my Number +(require 2htdp/image 2htdp/universe "shared.rkt") + +(provide launch-guess-client) + +; +; +; +; +; +; ;;;;;; ; +; ; ; ; +; ; ; ;;;; ;;;;;; ;;;; +; ; ; ; ; ; ; ; +; ; ; ; ; ; +; ; ; ;;;;;; ; ;;;;;; +; ; ; ; ; ; ; ; +; ; ; ; ;; ; ; ; ;; +; ;;;;;; ;;;; ;; ;;;; ;;;; ;; +; +; +; +; + +;; ClientState = String +(define ClientState0 "no guess available") + +;; Constants +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(define TEXT-SIZE 11) +(define HELP-TEXT + (text "↑ for larger numbers, ↓ for smaller ones" + TEXT-SIZE + "blue")) +(define HELP-TEXT2 + (text "Press = when your number is guessed; q to quit." + TEXT-SIZE + "blue")) +(define WIDTH (+ (image-width HELP-TEXT2) 10)) +(define HEIGHT 150) +(define COLOR "red") +(define SIZE 72) +(define TEXT-X 3) +(define TEXT-UPPER-Y 10) +(define TEXT-LOWER-Y 135) +(define MT-SC + (place-image/align + HELP-TEXT TEXT-X TEXT-UPPER-Y + "left" "top" + (place-image/align + HELP-TEXT2 + TEXT-X TEXT-LOWER-Y "left" "bottom" + (empty-scene WIDTH HEIGHT)))) + + +; +; +; +; ; +; ; +; ;;; ;;; +; ;; ;; +; ; ; ; ; ;;;; ;;; ;; ;;; +; ; ; ; ; ; ; ; ;; ; +; ; ; ; ; ; ; ; ; +; ; ; ; ;;;;;; ; ; ; +; ; ; ; ; ; ; ; +; ; ; ; ;; ; ; ; +; ;;; ;;; ;;;; ;; ;;;;;;; ;;; ;;; +; +; +; +; + + +;; String -> ClientState +;; Launch the Client. +(define (launch-guess-client n host) + (big-bang ClientState0 + (on-draw draw-guess) + (on-key handle-keys) + (name n) + (register host) + (on-receive handle-msg))) + +;; handle-keys: ClientState Key -> [Package ClientState CtoSMessage] or ClientState +;; if the key is "up" or "down", ask the server for a different guess +(define (handle-keys w key) + (cond [(key=? key "up") (make-package w "up")] + [(key=? key "down") (make-package w "down")] + [(key=? key "q") (stop-with w)] + [(key=? key "=") (stop-with w)] + [else w])) + +;; handle-msg: ClientState StoCMessage -> ClientState +;; if the message is a number, you got a new guess +(define (handle-msg c msg) + (number->string msg)) + +;; draw-guess: ClientState -> Scene +;; renders the state as an image +(define (draw-guess c) + (overlay (text c SIZE COLOR) MT-SC)) + +; +; +; +; +; +; ;;;;;;; ; +; ; ; ; ; +; ; ; ; ;;; ;;;; ; ;;;;;; ;;;; ; +; ; ; ; ; ; ; ;; ; ; ;; +; ; ; ; ; ; ; +; ; ;;;;;;; ;;;;; ; ;;;;; +; ; ; ; ; ; +; ; ; ; ; ; ; ; ; ; +; ;;;;; ;;;; ;;;;;; ;;;; ;;;;;; +; +; +; +; + +(module+ test + + (require rackunit rackunit/text-ui) + + ;; testing the client's key-handling + + (check-equal? (handle-keys "55" "up") (make-package "55" "up")) + (check-equal? (handle-keys "47" "down") (make-package "47" "down")) + (check-equal? (handle-keys "10" "=") (stop-with "10")) + (check-equal? (handle-keys "66" "k") "66") + + ;; testing the client's message handling + + (check-equal? (handle-msg "100" 99) "99") + (check-equal? (handle-msg "30" -34) "-34") + + ;; testing the client's rendering function + + (check-equal? (draw-guess ClientState0) (overlay (text ClientState0 SIZE COLOR) MT-SC)) + (check-equal? (draw-guess "50") (overlay (text "50" SIZE COLOR) MT-SC)) + (check-equal? (draw-guess "25") (overlay (text "25" SIZE COLOR) MT-SC)) + + "client: all tests run") \ No newline at end of file diff --git a/collects/realm/chapter13/readme.txt b/collects/realm/chapter13/readme.txt new file mode 100644 index 0000000000..d7055b12a3 --- /dev/null +++ b/collects/realm/chapter13/readme.txt @@ -0,0 +1,30 @@ + +This chapter implements a distributed version of the "guess my number" game. + +TO PLAY, open the file + + run.rkt + +in DrRacket. The instructions for playing are at the top of the file. + +TO EXPERIMENT, open the files + + -- run.rkt + -- server.rkt + -- client.rkt + -- shared.rkt + +in four different tabs or windows in DrRacket. Switch to the 'run.rkt' +tab and select + + View | Show Module browser + +to see how these files are related. To switch to one of these four files, +you may click the boxes in the module browsers. Alternatively click the +tab you wish to work on. It is also possible to select tabs via key +strokes. + +Each file except for 'run.rkt' comes with test submodules at the bottom of +the file. + + diff --git a/collects/realm/chapter13/run.rkt b/collects/realm/chapter13/run.rkt new file mode 100644 index 0000000000..a6decf36e0 --- /dev/null +++ b/collects/realm/chapter13/run.rkt @@ -0,0 +1,57 @@ +#lang racket + +#| + The Guess My Number game, a distributed version with a GUI + ----------------------------------------------------------- + + You pick a number. The program guesses the nunber, + by asking you questions. Your responses are "too + small" "too large" or "you uessed it". + + In the Distributed Guess My Number game a player uses a client to connect + to the server. The Server attempts to guess what number the client is thinking + of. Each time the server guesses, the client must use the arrow keys to tell + the server if it is right, too small, or too large. + + Play + ---- + + Click Run. Pick a number X between and . + Evaluate + (run) + This will pop up three windows: + + -- Adam: with instructions for interacting with the program + + -- Universe: the console for the central server + it displays the messages that it receives and sends + + -- your server's state: a window that displays the server's internal state. + + Play and watch the two latter window to understand how the server and client + interact in response to your actions. + + To run the game on two distinct computers: + + -- copy this folder to another computer, determine its IP number "12.345.67.98" + -- open run.rkt + -- evaluate + (launch-guess-server) + + -- on your own computer, open run.rkt and run + -- evaluate + (launch-guess-client "12.345.67.98") +|# + +(require 2htdp/universe "client.rkt" "server.rkt") + +;; play the game as "Adam" +(define (run) + (launch-many-worlds (launch-guess-client "Adam" LOCALHOST) + (launch-guess-server))) + +;; what happens if two players sign up with the server simultaneously +(define (bad) + (launch-many-worlds (launch-guess-client "Adam" LOCALHOST) + (launch-guess-server) + (launch-guess-client "Beatrice" LOCALHOST))) \ No newline at end of file diff --git a/collects/realm/chapter13/server.rkt b/collects/realm/chapter13/server.rkt new file mode 100644 index 0000000000..40735e4612 --- /dev/null +++ b/collects/realm/chapter13/server.rkt @@ -0,0 +1,182 @@ +#lang racket + +;; the server for distributed Guess my Number + +(provide + ;; starts the distributed guess my number game + ;; -> GmNState + launch-guess-server) + +(require 2htdp/image 2htdp/universe "shared.rkt") + +; +; +; +; +; +; ;;;;;; ; +; ; ; ; +; ; ; ;;;; ;;;;;; ;;;; +; ; ; ; ; ; ; ; +; ; ; ; ; ; +; ; ; ;;;;;; ; ;;;;;; +; ; ; ; ; ; ; ; +; ; ; ; ;; ; ; ; ;; +; ;;;;;; ;;;; ;; ;;;; ;;;; ;; +; +; +; +; + +;; A GmNState is one of: +;; -- #f +;; -- GuessRange + +(struct interval (small big) #:transparent) +;; A GuessRange is (interval Number Number) +;; always true: (interval l u) means (<= l u) + +(define u0 (interval LOWER UPPER)) + +; +; +; +; ; +; ; +; ;;; ;;; +; ;; ;; +; ; ; ; ; ;;;; ;;; ;; ;;; +; ; ; ; ; ; ; ; ;; ; +; ; ; ; ; ; ; ; ; +; ; ; ; ;;;;;; ; ; ; +; ; ; ; ; ; ; ; +; ; ; ; ;; ; ; ; +; ;;; ;;; ;;;; ;; ;;;;;;; ;;; ;;; +; +; +; +; + +(define (launch-guess-server) + (universe #f + (state #t) + (on-new connect) + (on-msg handle-msg))) + +;; GmNState IWorld -> [Bundle GmNState [Listof [Mail IWorld Nat]] [Listof IWorld]] +;; handles all new connections. It only accepts one connection. +(define (connect u client) + (if (false? u) + (make-bundle u0 (list (make-mail client (guess u0))) '()) + (make-bundle u empty (list client)))) + +;; GmNState IWorld CtoSMessage -> [Bundle GmNState [List [Mail IWorld Nat]] Empty] +;; handles a message from the client. +(define (handle-msg u client msg) + (define w (next-interval u msg)) + (make-bundle w (list (make-mail client (guess w))) '())) + +;; GmNState CtoSMessage -> GmNState +;; creates the new universe for a responce +(define (next-interval u msg) + (cond [(not (string? msg)) u] + [(string=? "up" msg) (bigger u)] + [(string=? "down" msg) (smaller u)] + [else u])) + +; +; +; +; +; ;; ; +; ; ;; +; ; ;; ;; ;;;; ;;;;; ;;;;; +; ; ; ; ; ; ; ; ; ; +; ; ;;;; ; ; ;;;;;; ;;;; ;;;; +; ; ; ; ; ; ; ; +; ; ; ; ;; ; ; ; ; ; +; ;;; ;; ;; ;;;;; ;;;;; ;;;;; +; +; +; +; + +;; GuessRange -> Boolean +;; Does the interval represent a single number? +;; > (single? (interval 1 1)) +;; #t +(define (single? w) + (= (interval-small w) (interval-big w))) + +;; GuessRange -> Number +;; Calculates a guess based on the given interval +;; > (guess (interval 0 100)) +;; 50 +(define (guess w) + (quotient (+ (interval-small w) (interval-big w)) 2)) + +;; GuessRange -> GuessRange +;; Recreates a GuessRange that lowers the upper bound +;; > (smaller (interval 0 100)) +;; (interval 0 50) +(define (smaller w) + (interval (interval-small w) (max (interval-small w) (sub1 (guess w))))) + +;; GuessRange -> GuessRange +;; Recreates a interval that raises the lower bound +;; > (bigger (0 100) +;; (interval 51 100) +(define (bigger w) + (interval (min (interval-big w) (add1 (guess w))) (interval-big w))) + + +; +; +; +; +; ; ; +; ;;;;; ;;; ;;;; ;;;;; ;;;; +; ; ; ; ; ; ; +; ; ;;;;; ;; ; ;; +; ; ; ; ; ; +; ; ; ; ; ; +; ;;; ;;;; ;;;; ;;; ;;;; +; +; + +(module+ test + + (require rackunit rackunit/text-ui) + + (define 51-100 (interval 51 100)) + + ;; testing the server's main function + (check-equal? (connect #f iworld1) + (make-bundle (interval 0 100) `(,(make-mail iworld1 50)) '())) + (check-equal? (handle-msg (interval 0 100) iworld1 "up") + (make-bundle 51-100 `(,(make-mail iworld1 (guess 51-100))) '())) + + + ;; testing the server's handlers + + (check-true (single? (interval 50 50))) + (check-false (single? (interval 50 51))) + + (check-equal? (guess (interval 0 100)) 50) + (check-equal? (guess (interval 50 100)) 75) + (check-equal? (guess (interval 0 50)) 25) + + (check-equal? (smaller (interval 0 100)) (interval 0 49)) + (check-equal? (smaller (interval 0 000)) (interval 0 0)) + (check-equal? (smaller (interval 0 50)) (interval 0 24)) + (check-equal? (smaller (interval 50 100)) (interval 50 74)) + (check-equal? (smaller (bigger (bigger (interval 0 100)))) + (interval 76 87)) + + (check-equal? (bigger (interval 0 100)) (interval 51 100)) + (check-equal? (bigger (interval 0 000)) (interval 0 0)) + (check-equal? (bigger (interval 0 100)) (interval 51 100)) + (check-equal? (bigger (interval 51 100)) (interval 76 100)) + (check-equal? (bigger (interval 0 50)) (interval 26 50)) + + "server: all tests run") diff --git a/collects/realm/chapter13/shared.rkt b/collects/realm/chapter13/shared.rkt new file mode 100644 index 0000000000..0f42b54c6a --- /dev/null +++ b/collects/realm/chapter13/shared.rkt @@ -0,0 +1,63 @@ +#lang racket + +#| + the shared vocabulary and knowledge of server and client + + Message Formats + --------------- + + StoCMessage is the set of numbers between LOWER and UPPER (inclusive). + The numbers represent the guess. + + CtoSMessage is one of the following two strings: + -- "up" + -- "down" + with the obvious meaning. + + + Message Exchanges + ----------------- + + server client + | | + | register | + |<------------| + | | + | | <----- guess ("up", "down") + | CtoSMessage | + |<------------| + | | + | StoCMessage | + |------------>| + | | + | | +|# + + +(provide + ;; the to-be-guessed number must be in [LOWER,UPPER] + UPPER + LOWER) + +; +; +; +; +; +; ;;;;;; ; +; ; ; ; +; ; ; ;;;; ;;;;;; ;;;; +; ; ; ; ; ; ; ; +; ; ; ; ; ; +; ; ; ;;;;;; ; ;;;;;; +; ; ; ; ; ; ; ; +; ; ; ; ;; ; ; ; ;; +; ;;;;;; ;;;; ;; ;;;; ;;;; ;; +; +; +; +; + +;; prefined upper and lower limits for a game. +(define UPPER 100) +(define LOWER 0)