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

Lab 7 Using Abstractions To Game

lab!

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

Textbook References: Chapter 16.1: Existing Abstractions

Note: only use existing abstractions in this lab when the lab tells you to.

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.

(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 (balls-click-game 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
(check-expect (modify-balls rest GAME-1) (make-click-game '() 10 1 1))
(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)))
 
; modify-time : [Number -> Number] ClickGame -> ClickGame
; Modify time
(check-expect (modify-time add1 GAME-1) (make-click-game (list BALL-1) 11 1 1))
(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)))
 
; modify-clicked : [Number -> Number] ClickGame -> ClickGame
; Modify clicked
(check-expect (modify-clicked add1 GAME-1) (make-click-game (list BALL-1) 10 2 1))
(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)))
 
; modify-missed : [Number -> Number] ClickGame -> ClickGame
; Modify missed
(check-expect (modify-missed add1 GAME-1) (make-click-game (list BALL-1) 10 1 2))
(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))))

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
(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))
(define (advance-time cg)
  (increment-time
   (generate-ball-if-time
    (apply-gravity
     (move-balls
      (remove-offscreen
       (increment-missed-offscreen cg)))))))
 
; increment-time : ClickGame -> ClickGame
; Increase time
(check-expect (increment-time GAME-1)
              (make-click-game (list BALL-1) 11 1 1))
(define (increment-time cg)
  (modify-time add1 cg))
 
; generate-ball-if-time : ClickGame -> ClickGame
; Generate a ball if it is the right time
(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))
(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)))
 
; apply-gravity : ClickGame -> ClickGame
; Apply gravity to 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))
(define (apply-gravity cg) ...)
 
; move-balls : ClickGame -> ClickGame
; Move the balls
(check-expect (move-balls GAME-1)
              (make-click-game (list (make-ball (make-posn 12 12) POSN-2 5 "red")) 10 1 1))
(define (move-balls cg) ...)
 
; remove-offscreen : ClickGame -> ClickGame
; Remove offscreen balls
(check-expect (remove-offscreen GAME-1) GAME-1)
(check-expect (remove-offscreen GAME-2)
              (make-click-game '() 1 0 0))
(define (remove-offscreen cg) ...)
 
; increment-missed-offscreen : ClickGame -> ClickGame
; Increment the balls that were missed
(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))
(define (increment-missed-offscreen cg) ...)

Exercise (Reviewed) 1 * As both increment-missed-offscreen and remove-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.

Exercise (Reviewed) 2 ** Fill out the definitions of increment-missed-offscreen and remove-offscreen. filter and local will be useful in both, and one will use modify-balls and the other will use modify-missed and length.

Exercise 3 ** Fill out the definitions of move-balls and apply-gravity. map, local, and modify-balls will be useful in both.

Exercise 4 ** Design draw, which draws the Balls on the BG. foldr will be helpful here.

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

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.

Finishing Up The Game

Exercise 6 ** 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 7 ** 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. filter, length, modify-clicked, and local will all be useful.

Exercise 8 ** Design a function which takes a ClickGame and an x and y coordinate and removes the balls that coordinate lies in. filter, modify-balls, and local will all be useful.

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

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

Exercise 11 * Play your game!

Exercise 12 ** 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?