Tuesday, 10 July 2012

Exercise 175: Design the program tetris-render...


Design the program tetris-render, which turns a given instance of Tetris into an Image.

You can download the code for this question from here.

This is a straight forward question. We have a data structure containing a collection of tetris blocks, which we need to turn into a world (game) scene.

We can do this by  recursing through our data structure until we have a complete scene.

I chose to split the tetris-render function into two functions - a main function and a helper function. This because the main render-tetris function only takes a single argument (a Tetris) and if I wanted to recurse into this I would need to continually be created a a new Tetris from the component parts I had (the head and the tail)

   
; Tetris is (make-tetris Block Landscape)
; Landscape is one of:
;  empty
;  (cons Block Landscape)
; Block is (make-block N N)
; interpretation: given (make-tetris (make-block x y) (list b1 b2 ...))
;   (x,y) is the logical position of the dropping block, while
;   b1b2, etc are the positions of the resting blocks
; a logical position (x,y) determines how many SIZEs the block is
;   from the leftxand from the topx.
   

Code

(define-struct block (x y))
(define-struct tetris (block landscape))

; physical constants
(define WIDTH 10) ; the maximal number of blocks horizontally


; graphical constants
(define SIZE 10) ; blocks are square
(define BLOCK ; they are rendered as red squares with black rims
  (overlay (rectangle (- SIZE 1) (- SIZE 1) "solid" "red")
           (rectangle SIZE SIZE "outline" "black")))


; this is just an example/test Tetris scene.
(define TETRIS  (make-tetris (make-block 10 60)
                             (list (make-block 20 40) )))


(define SCENE-SIZE (* WIDTH SIZE))




; This tetris-render function is a wrapper around the main function.
; This just splits the tetris into the initial block/landscape tuple and
; calls the main tetris-render-helper function.
(define (tetris-render tetris)
  (tetris-render-helper (tetris-block tetris)
                        (tetris-landscape tetris)))




; draw a tetris scene. This is the function that does the guts of the work.
; If our landscape is empty, just draw the block onto an empty scene
; otherwise recurse, and draw draw the current block on the result of drawing 
; the rest of the blocks.
(define (tetris-render-helper block landscape)
  (draw-block block
              (cond ((empty? landscape )    
                     (empty-scene SCENE-SIZE SCENE-SIZE))
                    (else (tetris-render-helper  (first landscape)
                                                 (rest landscape))))))


; draw our block on supplied background. 
(define (draw-block block background)
 (place-image BLOCK
                (block-x block) (block-y block)
                background))


(tetris-render  TETRIS)


; ## TESTS ##


; simple example - a single block in an empty landscape
(check-expect (tetris-render (make-tetris (make-block 20 40) empty))
              (place-image BLOCK 20 40 (empty-scene SCENE-SIZE SCENE-SIZE)))


; a slightly more complex test - drawing two blocks
(check-expect (tetris-render (make-tetris (make-block 10 60)
                              (list (make-block 20 40) )))
              (place-image BLOCK 10 60 (place-image BLOCK 20 40 
                              (empty-scene SCENE-SIZE SCENE-SIZE))))

Tuesday, 3 July 2012

Exercise 174: Equip your program from exercise 173 with food.


Exercise 174: Equip your program from exercise 173 with food 

At any point in time, the box should contain one piece of food. To keep things simple, a piece of food is of the same size as worm segment. When the worm’s head is located at the same position as the food, the worm eats the food, meaning the worm’s tail is extended by one segment. As the piece of food is eaten, another one shows up at a different location.
Your program should place the food randomly within the box. To do so properly, you need a design technique that you haven’t seen before—so-called generative recursion—so we provide function definitions:

; Posn -> Posn
; ???
(define (food-create p)
  (food-check-create p (make-posn (random MAX) (random MAX))))
; Posn Posn -> Posn
; generative recursion
; ???
(define (food-check-create p candidate)
  (if (equal? p candidate) (food-create p) candidate))
The code for the exercise can be downloaded here

Generative recursion is defined by wikipedia as recursion that creates an entirely new object.

The function food-check-create creates a random object (via food-create) and then checks to make sure it's not in the same position as the posn passed in.  This is  to ensure that the food does not appear directly under the snakes head thus prematurely ending the game.


The function as supplied does not ensure that the food does not appear under the snake body - however that shouldn't matter too much as this will not end the game due to the collision detection only checking the worms head - it would just seem a bit funny to watch.

In my answer below, I have modified this function somewhat. This is due to the nature of the error collision I have used in checking for food collisions. I am checking the co-ordinates of the food and of the snake and determining a collision has occurred if the x and y co-ordinates are the same. This is an issue because if the co-ordinates are out by 1-20 pixels, the snake would appear to touch (or overlap) the food and pass right over it.

Therefore I have locked the food generation to appear on only x and y co-ordinates that the snake can appear on. I could have also fixed this in the food-collision method, but this would have mean the snake could still only partially intersect the food. It would work, but still look funny (and this is not how snake games are meant to work!)

Whilst I was writing this final stage of this question, I finally recall the the posn structure and realise I probably should have been using them all along (instead of the structure I called segments). This would be a minor thing to change, but I will leave them called segments as this makes the code easier to read and makes it consistant with the answers I have already created.




; 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"))
(define FOOD (circle WORM-SIZE "solid" "black"))


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


; worm is a list of worm segments
; direction is the direction the worm is travelling
; segment is our current food segment
(define-struct world(worm direction segment))


(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





This first function food-create is one of the supplied functions. I have changed the dimensions for the allowed positions for the food segment. This is because we need to make sure the food is in a position that our worm can travel over completely. (Currently this is multiples of 20). If we don't do this, the food could be off by for example 5 pixels, so we could travel over it and not trigger a collision



; Posn -> Posn
; Creates a new item of food at anywhere on the screen *except* for the 
; point defined by posn p. (eg the current snake position)

(define (food-create p)
  (food-check-create
   p 
   (make-segment (* (random (/ WIDTH WORM-MOVE)) WORM-MOVE)
                 (* (random (/ HEIGHT WORM-MOVE)) WORM-MOVE))))



; Posn Posn -> Posn
; checks that the candidate and p are at different points on the screen.
; If they are at the same point, create a new candiate by recursing into
; food-create, otherwise return the new candidate (which will then be returned
; via food-create)
(define (food-check-create p candidate)
  (if (equal? p candidate) (food-create p) candidate))




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




; This is a new method for question 174.
; This draws the food on to the background of our world
; Returns a new image.
(define (draw-food background food)
  (place-image FOOD (segment-x-pos food) (segment-y-pos food) background) )


; function to determine if we have collided with a food block
; returns true if the worm's segment is on top of the food
(define (worm-hit-food? segment food)
  (cond ((and (= (segment-x-pos segment) (segment-x-pos food))
              (= (segment-y-pos segment) (segment-y-pos food))) true)
        (else false)))




; This is a new function for question 174. It grows the worm one segment.
; To do this we just append a segment in the existing foods position
; on to the worm. Another way to do this would be to append at the end of 
; the list.
; This method was simpler though (but doesn't look quite as good)
(define (grow-worm worm food)
 (cons (make-segment (segment-x-pos food) (segment-y-pos food)) worm  ))
       


; This is a new function for question 174. This method will check if we are
; in the same location as a food item, and if so, eat it. (and make the worm
; grow longer)
; returns a world state with either nothing changed, or a longer worm and
; a new food;
(define (eat-food world)
  (cond ((worm-hit-food? (first (world-worm world)) (world-segment world))
         (make-world  (grow-worm (world-worm world) (world-segment world))
                     (world-direction world)
                     (food-create (first (world-worm world)))))
        (else world)))



; Checks if the snake has collided with either itself, or with the walls
; Returns true in the event of a collision
(define (collision-detected world)
  (or
   (collision-detected-wall (first (move-worm-helper world )))
   (collision-detected-worm (first (move-worm-helper world))  (world-worm world ))))


; Returns true if the worm has collided with itself.
(define (collision-detected-worm segment worm)
  (member? segment worm))


; helper function for collision detection. Operates directly
; on a worm, rather than world object.
(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]))




; 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.
(define (show world)
  (draw-food
   (draw-worm (empty-scene WIDTH HEIGHT) (world-worm world))
   (world-segment 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))
)


; helper method to DRY up code when calling this from a world object
(define (move-worm-helper world)
  (move-worm (world-worm world) (world-direction world)))


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






; This has changed for question 174 - we now need to check for food 
; collisions on each clock tick.
; 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)
  (eat-food
   (make-world
    (move-worm  (world-worm world)
                (world-direction world))
    (world-direction world)
    (world-segment 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  (world-segment world)))




; This is the big bang function that drives the game.
(define (worm-main rate)
  (big-bang (make-world WORM
                        (make-direction 1 0)
                        (make-segment 300 100)
                        )
            (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


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

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