Skip to content

Instantly share code, notes, and snippets.

@fswalker
Created July 7, 2018 22:28
Show Gist options
  • Save fswalker/c4947813602f9fce5db319905fde81ae to your computer and use it in GitHub Desktop.
Save fswalker/c4947813602f9fce5db319905fde81ae to your computer and use it in GitHub Desktop.
Space invaders game written in Racket
(require 2htdp/universe)
(require 2htdp/image)
;; Space Invaders
;; Constants:
(define WIDTH 300)
(define HEIGHT 500)
(define INVADER-X-SPEED 1.5) ;speeds (not velocities) in pixels per tick
(define INVADER-Y-SPEED 1.5)
(define TANK-SPEED 2)
(define MISSILE-SPEED 10)
(define HIT-RANGE 10)
(define INVADE-RATE 100)
(define INVADE-TRESHOLD 10)
(define BACKGROUND (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 MISSILE-HEIGHT/2 (/ (image-height MISSILE) 2))
(define GAME-OVER
(text/font "GAME OVER" 48 "red"
"Uroob" 'modern 'normal 'bold #f))
(define EMPTY-LIST-IMG (rectangle WIDTH HEIGHT "outline" "transparent"))
;; 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 Missile data definition
#;
(define (fn-for-game s)
(... (fn-for-loinvader (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 12)) ;not landed, moving right
(define I2 (make-invader 150 HEIGHT -10)) ;exactly landed, moving left
(define I3 (make-invader 150 (+ HEIGHT 10) 10)) ;> landed, moving right
#;
(define (fn-for-invader invader)
(... (invader-x invader) (invader-y invader) (invader-dx invader)))
(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 (invader-x I1) (+ (invader-y I1) 10))) ;exactly hit U1
(define M3 (make-missile (invader-x I1) (+ (invader-y I1) 5))) ;> hit U1
#;
(define (fn-for-missile m)
(... (missile-x m) (missile-y m)))
(define G0 (make-game empty empty T0))
(define G1 (make-game empty empty T1))
(define G2 (make-game (list I1) (list M1) T1))
(define G3 (make-game (list I1 I2) (list M1 M2) T1))
(define G7 (make-game (list I1 I2 (make-invader 50 (- HEIGHT 100) 10)) (list M1 M2) T1))
;; =================
;; Functions:
;; Game -> Game
;; start the world with (main G0)
;;
(define (main game)
(big-bang game ; Game
(on-tick update-game) ; Game -> Game
(to-draw render-game) ; Game -> Image
(stop-when game-over? final-screen) ; (Game -> Boolean) -> (Game -> Image)
(on-key handle-key))) ; Game KeyEvent -> Game
;; Game -> Game
;; produce the next Game on tick
;; Move aliens, move missiles, hide destroyed aliens and missiles or beyond the limits of screen
(define (update-game g)
(make-game
(update-invaders g)
(update-missiles g)
(update-tank (game-tank g))))
;; Game -> Image
;; render game - tank, missiles, aliens
(define (render-game g)
(overlay
(redner-invaders (game-invaders g))
(redner-missiles (game-missiles g))
(render-tank (game-tank g) BACKGROUND)))
;; Game -> Boolean
;; decide if the game is over
(define (game-over? g)
(invader-landed? (game-invaders g)))
;; list of Invaders -> Boolean
;; Return true if any invader landed on Earth
(define (invader-landed? loi)
(cond [(empty? loi) false]
[else
(if (> (invader-y (first loi)) HEIGHT)
true
(invader-landed? (rest loi)))]))
;; Game -> Image
;; display last screen of the game
(define (final-screen g)
(overlay
GAME-OVER
(render-game g)))
;; Game KeyEvent -> Game
;; handle key press
(define (handle-key g ke)
(cond [(key=? ke " ") (fire-missile g)]
[(key=? ke "left") (move-tank-left g)]
[(key=? ke "right") (move-tank-right g)]
[else g]))
;;
;;
;; Helper functions
;;
;;
;; Tank -> Tank
;; Update tank position
(define (update-tank t)
(make-tank
(fix-tank-x
(calculate-tank-x t))
(tank-dir t)))
;; Game -> listof Invaders
;; Update list of invaders based on game
;; move all invaders first
;; remove invaders which got shot based on moved missiles
(define (update-invaders g)
(remove-destroyed-invaders
(move-missiles (game-missiles g))
(move-invaders
(add-invader-randomly
(game-invaders g)))))
;; listof Missile -> listof Invader -> listof Invader
;; Filter out invaders destroyed by missiles
(define (remove-destroyed-invaders lom loi)
(cond [(empty? loi) empty]
[else
(if (not (invader-exploded? lom (first loi)))
(cons (first loi) (remove-destroyed-invaders lom (rest loi)))
(remove-destroyed-invaders lom (rest loi)))]))
;; listof Missile -> Invader -> Boolean
;; Decide whether missile hit any invader
(define (invader-exploded? lom i)
(cond [(empty? lom) false]
[else
(if (missile-hit-invader? (first lom) i)
true
(invader-exploded? (rest lom) i))]))
;; listof Invader -> listof Invader
;; Adds random invader when the random number is less than 20% of the defined range
;; Random x and random direction should be defined
(define (add-invader-randomly loi)
(if (< (random INVADE-RATE) INVADE-TRESHOLD)
(cons
(make-invader
(random WIDTH)
(- 0 (image-height INVADER))
(if
(= 0 (random 2))
INVADER-X-SPEED
(- 0 INVADER-X-SPEED)))
loi)
loi))
;; listof Invader -> listof Invader
;; Move all invaders; when invader reaches the border, it should change direction
(define (move-invaders loi)
(cond [(empty? loi) empty]
[else (cons
(move-invader (first loi))
(move-invaders (rest loi)))]))
;; Invader -> Invader
;; Move Invader liearly in x and y directions, when hit left or right border, change direction
(check-expect (move-invader (make-invader 10 10 INVADER-X-SPEED))
(make-invader (+ 10 INVADER-X-SPEED) (+ 10 INVADER-Y-SPEED) INVADER-X-SPEED))
(check-expect (move-invader (make-invader (- WIDTH INVADER-X-SPEED) 50 INVADER-X-SPEED))
(make-invader WIDTH (+ 50 INVADER-Y-SPEED) (- 0 INVADER-X-SPEED)))
(check-expect (move-invader (make-invader (- WIDTH INVADER-X-SPEED) 50 INVADER-X-SPEED))
(make-invader WIDTH (+ 50 INVADER-Y-SPEED) (- 0 INVADER-X-SPEED)))
(check-expect (move-invader (make-invader (* 2 INVADER-X-SPEED) 50 (- 0 INVADER-X-SPEED)))
(make-invader INVADER-X-SPEED (+ 50 INVADER-Y-SPEED) (- 0 INVADER-X-SPEED)))
(check-expect (move-invader (make-invader INVADER-X-SPEED 10 (- 0 INVADER-X-SPEED)))
(make-invader 0 (+ 10 INVADER-Y-SPEED) INVADER-X-SPEED))
(check-expect (move-invader (make-invader (/ INVADER-X-SPEED 2) 10 (- 0 INVADER-X-SPEED)))
(make-invader (/ INVADER-X-SPEED 2) (+ 10 INVADER-Y-SPEED) INVADER-X-SPEED))
(define (move-invader i)
(adjust-position
(make-invader
(+ (invader-x i) (invader-dx i))
(+ (invader-y i) INVADER-Y-SPEED)
(invader-dx i))))
;; Invader -> Invader
;; Change direction and adjust x position when invader went too far
(define (adjust-position i)
(cond [(>= (invader-x i) WIDTH)
(make-invader
(- WIDTH (- (invader-x i) WIDTH))
(invader-y i)
(- 0 (invader-dx i)))]
[(<= (invader-x i) 0)
(make-invader
(- 0 (invader-x i))
(invader-y i)
(- 0 (invader-dx i)))]
[else i]))
;; Game -> listof Missiles
;; Update list of missiles based on game
;; move all missiles first
;; remove missiles which shot (firstly moved) invaders
(check-expect (update-missiles G0) empty)
(check-expect (update-missiles (make-game (list I1) (list M1) T1))
(list (make-missile
(missile-x M1)
(- (missile-y M1) MISSILE-SPEED))))
(check-expect (update-missiles (make-game (list I1 (make-invader 200 200 10))
(list M1 (make-missile 200 230)) T1))
(list
(make-missile
(missile-x M1)
(- (missile-y M1) MISSILE-SPEED))
(make-missile 200 (- 230 MISSILE-SPEED))))
; is exactly in the hit range after move
(check-expect (update-missiles (make-game (list I1 (make-invader 200 200 10))
(list M1 (make-missile 200 220)) T1))
(list
(make-missile
(missile-x M1)
(- (missile-y M1) MISSILE-SPEED))))
; after move is < hit range
(check-expect (update-missiles (make-game (list I1 (make-invader 200 200 10))
(list M1 (make-missile 200 215)) T1))
(list
(make-missile
(missile-x M1)
(- (missile-y M1) MISSILE-SPEED))))
(define (update-missiles g)
(remove-exploded-missiles
(move-invaders (game-invaders g))
(remove-hidden-missiles
(move-missiles (game-missiles g)))))
;; listof Invader -> listof Missile -> listof Missile
;; filter out missiles which reached a target and exploded
(define (remove-exploded-missiles loi lom)
(cond [(empty? lom) empty]
[else
(if (not (missile-exploded? loi (first lom)))
(cons (first lom) (remove-exploded-missiles loi (rest lom)))
(remove-exploded-missiles loi (rest lom)))]))
;; listof Invader -> Missile -> Boolean
;; Decide whether missile hit any invader
(define (missile-exploded? loi m)
(cond [(empty? loi) false]
[else
(if (missile-hit-invader? m (first loi))
true
(missile-exploded? (rest loi) m))]))
;; Invader -> Missile -> Boolean
;; Decide whether Missile hit Invader
(check-expect (missile-hit-invader?
(make-missile 100 100)
(make-invader 100 100 10))
true)
(check-expect (missile-hit-invader?
(make-missile 100 (+ 100 HIT-RANGE))
(make-invader 100 100 10))
true)
(check-expect (missile-hit-invader?
(make-missile (+ 100 HIT-RANGE) (+ 100 HIT-RANGE))
(make-invader 100 100 10))
true)
(check-expect (missile-hit-invader?
(make-missile (+ 100 HIT-RANGE 1) (+ 100 HIT-RANGE))
(make-invader 100 100 10))
false)
(check-expect (missile-hit-invader?
(make-missile 100 (+ 100 HIT-RANGE 1))
(make-invader 100 10 10))
false)
(define (missile-hit-invader? m i)
(and
(<= (abs (- (missile-x m) (invader-x i))) HIT-RANGE)
(<= (abs (- (missile-y m) (invader-y i))) HIT-RANGE)))
;; listof Missile -> listof Missile
;; filter out missiles which flew above the scene
(check-expect (remove-hidden-missiles
(list (make-missile 200 MISSILE-SPEED)))
(list (make-missile 200 MISSILE-SPEED)))
(check-expect (remove-hidden-missiles
(list (make-missile 200 (- 0 MISSILE-HEIGHT/2))))
empty)
(define (remove-hidden-missiles lom)
(cond [(empty? lom) empty]
[else
(if (not (missile-hidden? (first lom)))
(cons (first lom)
(remove-hidden-missiles (rest lom)))
(remove-hidden-missiles (rest lom)))]))
;; Missile -> Boolean
;; Decide if Missile is hidden
(check-expect (missile-hidden?
(make-missile 200 -10))
true)
(check-expect (missile-hidden?
(make-missile 200 (- 0 MISSILE-HEIGHT/2)))
true)
(check-expect (missile-hidden?
(make-missile 200 0))
false)
(check-expect (missile-hidden?
(make-missile 200 10))
false)
(define (missile-hidden? m)
(<= (missile-y m)
(- 0 MISSILE-HEIGHT/2)))
;; listof Missile -> listof Missile
;; Move all existing missiles forward
(check-expect (move-missiles empty) empty)
(check-expect (move-missiles (list M1))
(list (make-missile
(missile-x M1)
(- (missile-y M1) MISSILE-SPEED))))
(define (move-missiles lom)
(cond [(empty? lom) empty]
[else
(cons (move-missile (first lom))
(move-missiles (rest lom)))]))
;; Missile -> Missile
;; Move forward one missile
(check-expect (move-missile (make-missile 100 100))
(make-missile 100 (- 100 MISSILE-SPEED)))
(define (move-missile m)
(make-missile
(missile-x m)
(- (missile-y m) MISSILE-SPEED)))
;; List of Invaders -> Image
;; Render list of Invaders as an image
(define (redner-invaders loi)
(cond [(empty? loi) EMPTY-LIST-IMG]
[else (render-invader (first loi)
(redner-invaders (rest loi)))]))
;; Invader -> Image -> Image
;; Render Invader on the given Image
(define (render-invader inv img)
(place-image
INVADER
(invader-x inv) (invader-y inv)
img))
;; List of Missiles -> Image
;; Render list of Missiles as an image
(define (redner-missiles lom)
(cond [(empty? lom) EMPTY-LIST-IMG]
[else (render-missile (first lom)
(redner-missiles (rest lom)))]))
;; Missile -> Image -> Image
;; Render Missile on the given Image
(define (render-missile m img)
(place-image
MISSILE
(missile-x m) (missile-y m)
img))
;; Tank -> Image -> Image
;; Render tank on the given background image
(define (render-tank t bg)
(place-image
TANK
(tank-x t)
(- HEIGHT TANK-HEIGHT/2)
bg))
;; Game -> Game
;; Add new missile to the list of missiles based on current tank position
(define (fire-missile g)
(make-game
(game-invaders g)
(add-missile
(create-new-missile g)
(game-missiles g))
(game-tank g)))
;; Game -> Missile
;; Create new missile based on the tank position
(define (create-new-missile g)
(make-missile
(tank-x (game-tank g))
(- HEIGHT
(+ (image-height TANK)
MISSILE-HEIGHT/2))))
;; Missile -> listof Missiles -> listof Missiles
;; Add new missile to the list of missiles
(define (add-missile m lom)
(cond [(empty? lom) (cons m empty)]
[else
(cons m lom)]))
;; Game -> Game
;; Move tank to the left
(define (move-tank-left g)
(move-tank g -1))
;; Game -> Game
;; Move tank to the right
(define (move-tank-right g)
(move-tank g 1))
;; Game -> Integer[-1, 1] -> Game
;; Move tank in the given direction for left and right
(define (move-tank g dir)
(cond [(or (= -1 dir) (= 1 dir))
(make-game
(game-invaders g)
(game-missiles g)
(make-tank
(tank-x (game-tank g))
dir))]
[else g]))
;; Tank -> Integer
;; Calculate new Tank position based on its current position and direction
(define (calculate-tank-x t)
(+ (tank-x t)
(* (tank-dir t) TANK-SPEED)))
;; Integer -> Integer
;; Set left or right limit if tank passed any limit
(define (fix-tank-x x)
(cond [(< x 0) 0]
[(> x WIDTH) WIDTH]
[else x]))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment