5.92

2 1/14: Interfaces in Space!

New tools

The lab computers now have the GitHub Windows client available, so if you’re using that on other computers, feel free to use it here as well. TortiseGit is still available as well.

Space!

In this lab, we’re going to clean up all the junk in space, both asteroids and satellites, which are littering the galaxy.

Let’s start with a basic animation of this problem. A World consists of Junk, each of which has a location (x, y), can draw itself, and can step itself through an animation. A Junk, for now, is either a Asteroid or a Satellite. Satellites are round, and Asteroids are rectangular.

#lang class/0
(require 2htdp/image)
(require class/universe)
 
(define WIDTH  500)
(define HEIGHT 500)
 
; A World is a (new world% [Listof Junk])
(define-class world%
  (fields junks)
 
  ; -> World
  ; Advance the World
  (define (on-tick)
    (new world% (map (λ (c) (send c step)) (send this junks))))
 
  ; -> Image
  ; Draw the World
  (define (to-draw)
    (foldr (λ (c scn) (place-image (send c draw)
                                   (send c x)
                                   (send c y)
                                   scn))
           (empty-scene WIDTH HEIGHT)
           (send this junks))))
 
; A Junk is one of:
;  - Asteroid
;  - Satellite
 
; A Location is a Complex where (x,y) is represented as the complex number x+yi
; A Velocity is a Complex where (x,y) is represented as the complex number x+yi

In this lab we’re going to do the inverse of today’s class: we’re going to treat complex numbers as a given type, rather than implement them ourselves. We’re going to use a trick for working with 2D geometry: represent the location (x,y) with the complex number x+yi. Common geometric transformations can now be done simply with addition and subtraction.

For instance, if you’re at location (2,5) and want to translate by (-1,2)1 step left and 2 steps down, in screen coordinates—to get to location (1,7), then you can add your location 2+5i to the translation -1+2i to get your new location 1+7i.

Or consider the opposite situation: if you’re at (2,5) and want to know what you must translate by to get to (1,7), you can subtract 1+7i - 2+5i = -1+2i to find out that (-1,2) is the translation from (2,5) to (1,7).

There’s much more to say about the relationship between 2D geometry and complex arithmetic, but we can get a long way with just the basics. Now where were we?

; A Non-Negative is a non-negative Number
; A Color is a String
; A Satellite is a (new satellite% Non-Negative Color Location Velocity)
(define-class satellite%
  (fields radius color location velocity)
 
  ; -> Number
  ; The x-coordinate of the Satellite
  (define (x)
    (real-part (send this location)))
 
  ; -> Number
  ; The y-coordinate of the Satellite
  (define (y)
    (imag-part (send this location)))
 
  ; -> Image
  ; The image representing the Satellite
  (define (draw)
    (circle (send this radius) "solid" (send this color)))
 
  ; -> Satellite
  ; The next Satellite in the animation sequence
  (define (step)
    (new satellite%
         (send this radius)
         (send this color)
         (+ (send this velocity) (send this location))
         (send this velocity))))
 
(check-expect (send (new satellite% 5 "red" 50+10i 0+1i) x) 50)
(check-expect (send (new satellite% 5 "red" 50-10i 0+1i) x) 50)
(check-expect (send (new satellite% 5 "red" 50+10i 0+1i) y) 10)
(check-expect (send (new satellite% 5 "red" 50-10i 0+1i) y) -10)
(check-expect (send (new satellite% 5 "red" 50+10i 0+1i) draw)
              (circle 5 "solid" "red"))
(check-expect (send (new satellite% 5 "red" 50+10i 0+1i) step)
              (new satellite% 5 "red" 50+11i 0+1i))
(check-expect (send (new satellite% 5 "red" 50-10i 0+1i) step)
              (new satellite% 5 "red" 50-9i 0+1i))
(check-expect (send (new satellite% 5 "red" 50-10i 1) step)
              (new satellite% 5 "red" 51-10i 1))
 
; A Asteroid is a (new asteroid% Location Non-Negative Non-Negative Color Velocity)
(define-class asteroid%
  (fields width height color location velocity)
 
  ; -> Number
  ; The x-coordinate of the Asteroid
  (define (x)
    (real-part (send this location)))
 
  ; -> Number
  ; The y-coordinate of the Asteroid
  (define (y)
    (imag-part (send this location)))
 
  ; -> Image
  ; The image representing the Asteroid
  (define (draw)
    (rectangle (send this width) (send this height) "solid" (send this color)))
 
  ; -> Asteroid
  ; The next Asteroid in the animation sequence
  (define (step)
    (new asteroid%
         (send this width)
         (send this height)
         (send this color)
         (+ (send this velocity) (send this location))
         (send this velocity))))
 
(check-expect (send (new asteroid% 10 20 "blue"  50+60i 0+1i) x) 50)
(check-expect (send (new asteroid% 10 20 "blue" -50+60i 0+1i) x) -50)
(check-expect (send (new asteroid% 10 20 "blue"  50+60i 0+1i) y) 60)
(check-expect (send (new asteroid% 10 20 "blue" -50+60i 0+1i) y) 60)
(check-expect (send (new asteroid% 10 20 "blue" -50+60i 0+1i) draw)
              (rectangle 10 20 "solid" "blue"))
(check-expect (send (new asteroid% 10 20 "blue" 50+60i 0+1i) step)
              (new asteroid% 10 20 "blue" 50+61i 0+1i))
(check-expect (send (new asteroid% 10 20 "blue" -50+60i 0+1i) step)
              (new asteroid% 10 20 "blue" -50+61i 0+1i))
 
(big-bang (new world% (list (new satellite%   5    "red"   50+10i 0+1i)
                            (new satellite%  10    "red"  150+10i 1+1i)
                            (new satellite%  20    "red"  250+10i 1-1i)
                            (new satellite%  10    "red"  350+10i 1)
                            (new satellite%   5    "red"  450+10i 2+1i)
                            (new asteroid% 30 20 "blue"  50+60i 1-1i)
                            (new asteroid% 15 10 "blue" 150+60i 2+1i)
                            (new asteroid%  5  5 "blue" 250+60i 2-1i)
                            (new asteroid% 15 10 "blue" 350+60i 0+2i)
                            (new asteroid% 30 20 "blue" 450+60i 1))))

When you run this, you should see satellites and asteroids moving. (Simple beginnings...)

Exercise 1. Write a function that produces a random initial world, and runs the animation.

As we said above, a Junk is something that has a location along with convenience methods for its x and y coordinates, can draw itself, and can step itself through an animation.

Exercise 2. Add a description of the interface of Junk for the behaviors common to both satellite% and asteroid%.

Escape

Exercise 3. End the game when all the space junk escapes. Use an escaped? method on Junk to test whether the Junk has it outside of the screen, and end the game when all of it has done so.

When all the Junk escapes the screen, the game is over—but for this to be any fun, we need a way to win! Next we will add a spaceship to the bottom of the screen to destroy the space junk:

The ship won’t do much: it just updates its position to the horizontal position of the mouse. It’s vertical position stays fixed.

Exercise 4. Add a Ship to the game. Fix its vertical position near the bottom of the screen and keep its horizontal position aligned with the position of the mouse.

Suggestion: Define a ship% class that understands location, x, y, and draw. (They can be either methods or fields.)

How much did you have to change your to-draw method in world%? If your answer isn’t “very little”, then pause and reconsider your Ship design.

A Ship isn’t a Junk since it doesn’t step and never needs to answer escaped?, but it does draw and have an x, y, and location. Moreover, to-draw in world% only relies on these last four behaviors—but we currently lack an interface to codify it.

Exercise 5. Split the Junk interface into two: a Drawable interface to support the needs of to-draw, and a simpler Junk interface that just includes step and escaped?.

Of your classes, which should implement which interfaces?—note that a class might implement multiple interfaces.

Exercise 6. Now you have a variety of interfaces and classes. Take a moment to sanity check each of them. Which classes should implement which interfaces?

Surviving invasion

To clean up the junk, the Ship must shoot some kind of projectile at the Junk: bullets, lasers, bananas—your choice.

Exercise 7. Define a class for your projectile. For the sake of concreteness, I’ll assume you chose Banana. Bananas must (1) have a location, (2) know how to draw themselves, and (3) step upward over time.

Which of the classes can you reuse from to save yourself work? Which aren’t appropriate to reuse?

Which interfaces do Bananas implement?

Exercise 8. Add a list of Bananas to the World. Draw them when the World draws, and step them when the World ticks.

But whence Bananas?

Exercise 9. Add a shoot method to ship% that creates a new Banana at the Ship’s location. Also add a shoot method to world% that asks the Ship to shoot and begins tracking its newly fired Banana.

Fire Bananas when the user clicks the mouse.

Too many Bananas!

Exercise 10. Remove Bananas from the World after they leave the screen. Add an on-screen? method to banana%, and remove off-screen Bananas on each World tick.

If a Banana falls in a forest...

Exercise 11. Add a contains? method to the Junk interface that takes a Location and computes whether the location is within the spatial extent of the Junk.

Implement contains? appropriately for each of satellite% and asteroid%. Note that circles and rectangles occupy different parts of space...

Exercise 12. Add a zapped? method to Junk that takes a list of Bananas as input and tests whether the Junk has been hit by any of them. Each tick, remove all Junk from the World that are being zapped by a Banana.

For the purposes of detecting a collision, just test to see if the center of the Banana is contained in the Junk. (For added realism, try drawing your Bananas as very small dots. Or if you really want to try proper shape intersection, try something shaped not like a banana.)

Exercise 13. Finally, we can win the game! Add a counter to your world, which counts how many seconds it takes until all the junk is gone from your screen. The goal of the game is to clear the junk in the smallest amount of time.

Note that by default, there are 28 ticks per second.

Abstraction and Helper Classes

You’ve probably noticed at this point that a number of your classes have some very similar code. For example, Asteroid and Satellite contain very similar move methods.

One technique for refactoring this is to create helper classes that encapsulate a certain kind of behavior. For example, we can create a Location data defintion and location% class to encapsulate points that can move.

Exercise 14. The convenience methods x and y are useful in the World’s to-draw method where it needs to use the x- and y-coordinates separately, but notice that the methods are implemented in the same way for Satellites and Asteroids. Also notice that both classes have a location field.

Use delegation to abstract the location field and the x and y methods into a common class, location%.

Exercise 15. Similarly, various kinds of things are drawable. Perform further refactoring to remove duplicated drawing code.

Go, banana

Exercise 16. (Open ended) Now that we’ve laid the groundwork for our new hit iPhone game, all that’s left is a few splashes of creativity. Below are a few ideas—and remember: a little randomness can substitute for a lot of complexity.
  • Add new shapes of Junk

  • Give Junk more complicated movement

  • Let Junk shoot back

  • Increase the difficulty by using keyboard instead of mouse control

  • Spawn new Junk over time