Tuesday, 12 June 2012

Exercise 171: Modify your worm program so that it stops at the edge of the world

Modify your program from exercise 170 so that it stops if the worm has run into the walls
of the world. When the program stops because of this condition, it should render the final scene with the text "worm hit border" in the lower left of the world scene. Hint: You can use the stop-when clause in big-bang to render the last world in a special way. Challenge: Show the worm in this last scene as if it were on its way out of the box.
Download the code from this post here.
To detect when the worm leaves the box we need to add some form of boundary or collision detection. There is a method in racket/base called image-inside which detects if one image is inside another. This could have been used to determine if the worm was still inside the box but as I didn't see this method until after I had completed my manual boundary detection my implementation doesn't use this. The lesson here is to know your API.

The difficulty involved in the "challenge" is that to show the worm leaving the box the collision detection needs to trigger before the collision occurs. If you don't detect the worm has left the game wold until after it is outside the boundaries then you need to either move the worm backwards after a collision, or forecast when the worm is going to collide.

I choose to forecast the worm collision. This meant the break up of the move-worm function into two separate functions - one that moved the worm, and one that composed a new world object. This allows a separate instance of the worm to be moved forward outside of the context of the world scene. Within the collision-detection function this separate instance of the worm is moved forward and checked see if it would have collided.
In hindsight I suspect that moving the worm backwards after a collision would have easier. This could have been done simply via a function that reverses the direction the worm was travelling and moving it one unit.

Changes from Exercise 170


The on-tick callback in the big-bang function has been changed to call a new function progress-world. The on-tick method previously moved the worm and composed a new world object. I now wanted to be able to move the worm from outside of the big-bang function. The reason for this to help with the 'challenge'  -  we can now call move-worm separately from within our collision detection function to enable us to forecast a future crash. This will leave our worm still on screen at the end of the game.

There is a new collision-detection function that will return true whenever it detects that the worm has hit the side of the box.

There is a new final-scene function that is called when the game is ended. This will display our Game Over text and draw the worm "leaving the playing area".

The big-bang function has been changed to:
  • call the new progress-world function (instead of move-worm) 
  • call collision-detection to determine when the worm has crashed
  • call final-scene to draw the end game scene.

Code



; Constants

(define WORM-SIZE 10)
(define WORM-MOVE WORM-SIZE)
(define WIDTH 800)  ; width of the game

; these structs will hold the current position of the worm, the direction 
; the worm is travelling in, and our world object
(define-struct worm(x-pos y-pos))
(define-struct direction(x y))
(define-struct world(worm direction))

; To save repeating these directions in tests and code, we'll define them here
(define DOWN  (make-direction 0 1))
(define UP    (make-direction 0 -1))
(define RIGHT (make-direction  1 0))
(define LEFT  (make-direction -1 0))
(define HEIGHT 500) ; height of the game


; Functions 

; draw worm in its current location on the screen
(define (draw-worm background worm)
  (place-image (circle WORM-SIZE "solid" "red") 
               (worm-x-pos worm) (worm-y-pos worm) background))
    
; check if the snake has hit the edge of the world.
; return true if collision detected, false otherwise
(define (collision-detected world)
  (collision-detected-helper (move-worm world)))

; helper function for collision detection. Operates directly
; on a worm, rather than world object.
(define (collision-detected-helper worm)
 (cond [(> 0        (worm-x-pos worm)) true]  ; exceeding left edge
        [(> 0       (worm-y-pos worm)) true]  ; exceeding top edge
        [(< WIDTH   (worm-x-pos worm)) true]  ; exceeding right edge
        [(< HEIGHT  (worm-y-pos worm)) true]  ; exceeding bottom edge
        [else false]))

; Draw our final scene with the worm departing the board
; Displays a "Game Over" type message. We should probably be calculating the 
; width and height of the image to calculate the offsets, but it's simpler just 
; to arbitrarily put it somewhere in the bottom right of the screen
(define (final-scene world)
  (draw-worm
   (place-image (text "worm hit border" 20 "red")
                (- WIDTH 100)
                (- HEIGHT 50)
                (empty-scene WIDTH HEIGHT))
   (world-worm world)))

; Draws the current world. Currently this just consists of the snake.
(define (show world)
  (draw-worm (empty-scene WIDTH HEIGHT) (world-worm world)))

; Move the worm in the current direction. 
; This function is called on each clock tick and moves the worm in the 
; last direction that was pressed. It is also called from collision-detection
; to determine where the worm will be next turn.
(define (move-worm world)
   (make-worm  (+ (worm-x-pos (world-worm world)) 
                  (* WORM-MOVE (direction-x (world-direction world))))
               (+ (worm-y-pos (world-worm world)) 
                  (* WORM-MOVE (direction-y (world-direction world))))))

; On each clock tick, move the world further in time. This is a new function
; that takes part of the responsibily of the old move-worm function. It just
; moves the worm and creates a new world based on it.
(define (progress-world world)
   (make-world
    (move-worm  world)
    (world-direction world)))

; handle keyboard events. 
(define (handle-key-events ws ke)
  (cond
    [(string=? "left" ke)  (change-direction ws LEFT)]
    [(string=? "right" ke) (change-direction ws RIGHT)]
    [(string=? "up" ke)    (change-direction ws UP )]
    [(string=? "down" ke)  (change-direction ws DOWN)]
    [else ws]
  ))

; create a new world with the direction the worm is travelling in changed.
(define (change-direction world  direction)
  (make-world (world-worm world) direction))

; This is the big bang function that drives the game.
(define (worm-main rate)
  (big-bang (make-world (make-worm (/ WIDTH 2)  (/ HEIGHT 2))
                        (make-direction 1 0))
            (to-draw    show)
            (stop-when  collision-detected final-scene)
            (on-key     handle-key-events)
            (on-tick    progress-world rate) ))

; start the game off! 
(worm-main 0.1)


; ### NEW TESTS ###

; exceeding the bottom of the screen
(check-expect (collision-detected (make-world 
                                    (make-worm (- WIDTH 10) (+ HEIGHT 10)) RIGHT))
              true)
; exceeding the right side of the scren
(check-expect (collision-detected (make-world 
                                    (make-worm (+ WIDTH 10) (- HEIGHT 10)) RIGHT))
              true)

; exceeding the top of the screen
(check-expect (collision-detected (make-world 
                                    (make-worm -10 (- HEIGHT 10)) LEFT))
              true)
; exceeding the left side of the screen
(check-expect (collision-detected (make-world 
                                    (make-worm (- WIDTH 10) -10) LEFT))
              true)

;in the middle of the screen - should not collide
(check-expect (collision-detected (make-world 
                                    (make-worm (- WIDTH 10) (- HEIGHT 10)) LEFT))
              false)


; ### OLD TESTS

; Test our worm draws as we expect it.
(check-expect (draw-worm (empty-scene 100 100) (make-worm 50 50))
              (place-image (circle WORM-SIZE "solid" "red") 50 50 (empty-scene 100 100)))

; Test our worm moves in the direction we expect
(check-expect (move-worm (make-world (make-worm 50 50) DOWN))
              (make-worm 50 60))
(check-expect (move-worm (make-world (make-worm 50 50) UP))
              (make-worm 50 40))
(check-expect (move-worm (make-world (make-worm 50 50) LEFT))
              (make-worm 40 50))
(check-expect (move-worm (make-world (make-worm 50 50) RIGHT))
              (make-worm 60 50))

; Test our change-direction function changes the direction, but doesn't impact the postion
(check-expect (change-direction (make-world (make-worm 50 50) DOWN) UP)
              (make-world (make-worm 50 50) UP))
(check-expect (change-direction (make-world (make-worm 50 50) DOWN) RIGHT)
              (make-world (make-worm 50 50) RIGHT))   

No comments:

Post a Comment