Lecture 1: Monday morning Designing Interactive Games with world.ss Teachpack --------------------------------------------------- Fish swim across the screen from right to left. There may be one fish, or a whole school of fish, or none at a time. A hungry shark swims in place at the left side, moving only up and down, controlled by the "up" and "down" keys. It tries to catch and eat the fish. It gets bigger with each fish it eats, it keeps getting hungrier and hungrier as the time goes on between the catches. It dies of starvation, if not fed in time. We would like to design this game. We focus on the logic of the game - not on system interactions (key event listeners, timer, graphics context, drawing frames, windows, etc.). To design the game behavior we just follow our good-old design recipe. Where do we start? What do we need to do this? Step 1: --------------------------------------- PROBLEM ANALYSIS AND DATA DEFINITIONS --------------------------------------- Let us draw a picture of what the game looks like: +-----------------------------+ | | | Scene Clock tick transformation: -- a function tick-tock that consumes the current 'world' and produces the world as it looks one time tick later ;; tick-tock: World -> World Keyboard input response: -- a function consume-key that consumes the current 'world' and a KeyEvent data (representing a key that has been pressed or released) and produces the world as it looks in response to the given KeyEvent ;; consume-key: World KeyEvent -> World The data definition for the KeyEvent is: ;; A KeyEvent is a Char or a Symbol Examples of KeyEvents: \#x 'left 'up \#Z Defining the end of the game: -- a functions world-ends? that consumes the current 'world' and determines whether the game is over ;; world-ends?: World -> Boolean Running the game interactions: -- start with the function 'big-bang' that starts running the game: ;; run the game on a canvas of the width 200 and height 400 ;; with the clock ticking every 0.4 seconds ;; starting with the 'world' my-world-0 (big-bang 200 400 0.4 my-world-0) -- include after the 'big-bang' function the instructions what should happen to the world on each tick and in response to the key events, how the world is to be drawn, and when does the game stop - as follows: (on-tick-event tick-tock) (on-key-event process-key) (on-redraw draw-the-world) (stop-when world-ends?) --- Of course, the names of the functions 'tick-tock', 'process-key', 'draw-the-world', and 'world-ends?' can be anything you choose, as long as they are then used in describing the world and the game as shown above. -- The teachpack also allows us to program mouse actions (we'll see that in the lab). ------------------------------------------------- PROGRAMMING THE GAME ACTIONS - DATA DEFINITIONS ------------------------------------------------- We recall our first step -- the data definitions: ;;------------------------------------------------------------- ;; A Shark is (make-shark Posn Number) (define-struct shark (loc life)) ;; Examples of sharks: (define mac (make-shark (make-posn 10 100) 30)) (define mackie (make-shark (make-posn 10 100) -1)) (define mac-up (make-shark (make-posn 10 97) 30)) (define mac-down (make-shark (make-posn 10 103) 30)) ;;------------------------------------------------------------- ;; A Fish is (make-fish Posn) (define-struct fish (loc)) ;; Examples of fish: (define fishy (make-fish (make-posn 190 100))) (define fishy-moved (make-fish (make-posn 182 100))) (define gone2 (make-fish (make-posn -2 98))) ;;------------------------------------------------------------- ;; An OceanWorld is (make-ocean Shark Fish) (define-struct ocean (shark fish)) ;; Examples of ocean worlds: (define ow1 (make-ocean mac fishy)) (define ow1-up (make-ocean mac-up fishy)) (define ow1-down (make-ocean mac-down fishy)) (define ow2 (make-ocean mackie gone2)) ;;------------------------------------------------------------- ------------------------------------------- PROGRAMMING THE GAME - DRAWING THE WORLD ------------------------------------------- We start by making sure we can display the game scene with one shark and one fish. draw-the-world: --------------- -- We draw each fish as a small red circle (radius 10). -- We draw the shark as a scary black circle with radius given by the shark's lifespan. This way the player can see how close the shark is to dying. (We name our function 'draw-ocean') ;;------------------------------------------------------------- ;; Drawings ;;------------------------------------------------------------- ;; draw the image of the fish on the given scene ;; draw-fish: Fish Scene -> Scene (define (draw-fish a-fish a-scene) (place-image (circle 10 'solid 'red) (posn-x (fish-loc a-fish)) (posn-y (fish-loc a-fish)) a-scene)) ;; a visual test that the fish is drawn correctly - in the middle on the right (draw-fish fishy (empty-scene 200 200)) ;;------------------------------------------------------------- ;; draw the image of the shark on the given scene ;; draw-shark: Shark Scene -> Scene (define (draw-shark a-shark a-scene) (place-image (circle (shark-life a-shark) 'solid 'black) (posn-x (shark-loc a-shark)) (posn-y (shark-loc a-shark)) a-scene)) ;; a visual test that the shark is drawn correctly - in the middle on the left (draw-shark mac (empty-scene 200 200)) ;;------------------------------------------------------------- ;; draw the image of the ocean ;; draw-ocean: ocean -> Scene (define (draw-ocean an-ocean) (draw-shark (ocean-shark an-ocean) (draw-fish (ocean-fish an-ocean) (place-image (rectangle 200 200 'solid 'blue) 100 100 (empty-scene 200 200))))) ;; a visual test that the ocean is drawn correctly: one fish on the left ;; and the shark on the right - on a blue background (draw-ocean ow1) ;;------------------------------------------------------------- ------------------------------------------ PROGRAMMING THE GAME ACTIONS - THE CLOCK ------------------------------------------ The following tasks represent the basic functionality of the game: The fish swims as the time ticks away. The shark gets hungrier as the time ticks away. The shark moves up and down in response to the arrow keys. The shark dies of starvation?. We start by designing function that produces a new world after each tick of the clock. On each tick of the clock the world changes --- the fish swims and the shark gets hungrier. Later, we will let the shark eat the fish, replace the fish as it swims out of bounds, or is eaten. But at the beginning we just program the skeleton of the game. Here is the purpose and the contract: ;; produce a new OceanWorld after one minute elapsed: ;; move the fish, starve the shark ;; on-tick-ocean: OceanWorld -> OceanWorld (define (on-tick-ocean o-world) ...) Here are some examples: recall our data: (define mac (make-shark (make-posn 10 100) 30)) (define fishy (make-fish (make-posn 190 100))) (define ow1 (make-ocean mac fishy)) then we expect the following results: (on-tick-ocean ow1) --> (make-ocean (make-shark (make-posn 10 100) 29) fishy-moved)) The template makes us think: ;; produce a new OceanWorld after one minute elapsed: ;; move the fish, starve the shark ;; on-tick-ocean: OceanWorld -> OceanWorld (define (on-tick-ocean o-world) ... (ocean-fish o-world) ... -- Fish ... (ocean-shark o-world) ... -- Shark ) We need to delegate the task of moving fish and the task of starving the shark to wish list functions (you know how to follow the design recipe to get this done): ;;--------------------------------------------------------------------------- ;; move the fish to the left 3 pixels (later we may change the speed) ;; move-fish: Fish -> Fish (define (move-fish a-fish) (make-fish (make-posn (- (posn-x (fish-loc a-fish)) 8) (posn-y (fish-loc a-fish))))) (check-expect (move-fish fishy) fishy-moved) ;;--------------------------------------------------------------------------- ;; starve the shark a bit on each tick ;; starve-shark: Shark -> Shark (define (starve-shark a-shark) (make-shark (shark-loc a-shark) (- (shark-life a-shark) 1))) (check-expect (starve-shark (make-shark (make-posn 20 100) 10)) (make-shark (make-posn 20 100) 9)) Now we can design the body: ;; produce a new OceanWorld after one minute elapsed: ;; move the fish, starve the shark, check if the fish is eaten or has escaped ;; on-tick-ocean: OceanWorld -> OceanWorld (define (on-tick-ocean o-world) (make-ocean (starve-shark (ocean-shark o-world)) (move-fish (ocean-fish o-world)))) And run the tests: (check-expect (on-tick-ocean ow1) (make-ocean (make-shark (make-posn 10 100) 29) fishy-moved)) ;;--------------------------------------------------------------------------- The world ends when the shark starves to death. We need a function that consumes the world and determines whether the game is over. Again we defer to the function 'is-dead' the decision whether the shark died. (Later, we can then easily change the death criteria by dealing only with the shark.) The function to supply to the 'stop-when': ;;--------------------------------------------------------------------------- ;; determine if the world should end - whether the shark has starved to death ;; if the shark starved to death, end the world (define(world-ends? o-world) (is-dead? (ocean-shark o-world))) (check-expect (world-ends? (make-ocean (make-shark (make-posn 20 100) -2) fishy)) true) (check-expect (world-ends? (make-ocean (make-shark (make-posn 20 100) 21) fishy)) false) And the helper function that deals with the shark only: ;;--------------------------------------------------------------------------- ;; is the shark dead? ;; is-dead?: Shark -> Boolean (define (is-dead? a-shark) (< (shark-life a-shark) 3)) (check-expect (is-dead? (make-shark (make-posn 20 100) -2)) true) (check-expect (is-dead? (make-shark (make-posn 20 100) 21)) false) ;;--------------------------------------------------------------------------- We can now run our primitive world: (big-bang 200 200 0.4 ow1) (on-tick-event on-tick-ocean) (on-redraw draw-ocean) (stop-when world-ends?) ----------------------------------------------- PROGRAMMING THE GAME ACTIONS - THE KEY EVENTS ----------------------------------------------- The shark moves up and down in response to the 'up' and 'down' arrow keys. The fish does not do anything. So, we need a function that consumes the current world and the key event and produces a new world in response. The definition of the Key Event tells us: The data definition for the KeyEvent is: ;; A KeyEvent is a Char or a Symbol Examples of KeyEvents: \#x 'left 'up \#Z So, for our game we care about the following keys: 'up 'down as well as the letters #\u and #\d However, because only the shark responds to the key events, we delegate that task to a helper function 'on-arrow-key' and the rest of the function design is straightforward: ;;--------------------------------------------------------------------------- ;; world responds to the key events - only the shark matters ;; on-key: Ocean KeyEvent -> Ocean (define (on-key an-ocean a-key) (make-ocean (on-arrow-key (ocean-shark an-ocean) a-key) (ocean-fish an-ocean))) (check-expect (on-key ow1 'up) ow1-up) (check-expect (on-key ow1 'down) ow1-down) (check-expect (on-key ow1 'right) ow1) (check-expect (on-key ow1 #\u) ow1-up) (check-expect (on-key ow1 #\d) ow1-down) (check-expect (on-key ow1 #\x) ow1) We now need to design the function 'on-arrow-key': The purpose statement and the contract are: ;;--------------------------------------------------------------------------- ;; move the shark in response to the up and dpown arrow keys ;; on-arrow-key: Shark KeyEvent -> Shark (define (on-arrow-key a-shark a-key) ...) We need to make examples. The shark responds to the following KeyEvents: 'up -- moves up 'down -- moves down #\u -- moves up #\d -- moves down (on-arrow-key (make-shark (make-posn 20 100) 10) 'up) --> (make-shark (make-posn 20 97) 10)) (on-arrow-key (make-shark (make-posn 20 100) 10) 'down) --> (make-shark (make-posn 20 103) 10)) (on-arrow-key (make-shark (make-posn 20 100) 10) 'left) --> (make-shark (make-posn 20 100) 10)) (on-arrow-key (make-shark (make-posn 20 100) 10) #\u) --> (make-shark (make-posn 20 97) 10)) (on-arrow-key (make-shark (make-posn 20 100) 10) #\d) --> (make-shark (make-posn 20 103) 10)) (on-arrow-key (make-shark (make-posn 20 100) 10) #\x) --> (make-shark (make-posn 20 100) 10)) The template step here is really important. The KeyEvent is one of -- Symbol -- String and so we must cover both possibilities: ;;--------------------------------------------------------------------------- ;; move the shark in response to the up and dpown arrow keys ;; on-arrow-key: Shark KeyEvent -> Shark (define (on-arrow-key a-shark a-key) (cond [(symbol? a-key) ... ... (shark-loc a-shark) ... -- Posn ... (shark-life a-shark) ... -- Number] [(char? a-key) ... ... (shark-loc a-shark) ... -- Posn ... (shark-life a-shark) ... -- Number])) In each cond clause we need to ask additional questions - whether the shark moves up or down, or does not move at all. And, of course, we delegate the actual move function to another helper, 'move-shark': ;;--------------------------------------------------------------------------- ;; move the shark in response to the up and dpown arrow keys ;; on-arrow-key: Shark KeyEvent -> Shark (define (on-arrow-key a-shark a-key) (cond [(symbol? a-key) (cond [(symbol=? a-key 'up) (move-shark a-shark 0 -3)] [(symbol=? a-key 'down) (move-shark a-shark 0 3)] [else a-shark])] [(char? a-key) (cond [(char=? a-key #\u) (move-shark a-shark 0 -3)] [(char=? a-key #\d) (move-shark a-shark 0 3)] [else a-shark])])) Finally, we run our examples as tests: (check-expect (on-arrow-key (make-shark (make-posn 20 100) 10) 'up) (make-shark (make-posn 20 97) 10)) (check-expect (on-arrow-key (make-shark (make-posn 20 100) 10) 'down) (make-shark (make-posn 20 103) 10)) (check-expect (on-arrow-key (make-shark (make-posn 20 100) 10) 'left) (make-shark (make-posn 20 100) 10)) (check-expect (on-arrow-key (make-shark (make-posn 20 100) 10) #\u) (make-shark (make-posn 20 97) 10)) (check-expect (on-arrow-key (make-shark (make-posn 20 100) 10) #\d) (make-shark (make-posn 20 103) 10)) (check-expect (on-arrow-key (make-shark (make-posn 20 100) 10) #\x) (make-shark (make-posn 20 100) 10)) --- Oh, yes, the 'move-shark' method is defined as follows: ;;--------------------------------------------------------------------------- ;; move the shark give x y distance ;; move-shark: Shark Number Number -> Shark (define (move-shark a-shark dx dy) (make-shark (make-posn ( + dx (posn-x (shark-loc a-shark))) ( + dy (posn-y (shark-loc a-shark)))) (shark-life a-shark))) (check-expect (move-shark (make-shark (make-posn 20 100) 10) 2 5) (make-shark (make-posn 22 105) 10)) ;;--------------------------------------------------------------------------- We can now run our world with key events: (big-bang 200 200 0.4 ow1) (on-tick-event on-tick-ocean) (on-key-event on-key) (on-redraw draw-ocean) (stop-when world-ends?) ;;--------------------------------------------------------------------------- We can now add more complexity to the game: * On each tick the fish moves to the left (3 pixels, or given by its speed) -- if it has moved out of the game area, it is replaced by a new fish on the left * The shark moves only in response to the 'up and 'down keys * On each tick we need to check if the shark can eat the fish -- if yes, the fish is consumed and replaced by a new one on the left at the same time, the shark's lifetime is extended -- if the shark did not consume a fish, his lifetime is reduced -- if the shark's lifetime expired, the game ends. And, of course, we would like our ocean world to have a whole school of fish. We add one new feature at a time and completely test is before proceeding. We can also replace the drawing functions with ones that draw more elaborate shapes - a fish with a tail and an eye, a shark with a red mouth with white teeth and an eye, etc.