Wednesday, 29 August 2012

Special Exercise - Space Invaders Part 4

 Space Invaders Part 4


My goals for the 4th iteration were to:

  •   allow the tank to shoot at the invaders
  •   have invaders  'die' when hit


To implement these goals we need a number of new elements and behaviours:

  •  the world now has a collection of bullets.
  •  bullets  have a location and consist of a single block.
  •  bullets move upwards until they collide with a space invader (cause the  invader to disappear)


In addition play testing showed that we need to restrict the number of bullets on the screen (or introduce a delay between shots). Without this you can just fire a lethal stream of bullets taking out all the UFO's very quickly.

Bullets, tanks and ufos turned out to be structured very similarly. I should have implemented these using plain posn's as this would have allowed more function sharing.

Code


(require racket/base)
(define-struct ufo (x y direction) #:transparent)
(define-struct bullet (x y) #:transparent )
(define-struct tank (x y) #:transparent)
(define-struct world (ufos tank bullets))

; constants
(define WIDTH 300)     ; the maximal number of blocks horizontally
(define HEIGHT 300)    ; the maximal number of blocks horizontally
(define BULLET_DIST 5) ; the distance travelled by a bullet
(define SIZE 10)       ; the size of the block making up the ufo
(define UFO-WIDTH 50)
(define MAX-BULLETS 2) ; max number of bullets on screen at a time

(define BLOCK ;rendered as red squares with black rims  - stolen from tetris

  (overlay (rectangle (- SIZE 1) (- SIZE 1) "solid" "red")
           (rectangle SIZE SIZE "outline" "black")))



(define TEST-UFO (make-ufo 30 30 1))          ; for testing

(define TEST-UFOS (list (make-ufo 30 30 1)
                        (make-ufo 80 30 1)
                        (make-ufo 130 30 1))); for testing

; initial tank position should be middle of the screen, one block up 

; from the bottom
(define TANK (make-tank (/ WIDTH 2) (- HEIGHT (* SIZE 2))))

; creates a row of ufos on the y-cordinate

; Integer Ufos -> Ufos
(define (make-ufos-for-y y-cordinate ufos)
  (cond ((empty? ufos) (make-ufos-for-y y-cordinate  
                                        (cons (make-ufo 20 y-cordinate 1) ufos)))
        ((> (+ (ufo-x (first ufos)) UFO-WIDTH) WIDTH) ufos)
        (else (printf  "first ~a" (+ (ufo-x (first ufos))))
              (make-ufos-for-y y-cordinate 
                               (cons  (make-ufo (+ (ufo-x (first ufos)) 
                                           UFO-WIDTH) y-cordinate 1) ufos )))))


; create a collection of rows of ufos

; Integer Ufos -> Ufos
(define (make-ufos rows ufos)
  (cond ((= 0 rows) ufos)
        (else (make-ufos (- rows 1) 
                         (append ufos (make-ufos-for-y (- ( * rows 60) 30) 
                                                       empty))))))


; draw a triangular shaped alien

(define (draw-ufo ufo background)
  (place-image/align BLOCK (ufo-x ufo) (ufo-y ufo) "left" "bottom"
   (place-image/align BLOCK (- (ufo-x ufo) SIZE) (ufo-y ufo) "left" "bottom"
    (place-image/align BLOCK (+ (ufo-x ufo) SIZE) (ufo-y ufo) "left" "bottom"
     (place-image/align BLOCK (ufo-x ufo) (+ (ufo-y ufo) SIZE) "left" "bottom"
         background)))))

; draw a tank shaped tank
(define (draw-tank tank background)
  (place-image/align BLOCK (tank-x tank) (tank-y tank) "left" "bottom"
   (place-image/align BLOCK (- (tank-x tank) SIZE) (tank-y tank) "left" "bottom"
    (place-image/align BLOCK (+ (tank-x tank) SIZE) (tank-y tank) "left" "bottom"
     (place-image/align BLOCK (tank-x tank) (+ (tank-y tank) SIZE)
        "left" "bottom"
      (place-image/align BLOCK (- (tank-x tank) SIZE) (+ (tank-y tank) SIZE ) 
          "left" "bottom"
       (place-image/align BLOCK (+ (tank-x tank) SIZE) (+ (tank-y tank) SIZE) 
           "left" "bottom"
        (place-image/align BLOCK (tank-x tank) (- (tank-y tank) SIZE) 
           "left" "bottom"
          background))))))))


; Draw a bullet on the background
; bullet, scene -> scene
(define (draw-bullet bullet background)
   (place-image/align BLOCK (bullet-x bullet) 
                            (bullet-y bullet) "left" "bottom" background))

; Draw a collection of bullets
(define (draw-bullets bullets background)
  (cond ((empty? bullets) background)
        (else (draw-bullet (first bullets) 
                           (draw-bullets (rest bullets) background)))))

; Move the bullets;

; If the bullet moves past the top of the screen, remove it.
(define (move-bullets bullets)
  (cond ((empty? bullets) bullets)
        (else  (cond ((> 0 (bullet-y (first bullets))) 
                         (move-bullets (rest bullets)))
                     (else
                         (cons (move-bullet (first bullets))
                               (move-bullets (rest bullets))))))))

; simple test case for moving a list of bullets

(check-expect (move-bullets (list (make-bullet 100 100) (make-bullet 90 100)))
              (list (make-bullet 100 (- 100 BULLET_DIST))
                    (make-bullet 90 (- 100 BULLET_DIST))))


; Move a bullet

(define (move-bullet bullet)
  (make-bullet (bullet-x bullet) (- (bullet-y bullet) BULLET_DIST)))

; basic bullet moving test

(check-expect (move-bullet (make-bullet 50 50)) (make-bullet 50 (- 50 BULLET_DIST)))

                               

; draw the collection of ufos on to the background.
; a simple recursive function
; Ufos Scene -> Scene
(define (draw-ufos ufos background)
  (cond ((empty? ufos)  background)
        (else (draw-ufo (first ufos) (draw-ufos (rest ufos) background)))))


; draws the world as a scene

; World -> Scene
(define (render-scene world)
  (draw-ufos (world-ufos world)
             (draw-tank (world-tank world)
                        (draw-bullets (world-bullets world)
                                      (empty-scene WIDTH HEIGHT)))))

; move the ufo left or right according to it's current direction
; when it reaches the edge of the screen, flip the direction the ufo 
; is travelling
(define (move-ufo ufo)
 (future-ufo-x  ufo) (ufo-direction ufo) WIDTH)
  (cond ((> 10 (future-ufo-x ufo))  (change-ufo-direction ufo))
        ((< (- WIDTH 10) (future-ufo-x  ufo)) (change-ufo-direction ufo))
        (else    (make-ufo (future-ufo-x  ufo) 
                           (ufo-y ufo) (ufo-direction ufo)))))




; returns an identical ufo but with the direction changed
(define (change-ufo-direction ufo)
  (make-ufo (ufo-x ufo) (+ (ufo-y ufo) 30) (* -1 (ufo-direction ufo))))



; simplest test for changing direction.
(check-expect (change-ufo-direction (make-ufo 10 10 1)) (make-ufo 10 40 -1))
                                                                 

; moves each ufo one at a time
(define (move-ufos ufos)
  (cond ((empty? ufos) ufos)
        (else (cons (move-ufo (first ufos)) (move-ufos (rest ufos))))))



; moves the tank left or right
; the direction is positive/negative mulitplier that will
; determine in which direction the tank moves
; we use the future-tank-x function to determine if the tank would be moved
; into a position outside of the game word.
; tank direction -> tank
(define (move-tank tank direction)
  (cond ((> 10 (future-tank-x direction tank))  tank)
        ((< (- WIDTH 20) (future-tank-x direction tank))  tank)
        (else  (make-tank (future-tank-x direction tank) (tank-y tank) ))))


; returns the position the tank will be in if it moves in *direction*

; used for collision detection with walls and actual moving of tank
; Integer Tank -> Integer
(define (future-tank-x direction tank)
  (+ (* SIZE direction) (tank-x tank) ))


; just like future-tank-x but for ufos
; the diference is that the ufo holds the direction internally
(define (future-ufo-x  ufo)
   (+ (* 1 (ufo-direction ufo)) (ufo-x ufo) ))




; Fire a bullet and returns new collection of bullets

; Allows only MAX-BULLETS on screen
; World -> Bullets
(define (fire-tank world)
  (cond ((eq? MAX-BULLETS (length (world-bullets world))) (world-bullets world))
        (else
         (cons (make-bullet (tank-x (world-tank world) )
                     (tank-y (world-tank world) )) (world-bullets world) ))))

; respond to keyboard input.

; Left - Move tank left
; Right - Move tank right
; Space - Fire (not yet)
(define (handle-key-events world ke)
  (make-world (world-ufos world)
              (cond
                [(string=? "left" ke)  (move-tank (world-tank world) -1)]
                [(string=? "right" ke) (move-tank (world-tank world) 1) ]
                [else (world-tank world)])
              (cond
                [(key=? " " ke)  (fire-tank world)]
                [else (world-bullets world)]
              )))

; bullets/ufo collision detector

; returns true in the event of a collision
; ufo, bullets -> boolean
(define (collide? ufo bullets)
  (cond ((empty? bullets) false)
        ((collide-helper?  (first bullets) ufo) true)
        ( else (collide? ufo (rest bullets)))))

; bullet/ufo collision helper. detects collision between a single bullet and
; a ufo
; ufo, bullet -> boolean
(define (collide-helper?  bullet ufo)
  (cond ((and (between? (bullet-x bullet) (- (ufo-x ufo) SIZE 5) 
                                          (+ (ufo-x ufo)  SIZE 5))
              (between? (bullet-y bullet) (- (ufo-y ufo) SIZE 5)
                                          (+ (ufo-y ufo) SIZE 5)))
               true)
        (else false)))

; helper function to determine if a number is within a range.

; i'm sure there must be a similar one defined in the libs, but I 
; couldn't find it.
; Integer, Integr, Integer -> Boolean
(define (between? x low high)
  (cond ((and (> x low) (< x high) true))
        (else false)))

(check-expect (between? 50 10 100) true)

(check-expect (between? 10 50 100) false)

; remove any ufos from the collection that have been hit by a bullet

; ufos, bullets -> ufos
(define (bullet-collision-ufos ufos bullets)
  (cond ((empty? ufos)       ufos )
        (else (cond ((collide? (first ufos) bullets) 
                               (bullet-collision-ufos (rest ufos) bullets))
                    (else (cons (first ufos) 
                                (bullet-collision-ufos (rest ufos) bullets)))))))
                                                   

; On each clock tick, move the world further in time.

; this calls the bullet-collision-ufos function twice - this calculates which
; bullets and ufos are left. I wish I didn't have to call this twice but not 
; sure how else to save the value...
(define (progress-world world)
  (make-world
     (bullet-collision-ufos (move-ufos (world-ufos world)) 
                                       (world-bullets  world))
                (world-tank world)
                (move-bullets (world-bullets world))))


; This is the big bang function that drives the game.

(define (space-invaders-main rate)
  (big-bang (make-world (make-ufos 3 empty ) TANK empty)      
            (on-key     handle-key-events)
            (on-tick    progress-world rate)
            (to-draw    render-scene)))

(space-invaders-main 0.01)



; TESTS

; check moving ufo left and right works as expected
(check-expect (move-ufo (make-ufo 10 10 1)) (make-ufo 20 10 1))
(check-expect (move-ufo (make-ufo 20 10 -1)) (make-ufo 10 10 1))
; check hitting the edge of the screen changes the ufos direction
(check-expect (move-ufo (make-ufo WIDTH 10 1)) (make-ufo WIDTH 10 -1))


; check the ufo change direction changes left and right
(check-expect (change-ufo-direction (make-ufo 10 10 1)) (make-ufo 10 10 -1))
(check-expect (change-ufo-direction (make-ufo 10 10 -1)) (make-ufo 10 10 1))


; simple example - a single block in an empty landscape
(check-expect (draw-ufo TEST-UFO (empty-scene WIDTH HEIGHT))
  (place-image/align BLOCK (ufo-x TEST-UFO) (ufo-y TEST-UFO) "left" "bottom"
      (place-image/align BLOCK (- (ufo-x TEST-UFO) SIZE) (ufo-y TEST-UFO) 
         "left" "bottom"
       (place-image/align BLOCK (+ (ufo-x TEST-UFO) SIZE) (ufo-y TEST-UFO) 
         "left" "bottom"
       (place-image/align BLOCK (ufo-x TEST-UFO) (+ (ufo-y TEST-UFO) SIZE) 
         "left" "bottom" 
                (empty-scene WIDTH HEIGHT))))))

; test drawing three ufo's via the draw-ufos function
(check-expect (draw-ufos TEST-UFOS (empty-scene WIDTH HEIGHT))
              (draw-ufo (first TEST-UFOS) 
                (draw-ufo (second TEST-UFOS) 
                   (draw-ufo (third TEST-UFOS) (empty-scene WIDTH HEIGHT)))))

; test the tank drawing works
(check-expect (draw-tank TANK (empty-scene WIDTH HEIGHT))
  (place-image/align BLOCK (tank-x TANK) (tank-y TANK) "left" "bottom"
   (place-image/align BLOCK (- (tank-x TANK) SIZE) (tank-y TANK) "left" "bottom"
    (place-image/align BLOCK (+ (tank-x TANK) SIZE) (tank-y TANK) "left" "bottom"
     (place-image/align BLOCK (tank-x TANK) (+ (tank-y TANK) SIZE) "left" "bottom"
      (place-image/align BLOCK (- (tank-x TANK) SIZE) (+ (tank-y TANK) SIZE ) 
           "left" "bottom"
       (place-image/align BLOCK (+ (tank-x TANK) SIZE) (+ (tank-y TANK) SIZE) 
            "left" "bottom"
        (place-image/align BLOCK (tank-x TANK) (- (tank-y TANK) SIZE)
            "left" "bottom" 
        (empty-scene WIDTH HEIGHT)))))))))



No comments:

Post a Comment