Wednesday, 27 June 2012

Exercise 173: Re-design your program so that it stops if the worm has run into the walls of the world or into itself.


Re-design your program from exercise 172 so that it stops if the worm has run into the walls of the world or into itself. Display a message like the one in exercise 171 to explain whether the program stopped because the worm hit the wall or because it ran into itself.

Code for this exercise is available here.

Only fairly straight forward changes were required to accomodate this new iteration. Changes needed to be  made to the collision-detection function and the final-scene function.

The collision-detection function now needs to check for both collisions with the wall and collisions with the tail of the worm. By breaking this method into two independent functions; one checking for wall collisions and one checking for tail collisions, I was able to re-use the code for the wall collisions in final-scene function for determining which message to display - either "You have hit the wall", or "You have hit yourself".

Although we have a multi-segmented worm now, the collision detection functions didn't need to be drastically modified as it only needs to check the head of the worm. We can just pop off the first segment and use this to check for collisions. For worm tail collisions we use the member? function as specified in the hint.

For the final-scene function the only change was to determine which message to display. As the collision-detection function only returns true or false we can't use this directly. By calling the indivdiual functions within this method (ie collision-detection-wall) we can determine the method. There is no need to check for collisions with the worm, as if the worm has collided with something and it was not the wall, then it must have collided with itself.

Code



; Constants


(define WORM-SIZE 10)
(define WORM-MOVE (* WORM-SIZE 2))
(define WIDTH 800)  ; width of the game
(define HEIGHT 500) ; height of the game
(define SEGMENT (circle WORM-SIZE "solid" "red"))                              


; these structs hold the current list of worm segments, the direction
; the worm is travelling in, and our world object
(define-struct segment(x-pos y-pos))
(define-struct direction(x y))
(define-struct world(worm direction))
(define WORM (list (make-segment 100 100)
                   (make-segment 100 80)
                   (make-segment 100 60)) )      


; 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))


; Functions


; draw worm in its current location on the screen. This function recurses
; down our list of worm segments drawing each one at a time on to the background
; passed in
(define (draw-worm background worm)
  (cond [(empty? worm) background]
        [else (place-image
               SEGMENT
               (segment-x-pos (first worm)) (segment-y-pos (first worm))
               (draw-worm background (rest worm))
              )]))




This function has changed for this exercise so that instead of just checking for a collision with the wall, we check for a collision with the worm as well. This is accomplished by calling two separate collision functions - one for the wall, and one for the worm. The reason this has been split up is so that we can use the same function to determine which end of game message should be displayed. This function could be trivially extended to check for collisions with objects in the room.




; Check for collisions with either the walls or the rest of the worm.
; return true if collision detected, false otherwise
(define (collision-detected world)
  (or
   (collision-detected-wall (first (move-worm-helper world )))
   (collision-detected-worm (first (move-worm-helper world)) 
                            (world-worm world ))))





; a helper function for checking for collision with the worm. 
; This works by checking if area the worm would move into is already in the
; list of worm segments
; return true if collision detected, false otherwise
(define (collision-detected-worm segment worm)
  (member? segment worm))


; a helper function for collision detection with the wall
; returns true if a collision is detected, false otherwise
(define (collision-detected-wall segment)
 (cond [(> 0        (segment-x-pos segment)) true]  ; exceeding left edge
        [(> 0       (segment-y-pos segment)) true]  ; exceeding top edge
        [(< WIDTH   (segment-x-pos segment)) true]  ; exceeding right edge
        [(< HEIGHT  (segment-y-pos segment)) true]  ; exceeding bottom edge
        [else false]))




The final-scene method also changed for this exercise. To display the correct end of game message we need to determine if we've collided with the worm or the wall.
We can use the collision-detected-wall method to determine if the game finished because we hit the wall. If it didn't finish because of that, it must have finished because we hit the rest of the worm.


; 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
                  (cond  ((collision-detected-wall 
                            (first (move-worm-helper world )))
                          "worm hit border" )
                         (else "worm hit worm"))
               
                 20 "red")
                (- WIDTH 100)
                (- HEIGHT 50)
                (empty-scene WIDTH HEIGHT))
   (world-worm world)))




; Draws the current world. This consists of the snake and the food objects
(define (show world)
  (draw-worm (empty-scene WIDTH HEIGHT) (world-worm world)) )


; Move the worm in the current direction. To move the worm we add a segment 
; to start of the worm (in the current direction) and get rid of the end of
; the worm
(define (move-worm worm direction)
   (cons (new-segment (first worm) direction) (remove-last worm)))




; helper method to DRY up code.
(define (move-worm-helper world)
  (move-worm (world-worm world) (world-direction world)))


; returns a new segment moved in 'direction' from the segment
; passed in
(define (new-segment segment direction)
    (make-segment  (+ (segment-x-pos segment)
                  (* WORM-MOVE (direction-x direction)))
               (+ (segment-y-pos segment)
                  (* WORM-MOVE (direction-y direction)))))




; remove the last worm segment. this is a pretty unoptimised function. just
; reverse the list, grab the rest of it, and reverse it again to get it the
; correct order. 
(define (remove-last worm)
   (reverse (rest (reverse worm))))






; On each clock tick, move the world further in time. It moves the worm
; and creates a new world based on this.
(define (progress-world world)
   (make-world
    (move-worm  (world-worm world)  (world-direction 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 WORM
                        (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)




; TESTS


; Test when we move the worm up, a new segment is added to the start and removed
; from the end.
(check-expect (move-worm WORM LEFT)
              (list
               (make-segment (- 100 WORM-MOVE) 100)
               (make-segment 100 100)
               (make-segment 100 80)))


; Check the remove-last function removes the last segment correctly
(check-expect (remove-last WORM)
              (list (make-segment 100 100)
                   (make-segment 100 80)))


; Test that new segment returns a new segment in the correct position
; (as per the current direction)
(check-expect (new-segment  (make-segment 100 100) UP)
              (make-segment 100 (- 100 WORM-MOVE)))






;; 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)
;


;
;; Test our worm draws as we expect it.
(check-expect (draw-worm (empty-scene 200 200) WORM)
              (place-image SEGMENT 100 100
                           (place-image SEGMENT 100 80
                                        (place-image SEGMENT 100 60 
                                            (empty-scene 200 200)))))
             




; Test our worm moves in the direction we expect
(check-expect (move-worm (list (make-segment 50 50)) DOWN)
              (list (make-segment 50 70)))
(check-expect (move-worm (list (make-segment 50 50)) UP)
              (list (make-segment 50 30)))
(check-expect (move-worm (list (make-segment 50 50)) LEFT)
              (list (make-segment 30 50)))
(check-expect (move-worm  (list (make-segment 50 50)) RIGHT)
              (list (make-segment 70 50)))


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

Tuesday, 19 June 2012

Exercise 172: Develop a data representation for worms with tails.

Exercise 172: Develop a data representation for worms with tails. A worm’s tail is a possibly empty sequence of “connected” segments. Here “connected” means that the coordinates of a segment differ from those of its predecessor in at most one direction and, if rendered, the two segments touch. To keep things simple, treat all segments—head and tail segments—the same. Then modify your program from exercise 170 to accommodate a multi-segment worm. 
Download the code from this exercise here.
A multi segment worm should be able to be represented by a cons'd list of worm segments. Given the examples of data definitions we have, I'm not sure how (or even if) we can implement the restrictions that it must differ in direction from its predecessor in at most one direction, and that its segments must all be touching. I will implement these restrictions in code. If any one knows how you can specify these restrictions in the data definition, please comment and let me know.
 A MSWorm is one of:
 – (cons segment empty)
 – (cons segment MSWorm)


 Changes

The main change is worms are now represented by a series of segments, rather than a single segment. All the other changes flow on from this. As we have been told not to implement collision detection, these functions and related tests have been removed.

The draw-worm function now needs to draw a series of worm segments, rather than just a single segment. This is a straight forward list traversal problem so we can recurse down the list drawing a disk each time until we reach the end of the list.

 The move-worm function now consists of two calls to seperate functions. The function remove-last will remove the last segment of the worm. This is done by reversing the list, getting the rest of the segments and reversing the results. The other function called will produce a new segment in the correct new position. The results of these two function calls are then cons'd together to form a new worm.
All of the existing tests needed to be changed to cope with with new segment/worm definition. Most of these were just changing make-worm into make-segment, but some tests needed to logically changed.

Code

; Constants
(define WORM-SIZE 10)
(define WORM-MOVE (* WORM-SIZE 2))
(define WIDTH 800) ; width of the game
(define HEIGHT 500) ; height of the game
(define SEGMENT (circle WORM-SIZE "solid" "red")) 
 
; these structs will hold the current list of worm segments, the direction 
; the worm is travelling in, and our world object
(define-struct segment(x-pos y-pos))
(define-struct direction(x y))
(define-struct world(worm direction))
(define WORM (list (make-segment 100 100) 
 (make-segment 100 80)
 (make-segment 100 60)) )
; 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))
; Functions
; draw worm in its current location on the screen. Instead of simply drawing a single dot 
; on the screen, we need to recurse down our list of worm segments drawing each one at a
; time
(define (draw-worm background worm)
 (cond [(empty? worm) background]
 [else (place-image 
 SEGMENT
 (segment-x-pos (first worm)) (segment-y-pos (first worm)) 
 (draw-worm background (rest worm))
 )]))

; 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. Instead of changing the position of 
; a single segment, now we add a segment to start of the worm (in the current direction)
; and get rid of the end of the worm
(define (move-worm worm direction)
 (cons (new-segment (first worm) direction) (remove-last worm))
 )
 
; return a new segment moved in 'direction' from the segment 
; passed in
(define (new-segment segment direction)
 (make-segment (+ (segment-x-pos segment) 
 (* WORM-MOVE (direction-x direction)))
 (+ (segment-y-pos segment) 
 (* WORM-MOVE (direction-y direction)))))

; remove the last worm segment. this is a pretty optimised function. just
; reverse the list, grab the rest of it, and reverse it again to get it the 
; correct order. Is their a built in to do the same?
(define (remove-last worm)
 (reverse (rest (reverse worm))))
 
; 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-worm world) (world-direction 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 WORM
 (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 ###
; Test when we move the worm up, a new segment is added to the start and removed
; from the end.
(check-expect (move-worm WORM LEFT)
 (list
 (make-segment (- 100 WORM-MOVE) 100)
 (make-segment 100 100) 
 (make-segment 100 80)))
; Check the remove-last function removes the last segment correctly
(check-expect (remove-last WORM) 
 (list (make-segment 100 100) 
 (make-segment 100 80)))
; Test that new segment returns a new segment in the correct position
; (as per the current direction)
(check-expect (new-segment (make-segment 100 100) UP)
 (make-segment 100 (- 100 WORM-MOVE)))

; ### OLD TESTS
;
;; Test our worm draws as we expect it.
(check-expect (draw-worm (empty-scene 200 200) WORM)
 (place-image SEGMENT 100 100 
 (place-image SEGMENT 100 80 
 (place-image SEGMENT 100 60 (empty-scene 200 200)))))

; Test our worm moves in the direction we expect
(check-expect (move-worm (list (make-segment 50 50)) DOWN)
 (list (make-segment 50 70)))
(check-expect (move-worm (list (make-segment 50 50)) UP)
 (list (make-segment 50 30)))
(check-expect (move-worm (list (make-segment 50 50)) LEFT)
 (list (make-segment 30 50)))
(check-expect (move-worm (list (make-segment 50 50)) RIGHT)
 (list (make-segment 70 50)))
; Test our change-direction function changes the direction, but doesn't impact the postion
(check-expect (change-direction (make-world (make-segment 50 50) DOWN) UP)
 (make-world (make-segment 50 50) UP))
(check-expect (change-direction (make-world (make-segment 50 50) DOWN) RIGHT)
 (make-world (make-segment 50 50) RIGHT))

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))   

Tuesday, 5 June 2012

Exercise 170: Design an interactive GUI rogram that continually moves a one-segment worm...


 Design an interactive GUI program that continually moves a one-segment worm and enables a player to control the movement of the worm with the four cardinal arrow keys. Your program should use a red disk to render the one-and-only segment of the worm. For each clock tick, the worm should move diameter.
Download the complete source for this article here.


I  enjoyed this exercise - it was interesting to see how to develop this question. The main difficulty I faced was finding concise documentation on how the big-bang function works. This is the main function that drives these "world" exercises. It's heavily documented within the racket documentation, but in a very dry manner that just lists the individual methods, but doesn't describe clearly how to put every thing together. With the HTDP2e book, it's documented well, but in a very scattered manner over several chapters. Eventually I found  this pdf (provided by the same authors) that gave much in the way of needed detail.

The concept I was missing was the 'world' data structure provided by the first argument to big-bang. This is the main  'world scene' object -  a data structure containing the dynamic (changing) aspects of the game world. This is passed automatically into each of the given callback functions you supply (along with possibly other arguments) and is expected to be returned from each of these functions - possibly with a new world state.

As this is a functional program, we are not changing this world data structure, but rather  creating an entirely new representation of the world each time.

Game constraints and constants


We make the worm size configurable here and when we move the worm it will be by the amount specified. The hints provided in the question suggest two different means to keep track of the worm - via a physical or logical location and suggests that one of these may be easier than the other in terms of changing the size of the worm or the game board. They both seem fairly similar to me in this regard, but I have chosen a physical representation as it seems much easier in terms of drawing the wold. The logical position would need to be converted back to a physical position at the time of drawing.

(define WORM-SIZE 10)
(define WORM-MOVE WORM-SIZE)


(define WIDTH 800)  ; width of the game
(define HEIGHT 500) ; height of the game


; this struct will hold the current position of the struct. I imagine later 
; on it will end up holding a list of the tail of the worm
(define-struct worm(x-pos y-pos))


; a struct to hold the direction the worm currently moving in. 
(define-struct direction(x y))


; 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)) 

This is a key struct - this is our world data-structure and will be passed around to all the functions so that they can take appropriate action and modify the world as needed. Currently it just holds the worm position and direction, but later it will hold the positions of the worm food.

(define-struct world(worm direction))

Functions 


; This function draws a 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)) 





; This draws the current world. Currently it is just drawing the worm but this 
; will probably be extended to drawing other things such as food and obstacles.
(define (show world)
  (draw-worm (empty-scene WIDTH HEIGHT) (world-worm world)) )






The move-worm function is called on each clock tick and moves the worm in the last direction that was pressed. This function takes in a world and returns  a new world with the worm moved along the current direction. The direction doesn't need to change so isn't changed. To determine the new position of the worm, we multiply our direction for both the x and the y axis by the distance the worm should move and add this to the current position. As this x and y vector will be either -1, 0 or 1, this will either subtract, leave unchanged or add the worm travel distance to the  current position.



; This functions takes in a world and constructs a new world with the worm
; moved along the current direction.

(define (move-worm world)
  (make-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)))))
   (world-direction world)))




The handle-key-events functions handles keyboard events. This is a very simple function that just calls the change-direction function with a new direction vector. This returns the new world state as returned by change-direction.


; Handle key board events and return a new world state.
(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]
  ))




The change-direction function creates a new world with the direction the worm is travelling in changed to represent the key just pressed. This function doesn't do much - it just creates a new world struct and returns it. This could have been done in the handle-key-events function, but having a named function for it makes things clearer.


; Return a new world with the direction changed to match the direction passed in.
(define (change-direction world  direction)  
  (make-world (world-worm world) direction)) 




The worm-main function is the function that will start our game running. It calls the big-bang function with the initial state of the world, and details the call-back functions required to make the game work.
The initial state of our world is an empty scene with a worm stationed in the center moving to the right. We supply:

  •  a function show that will draw our world as it currently stands
  •  a function handle-key-events that will change the direction the worm is travelling
  •  a function on-tick that will move the worm one unit in the current direction on each game tick



(define (worm-main rate)
  (big-bang (make-world (make-worm (/ WIDTH 2)  (/ HEIGHT 2))
                        (make-direction 1 0))
            (to-draw show) 
            (on-key handle-key-events)
            (on-tick move-worm rate) ))




This start the game off! 10 movements a second seems to provide a reasonable speed for the current worm/world size


(worm-main 0.1)


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-world (make-worm 50 60) DOWN))
(check-expect (move-worm (make-world (make-worm 50 50) UP))
              (make-world (make-worm 50 40) UP))
(check-expect (move-worm (make-world (make-worm 50 50) LEFT))
              (make-world (make-worm 40 50) LEFT))
(check-expect (move-worm (make-world (make-worm 50 50) RIGHT))
              (make-world (make-worm 60 50) RIGHT))


; 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))