On this page:
1 Design sketch
1.1 Main
1.2 Data Definitions
1.3 Defined Constants
1.4 Game functions
1.5 Snake functions
1.6 Posn functions
2 How to approach
3 Submit
6.12

Assignment 2: Snek

Due: Sunday, July 21, 11:59:59 PM EST.

In this assignment, you are tasked with designing a variant of the classic game Snake. In this version, which we call Snek, the player controls a small snake on a screen. The snake is able to move up, down, left, or right. There is also a single peice of food on the screen. If the snake is moved to where the food is, it eats the food, grows one segment longer, and another food appears somewhere on the screen.

The snake dies and the game is over if it ever crosses the boundaries of the screen or collides with itself.

You must use BSL to design and implement Snek. It will require use of the 2htdp/image and 2htdp/universe libraries.

1 Design sketch

To get started, save a copy of your solution to Assignment 1: Bug as assign2.rkt and open it in DrRacket. You will need to adapt your Bug program as follows.

1.1 Main

The provided code consists of several "sections" of code marked with lines of semicolons and section headings. This program files a top-down organization. So the first section describes the main function, which is used to launch the Snek program.

This function is defined as:

; main : Number -> Game
; Launches a game of "Snek" played at given tick rate
; Example: (main 1/10)
(define (main r)
  (big-bang G0
    [to-draw game-draw]
    [on-tick game-advance r]
    [on-key game-handle-key]
    [stop-when game-over?]))

This big-bang program starts in an initial state G0, whose definition we will see shortly.

There are three event handling functions. Draw events use the game-draw function, tick events use the game-advance function, and keyboard events use the game-handle-key function. You will have to design and implement these functions to make the game.

The main function takes a single argument: the speed (measured in ticks per second) at which the game will play.

1.2 Data Definitions

The data definitions section is the most important section. It defines how all of the information that occurs in the Bug game will be represented as data in BSL. It contains the following definitions:

; A Game is a (make-game Snake Food)
(define-struct game (snake food))
 
; A Snake is a (make-snake Dir Posn LoPosn)
(define-struct snake (dir head tail))
 
; A LoPosn is one of:
; - '()
; - (cons Posn LoPosn)
 
; A Food is a Posn
 
; A Posn is a (make-posn Integer Integer)
 
; A Dir is one of:
; - "left"
; - "right"
; - "up"
; - "down"

These definitions define classes of values used in the Snake program. The first class of values are Games. A Game is an instance of a game structure, which has a Snake and a Food. A Snake is an instance of a snake structure with a Dir, representing the direction the snake is moving, and a Posn for the head segment of the snake, and a list of Posns for the tail (or body) of the snake. Just like in Bug, a Food is an instance of a posn structure with two Integers. A Dir is an enumeration of strings representing a direction.

These definition will be used throughtout the signatures in the rest of program and will drive the design of our functions.

1.3 Defined Constants

The defined constants gives names (by convention, we use uppercase for constant names) to values that will be used repeatedly in the rest of the code. These do not need to change from Bug.

The first three constants concern the visual rendering of the Bug game. The game is played on a WIDTH by HEIGHT grid. Here we’re saying it’s a 20 x 20 grid. Each unit of the grid measures PX/U pixels; in this case 20.

(define PX/U 20) ; pixels per unit
(define WIDTH 20)  ; units
(define HEIGHT 20) ; units

Changing the definition of these constants will change the sizing of the game. For example, changing PX/U to 10 will cause the screen to shrink by half. The game will still logically play on a 20 x 20 grid, but it will just be scaled down. On the other hand, changing WIDTH to 100 will change the game to play on a 100 x 20 grid.

The main motivation for making defined constants is so that there is a single point of control for these parameters of the program. A well-designed program can have constants change and continue to work as expected.

The next defined constant is G0, the initial Game state used in the main function. It creates an instance of a Game with a Snake located in the center of the board, moving up, and a Food placed at position (0,0). Initially, the snake has no tail (it’s just a head segment), but will grow over time as the snake eats food.

(define G0 ; an initial game state
  (make-game (make-snake "up"
                         (make-posn (quotient WIDTH 2)
                                    (quotient HEIGHT 2))
                         '())
             (make-posn 0 0)))
1.4 Game functions

This section contains the functions relevant to computing with data represented as a Game.

There were three functions used in the main function and they are stubbed out here. These functions will need to be adapted from Bug to work for the new data in Snek:

; game-handle-key : Game KeyEvent -> Game
; Handle a key event in this game
(define (game-handle-key g ke)
  ; stub
  g)
 
; game-advance : Game -> Game
; Adance the snake, maybe eating the food
(define (game-advance g)
  ; stub
  g)
 
; game-draw : Game -> Scene
; Render the game as a scene
(define (game-draw g)
  ; stub
  (empty-scene (* WIDTH PX/U)
               (* HEIGHT PX/U)))
 
; game-over? : Game -> Boolean
; Has the snake hit itself or gone off the screen?
(define (game-over? g)
  ; stub
  #false)

You will need to design these functions. Each of them will likely require breaking things down into smaller problems to solve. For each of those subproblems, you will need to design a function. Those functions should be added to sections of the program that contain all the functions relevant to the same kind of data (e.g. Bug functions should be in a section together, Posn function should be in a section together, etc.)

1.5 Snake functions

This section contains functions relevant to snakes. A couple of functions that you may find useful are stubbed here.

; snake-change-dir : Snake Dir -> Snake
; Change snake's direction to given one
(define (snake-change-dir s d)
  ; stub
  s)
 
; snake-advance : Snake -> Snake
; Advance bug in its current direction
(define (snake-advance s)
  ; stub
  s)
 
; snake-advance&grow : Snake -> Snake
; Advance bug in its current direction and grow by one segment
(define (snake-advance&grow s)
  ; stub
  s)
 
; snake-self-colliding? : Snake -> Boolean
; Is the snake colliding with itself?
(define (snake-self-colliding? s)
  ; stub
  #false)
 
; snake-wall-colliding? : Snake -> Boolean
; Has the snake gone past the boundaries of the screen?
(define (snake-wall-colliding? s)
  ; stub
  #false)

The movement and growing of a snake is a bit subtle. Here’s how it should work. If the snake’s head is currently over the food and a tick happens, the food should be consumed, appear somewhere else, and the snake should advance and grow. If the snake’s head is not over food, then it simply advances (but does not grow).

The way to advance a snake is generate a new head based on advancing the posn of where the current head is (in the direction the snake is going). The current head should become the first segment of the tail, and the last segment of the tail should be removed. (Try to make examples on paper to convince yourself that this works.) You do not have to advance every segment in a snake. It won’t work. Instead you add something new at the front and drop something old at the end and voilà the snake moves.

Advancing and growing a snake works exactly the same way except you don’t drop the last segment of the tail! (Again, make examples on paper to get a feel for how it works.)

This does suggest the need for the following helper function that works on non-empty lists of Posns:

; NeLoPosn -> LoPosn
; Drop the last element of the given list
(define (drop-last ps)
  ; stub
  ps)

Note that to help you with this, consider a data defintion for non-empty lists of Posns:

; A NeLoPosn is one of:
; - (cons Posn '())
; - (cons Posn NeLoPosn)

A template for functions that consume NeLoPosn is:

(define (neloposn-template ps)
  (cond [(empty? (rest ps)) (... (first ps) ...)]
        [(cons? (rest ps))
         (... (first ps)
              (neloposn-template (rest ps))
              ...)]))

Use this template to help you write drop-last.

1.6 Posn functions

You will probably want to keep all your Posn functions from Bug.

2 How to approach

You will need to design all of the pieces above and put them together to make Snek. Use the design recipe to help you. Templates for list functions are particularly helpful. Make examples. Write tests.

3 Submit

Submit your assign2.rkt file to assignment 2 on ELMS. You can submit early and often. We will grade the last submission before the deadline.