On this page:
Click The Dropping Balls
Finishing Up The Game
Before you go...
7.4

6 Using Abstractions To Game

home work!

Purpose: The purpose of this lab is to practice the use of existing list-processing functions, local, and λ.

Textbook References: Chapter 16.1: Existing Abstractions

Click The Dropping Balls

We’re going to design a game where every half second a ball drops from the sky from a random starting position, with a random horizontal velocity, radius, and color. Clicking the balls makes them disappear, but they can also fall off the edge of the screen. The game ends when too many balls have fallen off the edge or enough have been clicked, and the game will inform the player if they have won or lost.

What constitutes too many/enough balls, respectively, will be the two arguments supplied to the main function. We will use the power of local to avoid these having to become part of the world state.

Starter Code: Below is the data definiton for the game as well as constants that will prove helpful both graphically and in gameplay.

(require 2htdp/image)
(require 2htdp/universe)
 
(define WIDTH 500)
(define HEIGHT 500)
(define GRAVITY 0.1)
(define BALL-FREQUENCY 14)
(define BG (empty-scene WIDTH HEIGHT))
(define WINNER (overlay (text "Winner!" 20 "blue") BG))
(define LOSER (overlay (text "Loser!" 20 "red") BG))
 
; A ClickGame is a (make-click-game [List-of Ball] Number Number Number)
(define-struct click-game [balls time clicked missed])
; and represents the list of balls on screen, the time passed, the # clicked, and the # missed
 
; A Ball is a (make-ball Posn Posn Number Color)
(define-struct ball [pos vel rad color])
; and represents its position, velocity, radius, and color
 
; A Posn is a (make-posn Number Number)
 
(define posn-10 (make-posn 10 10))
(define posn-2 (make-posn 2 2))
 
(define ball-1 (make-ball posn-10 posn-2 5 "red"))
(define ball-2 (make-ball (make-posn (add1 WIDTH) 0) (make-posn 0 0) 5 "red"))
 
(define game-1 (make-click-game (list ball-1) 10 1 1))
(define game-2 (make-click-game (list ball-2) 1 0 0))
 
; clickgame-temp : ClickGame -> ?
(define (clickgame-temp cg)
  (... (lob-temp (click-game-balls cg))
       (click-game-time cg)
       (click-game-clicked cg)
       (click-game-missed cg)))
 
; lob-temp : [List-of Ball] -> ?
(define (lob-temp lob)
  (... (cond [(empty? lob) ...]
             [(cons? lob) (... (ball-temp (first lob))
                               (lob-temp (rest lob)))])))
 
; ball-temp : Ball -> ?
(define (ball-temp b)
  (... (posn-temp (ball-pos b)) (posn-temp (ball-vel b)) (ball-rad b) (ball-color b)))
 
; posn-temp : Posn -> ?
(define (posn-temp p)
  (... (posn-x p) (posn-y p)))

Starter Code: Instead of having to manually reconstruct a click-game every time we want to modify it, it will be much more useful to have functions that modify the components of the struct and rebuild it for us.

; modify-balls : [[List-of Ball] -> [List-of Ball]] ClickGame -> ClickGame
; Modify the list of balls
(define (modify-balls balls-modifier cg)
  (make-click-game (balls-modifier (click-game-balls cg))
                   (click-game-time cg)
                   (click-game-clicked cg)
                   (click-game-missed cg)))
(check-expect (modify-balls rest game-1) (make-click-game '() 10 1 1))
 
; modify-time : [Number -> Number] ClickGame -> ClickGame
; Modify time
(define (modify-time time-modifier cg)
  (make-click-game (click-game-balls cg)
                   (time-modifier (click-game-time cg))
                   (click-game-clicked cg)
                   (click-game-missed cg)))
(check-expect (modify-time add1 game-1) (make-click-game (list ball-1) 11 1 1))
 
; modify-clicked : [Number -> Number] ClickGame -> ClickGame
; Modify clicked
(define (modify-clicked clicked-modifier cg)
  (make-click-game (click-game-balls cg)
                   (click-game-time cg)
                   (clicked-modifier (click-game-clicked cg))
                   (click-game-missed cg)))
(check-expect (modify-clicked add1 game-1) (make-click-game (list ball-1) 10 2 1))
 
; modify-missed : [Number -> Number] ClickGame -> ClickGame
; Modify missed
(define (modify-missed missed-modifier cg)
  (make-click-game (click-game-balls cg)
                   (click-game-time cg)
                   (click-game-clicked cg)
                   (missed-modifier (click-game-missed cg))))
(check-expect (modify-missed add1 game-1) (make-click-game (list ball-1) 10 1 2))

Starter Code: Below is the main function. Note how it uses local so we don’t have to keep the limit on balls that can be missed and hit in the state of the world even though it changes every time main is called.

; main : Number Number -> Number
; Given the limit on balls that can be missed and hit, play the game
; and produce the time passed
(define (main missed clicked)
  (local [; click-game-over?/main : ClickGame -> Boolean
          ; Is the game over?
          (define (click-game-over?/main cg)
            (click-game-over? cg missed clicked))
          ; final-screen/main : ClickGame -> Image
          ; Final screen
          (define (final-screen/main cg)
            (final-screen cg missed clicked))]
    (click-game-time (big-bang (make-click-game '() 0 0 0)
                       [on-tick advance-time]
                       [on-mouse click]
                       [stop-when click-game-over?/main final-screen/main]
                       [to-draw draw]))))

Starter Code: Below is a top-down definition of the on-tick function, advance-time.

The function is structured so that it increments the count of balls that fell offscreen before they could be clicked, removes them, moves the balls that still remain, applies gravity to their velocity, generates a ball if it’s the correct time, and increments the time.

Notice how for each one of these steps, a function is called that takes and produces a ClickGame.

The signature, purpose statement, and tests of the functions it calls are given, as is the body of increment-time and generate-ball-if-time. The former provides a simple example of how the modify functions provided above can be used. The latter shows how even though the list of balls are being modified, with the power of local we are able to use the game’s time in the function we pass to modify-balls.

; advance-time : ClickGame -> ClickGame
; Advance the time
(define (advance-time cg)
  (increment-time
   (generate-ball-if-time
    (apply-gravity
     (move-balls
      (remove-offscreen
       (increment-missed-offscreen cg)))))))
(check-expect (advance-time game-1)
              (make-click-game (list (make-ball
                                      (make-posn 12 12)
                                      (make-posn 2 (+ GRAVITY 2))
                                      5
                                      "red")) 11 1 1))
(check-expect (advance-time game-2)
              (make-click-game '() 2 0 1))
 
; increment-time : ClickGame -> ClickGame
; Increase time
(define (increment-time cg)
  (modify-time add1 cg))
(check-expect (increment-time game-1)
              (make-click-game (list ball-1) 11 1 1))
 
; generate-ball-if-time : ClickGame -> ClickGame
; Generate a ball if it is the right time
(define (generate-ball-if-time cg)
  (local [; new-ball : ? -> Ball
          ; Generate a new ball
          (define (new-ball _)
            (make-ball (make-posn (+ (/ WIDTH 5) (* 3/5 (random WIDTH))) 0)
                       (make-posn (* (if (zero? (random 2)) 1 -1) (random 3)) 0.1)
                       (+ 10 (random 10))
                       (make-color (random 256) (random 256) (random 256))))
          ; add-new-ball-if-appropriate : [List-of Ball] -> [List-of Ball]
          ; Add a new ball if it's time
          (define (add-new-ball-if-appropriate lob)
            (if (zero? (modulo (click-game-time cg) BALL-FREQUENCY))
                (cons (new-ball #f) lob)
                lob))]
    (modify-balls add-new-ball-if-appropriate cg)))
(check-expect (generate-ball-if-time game-1) game-1)
(check-random (generate-ball-if-time (make-click-game '() 0 0 0))
              (make-click-game
               (list (make-ball (make-posn (+ (/ WIDTH 5) (* 3/5 (random WIDTH))) 0)
                                (make-posn (* (if (zero? (random 2)) 1 -1) (random 3)) 0.1)
                                (+ 10 (random 10))
                                (make-color (random 256) (random 256) (random 256))))
               0 0 0))
 
; apply-gravity : ClickGame -> ClickGame
; Increase the velocity of the balls due to gravity
(define (apply-gravity cg) ...)
(check-expect (apply-gravity game-1)
              (make-click-game (list (make-ball posn-10 (make-posn 2 (+ 2 GRAVITY)) 5 "red")) 10 1 1))
 
; move-balls : ClickGame -> ClickGame
; Move the balls
(define (move-balls cg) ...)
(check-expect (move-balls game-1)
              (make-click-game (list (make-ball (make-posn 12 12) posn-2 5 "red")) 10 1 1))
 
; remove-offscreen : ClickGame -> ClickGame
; Remove offscreen balls
(define (remove-offscreen cg) ...)
(check-expect (remove-offscreen game-1) game-1)
(check-expect (remove-offscreen game-2)
              (make-click-game '() 1 0 0))
 
; increment-missed-offscreen : ClickGame -> ClickGame
; Increment the balls that were missed
(define (increment-missed-offscreen cg) ...)
(check-expect (increment-missed-offscreen game-1) game-1)
(check-expect (increment-missed-offscreen game-2)
              (make-click-game (list ball-2) 1 0 1))

Sample Problem As both remove-offscreen and increment-missed-offscreen will need to be able to tell what balls have moved off screen, design a function which determines if a ball is offscreen. Note: You will have to comment out a large portion of the given code in order to run your tests for this exercise, as we have not finished implementing all the functions and the corresponding tests will therefore break.

; offscreen? : Ball -> Boolean
; Is b offscreen?
(define (offscreen? b)
  (local [; offscreen?/posn : Posn -> Boolean
          ; Is p offscreen?
          (define (offscreen?/posn p)
            (or (< (posn-x p) 0)
                (>= (posn-x p) WIDTH)
                (< (posn-y p) 0)
                (>= (posn-y p) HEIGHT)))]
    (offscreen?/posn (ball-pos b))))
(check-expect (offscreen? ball-1) #f)
(check-expect (offscreen? ball-2) #t)

Sample Problem Fill out the definitions of remove-offscreen and increment-missed-offscreen.

; remove-offscreen : ClickGame -> ClickGame
; Remove offscreen balls
(define (remove-offscreen cg)
  (local [; remove-offscreen/lob : [List-of Ball] -> [List-of Ball]
          ; Remove offscreen balls in lob
          (define (remove-offscreen/lob lob)
            (filter (λ (b) (not (offscreen? b))) lob))]
    (modify-balls remove-offscreen/lob cg)))
(check-expect (remove-offscreen game-1) game-1)
(check-expect (remove-offscreen game-2)
              (make-click-game '() 1 0 0))
 
; increment-missed-offscreen : ClickGame -> ClickGame
; Increment the balls that were missed
(define (increment-missed-offscreen cg)
  (local [; add-offscreen : Number -> Number
          ; Add the count of the offscreen balls in cg to missed
          (define (add-offscreen missed)
            (foldr (λ (b count)
                     (if (offscreen? b)
                         (add1 count)
                         count))
                   missed
                   (click-game-balls cg)))]
    (modify-missed add-offscreen cg)))
(check-expect (increment-missed-offscreen game-1) game-1)
(check-expect (increment-missed-offscreen game-2)
              (make-click-game (list ball-2) 1 0 1))

Exercise 1 Fill out the definitions of move-balls and apply-gravity.

Exercise 2 Design draw, which draws the Balls on the BG.

Exercise 3 Comment out every big-bang clause except for to-draw and on-tick. Do the balls act as expected?

Switch pair programming roles before continuing!

Finishing Up The Game

Exercise 4 Design a function which takes a Ball and an x and y coordinate and determines if that coordinate is in the Ball. Recall from math class that a point is in a circle if its distance from the center is less than or equal to the radius, and that the distance between two points is the square root of the sum of the squares of the x- and y- distances between the two points.

Exercise 5 Design a function which takes a ClickGame and an x and y coordinate and increments the count of the circles clicked by how many balls that coordinate lies in.

Switch pair programming roles before continuing!

Exercise 6 Design a function which takes a ClickGame and an x and y coordinate and removes the balls that coordinate lies in.

Exercise 7 Design click, the on-mouse function for the game.

Exercise 8 Design click-game-over? and final-screen. Infer their signatures and purpose statements from their use in main.

Switch pair programming roles before continuing!

Exercise 9 Play your game!

Exercise 10 Modify your game so that instead of producing one ball at the set frequency, it produces between 1 and 5 (inclusive) unique balls with a uniform probability. You shouldn’t need to create any new functions to accomplish this, but you will likely need to rewrite a test. Use check-satisfied and the appropriate pre-defined list abstraction to ensure all of the balls produced have the properties you expect them to.

Before you go...

If you had trouble finishing any of the exercises in the lab or homework, or just feel like you’re struggling with any of the class material, please feel free to come to office hours and talk to a TA or tutor for additional assistance.

Exercise 11 Modify your game to your heart’s content. If you think the game is too hard or easy, changing GRAVITY and the values used in generate-ball-if-time should have a large impact. How many tests will you have to change by just changing some numerical values? When changing constants, is it helpful or tedious to have to change tests? How does one avoid having to update tests excessively?