Created
July 7, 2018 22:28
-
-
Save fswalker/c4947813602f9fce5db319905fde81ae to your computer and use it in GitHub Desktop.
Space invaders game written in Racket
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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