space-invaders-starter.rkt
(require 2htdp/universe)
(require 2htdp/image)
;; Space Invaders
;; ==========
;; Constants:
;; ==========
(define WIDTH 300)
(define HEIGHT 500)
(define INVADER-X-SPEED 2) ;speeds (not velocities) in pixels per tick
(define INVADER-Y-SPEED 2)
(define TANK-SPEED 4)
(define MISSILE-SPEED -5)
(define HIT-RANGE 10)
(define INVADE-RATE 100)
(define MTS (empty-scene WIDTH HEIGHT))
(define INVADER
(overlay/xy (ellipse 10 15 "outline" "blue") ;cockpit cover
-5 6
(ellipse 20 10 "solid" "blue"))) ;saucer
(define TANK
(overlay/xy (overlay (ellipse 28 8 "solid" "black") ;tread center
(ellipse 30 10 "solid" "green")) ;tread outline
5 -14
(above (rectangle 5 10 "solid" "black") ;gun
(rectangle 20 10 "solid" "black")))) ;main body
(define TANK-HEIGHT/2 (/ (image-height TANK) 2))
(define MISSILE (ellipse 5 15 "solid" "red"))
(define BLANK (square 0 "solid" "white"))
(define TANK-OFFSET (- HEIGHT 15)) ; distance between the tank and the bottom of the frame
(define MISSILE-OFFSET 20)
(define NEW_INVADER_FREQUENCY 1)
;; =================
;; Data Definitions:
;; =================
(define-struct game (invaders missiles tank))
;; Game is (make-game (listof Invader) (listof Missile) Tank)
;; interp. the current state of a space invaders game
;; with the current invaders, missiles and tank position
;; Game constants defined below ListOfMissile data definition
#;
(define (fn-for-game s)
(... (fn-for-loi (game-invaders s))
(fn-for-lom (game-missiles s))
(fn-for-tank (game-tank s))))
(define-struct tank (x dir))
;; Tank is (make-tank Number Integer[-1, 1])
;; interp. the tank location is x, HEIGHT - TANK-HEIGHT/2 in screen coordinates
;; the tank moves TANK-SPEED pixels per clock tick left if dir -1, right if dir 1
(define T0 (make-tank (/ WIDTH 2) 1)) ;center going right
(define T1 (make-tank 50 1)) ;going right
(define T2 (make-tank 50 -1)) ;going left
#;
(define (fn-for-tank t)
(... (tank-x t)
(tank-dir t)))
(define-struct invader (x y dx))
;; Invader is (make-invader Number Number Number)
;; interp. the invader is at (x, y) in screen coordinates
;; the invader along x by dx pixels per clock tick
(define I1 (make-invader 150 100 3)) ;not landed, moving right
(define I2 (make-invader 150 200 -1))
(define I7 (make-invader 150 TANK-OFFSET -1)) ;exactly landed, moving left
(define I8 (make-invader 150 (+ TANK-OFFSET 1) 1)) ;> landed, moving right
; these examples were created to test the advance-invader function:
(define I4 (make-invader 150 100 -12))
(define I5 (make-invader 150 100 0))
; these examples were created to test the game:
(define I6 (make-invader 0 0 -2))
(define END_INVADER (make-invader 0 TANK-OFFSET 2))
(define COLLIDE_INVADER (make-invader 200 50 -2))
#;
(define (fn-for-invader i)
(... (invader-x i)
(invader-y i)
(invader-dx i)))
;; ListOfInvader is one of:
;; - empty
;; - (cons Invader ListofInvader)
;; interp. a list of Invaders present in the current state of the game
(define LOI1 empty)
(define LOI2 (list I1))
(define LOI3 (list I2 I1))
#;
(define (fn-for-loi loi)
(cond [(empty? loi) false]
[else
(... (first loi)
(fn-for-loi (rest loi)))]))
(define-struct missile (x y))
;; Missile is (make-missile Number Number)
;; interp. the missile's location is x y in screen coordinates
(define M1 (make-missile 150 300)) ;not hit U1
(define M2 (make-missile 150 (+ (invader-y I1) 10))) ; exactly hit U1
(define M3 (make-missile 150 (+ (invader-y I1) 5))) ; overhit U1
(define COLLIDE_MISSILE (make-missile 200 50))
#;
(define (fn-for-missile m)
(... (missile-x m)
(missile-y m)))
;; ListOfMissile is one of:
;; - empty
;; - (cons Missile ListOfMissile)
;; interp. a list of Missiles present in the current state of the game
(define LOM1 empty)
(define LOM2 (list M1))
(define LOM3 (list M2 M1))
#;
(define (fn-for-lom lom)
(cond [(empty? lom) false]
[else
(... (first lom)
(fn-for-lom (rest lom)))]))
;; Examples from the data definition for Game earlier on:
(define G0 (make-game empty empty T0))
(define G1 (make-game empty empty T1))
(define G2 (make-game (list I1) (list M1) T2))
(define G3 (make-game (list I1 I2) (list M1 M2) T1))
(define G4 (make-game empty empty T2))
(define END_GAME (make-game (list END_INVADER)
empty
T0))
;; ==========
;; Functions:
;; ==========
(define (main game)
(big-bang game
(on-tick advance-game)
(to-draw render-game)
(on-key shoot-or-scoot)))
;; Game -> Game
;; interp. moves the missiles, invaders, and tanks by preset speeds
;; every tick
;; !!! add function to eliminate colliding missiles and invaders
; (define (advance-game g) G0) ; stub
(check-expect (advance-game G0)
(make-game empty
empty
(advance-tank (game-tank G0)))) ; (game-tank G0) -> T0 -> (make-tank (/ WIDTH 2) 1)
;; (define G3 (make-game (list I1 I2) (list M1 M2) T1))
(check-expect (advance-game G3)
(make-game (advance-loi (game-invaders G3)(game-missiles G3))
(advance-lom (game-invaders G3)(game-missiles G3))
(advance-tank (game-tank G3))))
;; edge case 1: an invader reaches the bottom of the screen. game must end
(check-expect (advance-game END_GAME)
(make-game (game-invaders END_GAME)
(game-missiles END_GAME)
(game-tank END_GAME)))
#;
(define (fn-for-game s)
(... (fn-for-loi (game-invaders s))
(fn-for-lom (game-missiles s))
(fn-for-tank (game-tank s))))
(define (advance-game g)
(cond [(invader-win? (game-invaders g))
(make-game (game-invaders g)
(game-missiles g)
(game-tank g))]
[(<= (random 50) NEW_INVADER_FREQUENCY)
(make-game (advance-loi (append (game-invaders g)
(list (make-invader (random WIDTH)
-5
INVADER-Y-SPEED)))
(game-missiles g))
(advance-lom (game-invaders g)(game-missiles g))
(advance-tank (game-tank g)))]
[(<= (random 100) NEW_INVADER_FREQUENCY)
(make-game (advance-loi (append (game-invaders g)
(list (make-invader (random WIDTH)
-5
(- INVADER-Y-SPEED))))
(game-missiles g))
(advance-lom (game-invaders g)(game-missiles g))
(advance-tank (game-tank g)))]
[else
(make-game (advance-loi (game-invaders g)(game-missiles g))
(advance-lom (game-invaders g)(game-missiles g))
(advance-tank (game-tank g)))]))
;; ListOfInvader -> Boolean
;; interp. returns true if an Invader in the ListOfInvader has reached the
;; bottom of the frame
;; (define (invader-win? LOI1) true) ; stub
(check-expect (invader-win? empty) false)
(check-expect (invader-win? (list END_INVADER)) true)
(check-expect (invader-win? (list I1)) false)
(check-expect (invader-win? (list I1 I6)) false)
(check-expect (invader-win? (list I1 I6 END_INVADER)) true)
#;
(define (fn-for-loi loi)
(cond [(empty? loi) false]
[else
(... (first loi)
(fn-for-loi (rest loi)))]))
(define (invader-win? loi)
(cond [(empty? loi) false]
[(>= (invader-y (first loi))
TANK-OFFSET) true]
[else
(invader-win? (rest loi))]))
;; ListOfInvader ListOfMissile -> ListOfInvader
;; interp. moves a list of invaders 45 degrees downwards at invader-dx speed
;; or removes invaders which collide with missiles
;; !!! add functionality which randomly generates new invaders
;; (define (advance-loi loi lom) empty) ; stub
(check-expect (advance-loi empty empty) empty)
(check-expect (advance-loi (list I1) empty)
(list (advance-invader I1)))
(check-expect (advance-loi (list I2 I1) empty)
(list (advance-invader I2)
(advance-invader I1)))
(check-expect (advance-loi (list COLLIDE_INVADER I1 I2)
(list COLLIDE_MISSILE))
(list (advance-invader I1)
(advance-invader I2)))
#;
(define (fn-for-loi loi)
(cond [(empty? loi) false]
[else
(... (first loi)
(fn-for-loi (rest loi)))]))
(define (advance-loi loi lom)
(cond [(empty? loi) empty]
[(collide-invader? (first loi) lom)
(advance-loi (rest loi) lom)]
[else
(cons (advance-invader (first loi))
(advance-loi (rest loi) lom))]))
;; Invader ListOfMissile -> Boolean
;; interp. returns true if the invader collides with any missile
;; in the ListOfMissile
;; (define (collide-invader? I1 LOM1) false) ; stub
(check-expect (collide-invader? COLLIDE_INVADER
empty)
false)
(check-expect (collide-invader? COLLIDE_INVADER
(list (make-missile 40 40)))
false)
(check-expect (collide-invader? COLLIDE_INVADER
(list COLLIDE_MISSILE))
true)
(check-expect (collide-invader? COLLIDE_INVADER
(list (make-missile 40 40)
(make-missile 50 50)))
false)
(check-expect (collide-invader? COLLIDE_INVADER
(list (make-missile 40 40)
COLLIDE_MISSILE))
true)
(check-expect (collide-invader? COLLIDE_INVADER
(list (make-missile 40 40)
(make-missile 50 50)
COLLIDE_MISSILE
(make-missile 60 60)))
true)
(check-expect (collide-invader? (make-invader 55 55 -1)
(list (make-missile 40 40)
(make-missile 50 50)
COLLIDE_MISSILE
(make-missile 60 60)))
true)
#;
(define (fn-for-lom lom)
(cond [(empty? lom) false]
[else
(... (first lom)
(fn-for-lom (rest lom)))]))
(define (collide-invader? i lom)
(cond [(empty? lom) false]
; ensure that a collision will be registered as long as
; ANY PART of the INVADER image is hit
[(and (<= (- (invader-x i)
(/ (image-width INVADER) 2))
(missile-x (first lom))
(+ (invader-x i)
(/ (image-width INVADER) 2)))
(<= (- (invader-y i)
(/ (image-height INVADER) 2))
(missile-y (first lom))
(+ (invader-y i)
(/ (image-height INVADER) 2))))
true]
[else
(collide-invader? i (rest lom))]))
;; Invader -> Invader
;; interp. moves ONE invader 45 degrees downwards at invader-dx speed
;; if the invader hits a wall, it bounces off at a 45 degree angle
;; and continues in the opposite direction
;; (define (advance-invader invader) I1) ; stub
(check-expect (advance-invader I4) ; invader moving left, -ve invader-dx
(make-invader (+ (invader-x I4)
(invader-dx I4))
(- (invader-y I4)
(invader-dx I4))
(invader-dx I4)))
(check-expect (advance-invader I1) ; invader moving right, +ve invader-dx
(make-invader (+ (invader-x I1)
(invader-dx I1))
(+ (invader-y I1)
(invader-dx I1))
(invader-dx I1)))
(check-expect (advance-invader I5) ; invader is stationary, invader-dx = 0
(make-invader (invader-x I5)
(invader-y I5)
(invader-dx I5)))
; edge case 1a: invader moves left and hits left side of screen, needs to change direction
(check-expect (advance-invader (make-invader 0 100 -12))
(make-invader 0 100 12))
; edge case 1b: invader moves right from the left side of the screen
(check-expect (advance-invader (make-invader 0 100 12))
(make-invader 12 112 12))
; edge case 2: invader moves right and hits right side of screen, needs to change direction
(check-expect (advance-invader (make-invader WIDTH 100 12))
(make-invader WIDTH 100 -12))
#;
(define (fn-for-invader invader)
(... (invader-x invader)
(invader-y invader)
(invader-dx invader)))
(define (advance-invader i)
(cond [(or (and (<= -5
(invader-x i)
0)
(< (invader-dx i) 0)) ; invader hits left side of screen (with a 5 pixel buffer)
(and (<= WIDTH
(invader-x i)
(+ WIDTH 5))
(> (invader-dx i) 0))) ; invader hits right side of screen (with a 5 pixel buffer)
(make-invader
(invader-x i)
(invader-y i)
(- (invader-dx i)))]
[(< (invader-dx i) 0) ; invader moving left, -ve invader-dx
(make-invader
(+ (invader-x i)
(invader-dx i))
(- (invader-y i)
(invader-dx i))
(invader-dx i))]
[(> (invader-dx i) 0) ; invader moving right, +ve invader-dx
(make-invader
(+ (invader-x i)
(invader-dx i))
(+ (invader-y i)
(invader-dx i))
(invader-dx i))]
[else ; invader remains still, invader-dx = 0
(make-invader
(invader-x i)
(invader-y i)
(invader-dx i))]))
;; ListOfInvaders ListOfMissiles -> ListOfMissiles
;; interp. moves a list of missiles upwards onscreen by MISSILE-SPEED after every tick
;; !!! add functionality which removes a missile which collides with an invader
;; !!! add functionality which removes a missile once it goes offscreen
;; (define (advance-lom loi lom) empty) ; stub
(check-expect (advance-lom empty (list M1))
(list (advance-missile M1)))
(check-expect (advance-lom empty (list M2 M1))
(list (advance-missile M2)
(advance-missile M1)))
#;
(define (fn-for-lom lom)
(cond [(empty? lom) false]
[else
(... (first lom)
(fn-for-lom (rest lom)))]))
(define (advance-lom loi lom)
(cond [(empty? lom) empty]
[(collide-missiles? (first lom) loi)
(advance-lom loi (rest lom))]
[else
(cons (advance-missile (first lom))
(advance-lom loi (rest lom)))]))
;; Missile ListOfInvaders -> Boolean
;; returns true if the missile collides with any invader
;; in the ListOfInvaders
;; !!!
;; (define (collide-missiles? M1 LOI1) false) ; stub
(check-expect (collide-missiles? COLLIDE_MISSILE empty) false)
(check-expect (collide-missiles? COLLIDE_MISSILE
(list (make-invader 40 40 1)))
false)
(check-expect (collide-missiles? COLLIDE_MISSILE
(list COLLIDE_INVADER))
true)
(check-expect (collide-missiles? COLLIDE_MISSILE
(list (make-invader 40 40 1)
(make-invader 50 50 1)))
false)
(check-expect (collide-missiles? COLLIDE_MISSILE
(list (make-invader 40 40 1)
(make-invader 50 50 1)
COLLIDE_INVADER
(make-invader 60 60 1)))
true)
(check-expect (collide-missiles? (make-missile 55 55)
(list (make-invader 40 40 1)
(make-invader 50 50 1)
COLLIDE_INVADER
(make-invader 60 60 1)))
true)
#;
(define (fn-for-loi loi)
(cond [(empty? loi) false]
[else
(... (first loi)
(fn-for-loi (rest loi)))]))
(define (collide-missiles? m loi)
(cond [(empty? loi) false]
; ensure that a collision will be registered as long as
; ANY PART of the INVADER image is hit
[(and (<= (- (invader-x (first loi))
(/ (image-width INVADER) 2))
(missile-x m)
(+ (invader-x (first loi))
(/ (image-width INVADER) 2)))
(<= (- (invader-y (first loi))
(/ (image-height INVADER) 2))
(missile-y m)
(+ (invader-y (first loi))
(/ (image-height INVADER) 2))))
true]
[else
(collide-missiles? m (rest loi))]))
;; Missile -> Missile
;; interp. moves ONE missile upwards onscreen by MISSILE-SPEED after every tick
; (define (advance-missile missile) M1) ; stub
(check-expect (advance-missile M1)
(make-missile (missile-x M1)
(+ (missile-y M1)
MISSILE-SPEED)))
(check-expect (advance-missile M2)
(make-missile (missile-x M2)
(+ (missile-y M2)
MISSILE-SPEED)))
#;
(define (fn-for-missile m)
(... (missile-x m)
(missile-y m)))
(define (advance-missile m)
(make-missile (missile-x m)
(+ (missile-y m)
MISSILE-SPEED)))
;; Tank -> Tank
;; interp. moves a given tank horizontally by tank-dir pixels every tick
;; (define (advance-tank tank) T0) ; stub
(check-expect (advance-tank T0)
(make-tank (+ (tank-x T0)
(tank-dir T0))
(tank-dir T0)))
(check-expect (advance-tank T1)
(make-tank (+ (tank-x T1)
(tank-dir T1))
(tank-dir T1)))
; edge case 1: tank hits the left side of the screen. it should should stop moving
; and remain within the frame
(check-expect (advance-tank (make-tank 15 -5))
(make-tank 15 -5))
; edge case 2: tank hits the right side of the screen. it should should stop moving
; and remain within the frame
(check-expect (advance-tank (make-tank (- WIDTH 15) 5))
(make-tank (- WIDTH 15) 5))
#;
(define (fn-for-tank t)
(... (tank-x t)
(tank-dir t)))
(define (advance-tank t)
(if (or (and (<= (tank-x t) 15)
(< (tank-dir t) 0))
(and (>= (tank-x t) (- WIDTH 15))
(> (tank-dir t) 0)))
(make-tank (tank-x t)
(tank-dir t))
(make-tank (+ (tank-x t)
(tank-dir t))
(tank-dir t))))
;; Game -> Image
;; interp. produces an image based on the existing state of a
;; given Game.
;; (define (render-game G0) BLANK) ; stub
(check-expect (render-game G0)
(render-tank (game-tank G0) MTS))
(check-expect (render-game G3)
(render-invaders (game-invaders G3)
(render-missiles (game-missiles G3)
(render-tank (game-tank G1)
MTS))))
#;
(define (fn-for-game s)
(... (fn-for-loi (game-invaders s))
(fn-for-lom (game-missiles s))
(fn-for-tank (game-tank s))))
(define (render-game g)
(render-invaders (game-invaders g)
(render-missiles (game-missiles g)
(render-tank (game-tank g)
MTS))))
;; ListOfInvaders Image -> Image
;; interp. produces an image of the invaders present in the current state of the game
;; (define (render-invaders LOI1 BLANK) BLANK) ; stub
(check-expect (render-invaders empty MTS)
MTS)
(check-expect (render-invaders (list I1 I2) MTS)
(render-invader I1 (render-invader I2 MTS)))
#;
(define (fn-for-loi loi)
(cond [(empty? loi) false]
[else
(... (first loi)
(fn-for-loi (rest loi)))]))
(define (render-invaders loi img)
(cond [(empty? loi) img]
[else
(render-invader (first loi)
(render-invaders (rest loi) img))]))
;; Invader Image -> Image
;; interp. produces an image of an Invader based on the Invader's data
;; (define (render-invader I1 MTS) MTS) ; stub
(check-expect (render-invader I1 MTS)
(place-image INVADER
(invader-x I1)
(invader-y I1)
MTS))
#;
(define (fn-for-invader i)
(... (invader-x i)
(invader-y i)
(invader-dx i)))
(define (render-invader i img)
(place-image INVADER
(invader-x i)
(invader-y i)
img))
;; ListOfMissiles Image -> Image
;; interp. produces an image of the missiles present in the current state of the game
;; (define (render-missiles LOM1 BLANK) BLANK) ; stub
(check-expect (render-missiles LOM1 MTS)
MTS)
(check-expect (render-missiles LOM2 MTS)
(render-missile M1 MTS))
(check-expect (render-missiles LOM3 MTS)
(render-missile M2
(render-missile M1 MTS)))
#;
(define (fn-for-lom lom)
(cond [(empty? lom) false]
[else
(... (first lom)
(fn-for-lom (rest lom)))]))
(define (render-missiles lom img)
(cond [(empty? lom) img]
[else
(render-missile (first lom)
(render-missiles (rest lom) img))]))
;; Missile Image -> Image
;; interp. produces an image of an Missile based on the Missile's data
;; (define (render-missile M1 MTS) MTS) ; stub
(check-expect (render-missile M1 MTS)
(place-image MISSILE
(missile-x M1)
(missile-y M1)
MTS))
(check-expect (render-missile M2 MTS)
(place-image MISSILE
(missile-x M2)
(missile-y M2)
MTS))
#;
(define (fn-for-missile m)
(... (missile-x m)
(missile-y m)))
(define (render-missile m img)
(place-image MISSILE
(missile-x m)
(missile-y m)
img))
;; Tank Image -> Image
;; interp. produces an image of the tank based on its position in the current state of the game
;; (define (render-tank T0 MTS) MTS) ; stub
(check-expect (render-tank T1 MTS)
(place-image TANK
(tank-x T1)
TANK-OFFSET
MTS))
(check-expect (render-tank T2 MTS)
(place-image TANK
(tank-x T2)
TANK-OFFSET
MTS))
#;
(define (fn-for-tank t)
(... (tank-x t)
(tank-dir t)))
(define (render-tank t img)
(place-image TANK
(tank-x t)
TANK-OFFSET
img))
;; Game KeyEvent -> Game
;; interp. changes the state of specific assets when specific keys are pressed
;; helper function 1:
;; - produces missile when spacebar is pressed
;; helper function 2:
;; - changes the direction of the tank when arrow keys are pressed
;; (define (shoot-or-scoot G0 ke) G0) ; stub
(check-expect (shoot-or-scoot G0 " ")
(make-game (game-invaders G0)
(cons (make-missile (tank-x (game-tank G0))
(- TANK-OFFSET MISSILE-OFFSET))
(game-missiles G0))
(game-tank G0)))
(check-expect (shoot-or-scoot G0 "a")
(make-game (game-invaders G0)
(game-missiles G0)
(game-tank G0)))
(check-expect (shoot-or-scoot G1 "left") ; moving right
(make-game (game-invaders G1)
(game-missiles G1)
(change-tank-dir (game-tank G1))))
(check-expect (shoot-or-scoot G1 "right") ; moving right
(make-game (game-invaders G1)
(game-missiles G1)
(game-tank G1)))
(check-expect (shoot-or-scoot G2 "left") ; moving left
(make-game (game-invaders G2)
(game-missiles G2)
(game-tank G2)))
(check-expect (shoot-or-scoot G2 "right") ; moving left
(make-game (game-invaders G2)
(game-missiles G2)
(change-tank-dir (game-tank G2))))
#;
(define (fn-for-key-event kevt)
(cond [(key=? " " kevt) (...)]
[else
(...)]))
(define (shoot-or-scoot g ke)
(cond [(key=? " " ke) ; shoot
(make-game (game-invaders g)
(cons (make-missile (tank-x (game-tank g))
(- TANK-OFFSET MISSILE-OFFSET))
(game-missiles g))
(game-tank g))]
[(or ; scoot
(and (key=? "left" ke)
(> (tank-dir (game-tank g)) 0))
(and (key=? "right" ke)
(< (tank-dir (game-tank g)) 0)))
(make-game (game-invaders g)
(game-missiles g)
(change-tank-dir (game-tank g)))]
[else
(make-game (game-invaders g)
(game-missiles g)
(game-tank g))]))
;; Tank -> Tank
;; interp. changes the direction of the tank
;; (define (change-tank-dir T0) T0) ; stub
(check-expect (change-tank-dir T1)
(make-tank 50 -1))
(check-expect (change-tank-dir T2)
(make-tank 50 1))
(check-expect (change-tank-dir (make-tank 50 0))
(make-tank 50 0))
#;
(define (fn-for-tank t)
(... (tank-x t)
(tank-dir t)))
(define (change-tank-dir t)
(make-tank (tank-x t)
(- (tank-dir t))))
;; ======================
;; MAIN PROGRAM EXECUTION
;; ======================
(main (make-game empty
empty
(make-tank (/ WIDTH 2) TANK-SPEED)))