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))
No comments:
Post a Comment