Submission Details
This exercise will be focused on implementing the game Snake. If you’ve never played before, I recommend checking out this browser-based version before starting the assignment but keep in mind our game is slightly different (make sure to read through Part 0 very carefully).
The purpose of this assignment is to:
- have a summative assignment of nearly all the topics we’ve covered in the first 7 weeks of class
- get you used to working on a large “existing code base” where your job is to only implement a few parts of a larger project
- gain experience in an environment where “testing” involves both written tests and user tests
- see how purely functional programming can be used to create larger projects
This is an authentic example of what you’ll be doing if you continue on in CS past our course. It’s incredibly rare to work on something just totally by yourself. And even if you’re working by yourself, you usually rely on outside libraries and other people’s programs as the basis of your larger programs.
To further this learning goal you can choose to work with a Partner on this assignment. If you wish to work with a partner on Exercise 6 follow the instructions in the button below. Working together on this assignment without signing up as an official partnership will be treated as a violation of our Academic Honesty Policy.If you wish to work alone that’s totally fine and you should just submit the Exercise like normal.
Signing-up with a Partner
Step 1. Head to the People section of our Canvas page.
Step 2. Click on the Groups tab.
Step 3. Use the search box to search for groups with "Exercise 6" in the group name.

Step 4. Pick the group you and your partner coordinate to select and sign-up for that group.
!!!IMPORTANT!!! Do not submit anything to the assignment before signing up for a group if you intend to have a partner. Once you've successfully signed up for a group, you're free to submit like normal. Only one person in the group needs to submit. The submission will automatically be tagged with the other group member's info. You will receive the exact same TypeCheck and Grader reports.
Modification to our Academic Honesty Policy for this Exercise Only - for registered Partners only. Working with a partner will necessitate looking at the same code base. That is okay for this assignment for people who successfully register with a partner. For everyone else, our AHP remains the same.
Note that working with a partner does not mean you can simply split the work across two people. I highly recommend you instead practice pair programming either while being physically co-located or via Zoom and screen-sharing. This will be by far the most effective and the most fun way to complete the assignment.
Screenshot of Final Game
Getting Started
In this game, you control a snake (green squares) that wanders around a grid (square) board controlled using the arrow keys on your keyboard, eating food morsels (the gold stars), and avoiding obstacles (the gray stones).
Want to customize the look of your game?
I'm totally happy to help people customize the game further if you'd like. Come see me (Prof. Bain) and I can show you how to make some edits to the visuals. Hold off on any mechanics changes until you complete the assignment first.
The player can only control the direction that the snake moves by using the arrow keys. The snake itself never stops moving. Each time the snake eats a morsel of food, it gets 1 segment longer.
If the snake ever runs into any of the following, then the game is over.
- Itself
- The edge of the board
- An obstacle
To begin, download the template files below and make sure that all the files snake_lib.rkt and exercise_6.rkt are in a folder where you will do your work. Your job is to complete the activities described in the parts below. You do not need to complete anything that isn’t specifically defined below.
DO NOT MAKE ANY CHANGES TO snake_lib.rkt OR THE FIRST 3 LINES OF exercise_6.rkt AT ANY POINT.
-
exercise_6.rktMUST START WITH#lang htdp/isl+AND(require "./snake_lib.rkt"); don't add any code before these two lines. Do NOT change toaslas this assignment DOES NOT use imperative programming. - You will do your ALL YOUR WORK in
exercise_6.rkt . Make sure your code passes the requiredcheck-expectsand write additionalcheck-expectsto ensure you’ve covered all possible situations. - You can use ONLY the libraries already required in the starter code or
"./iterated-images.rkt"and"./remove_duplicates.rkt"(however neither of these are actually required to complete the assignment). If you want to use them, you can copy them over from the earlier exercises.
Part 0. Representing the Game State
Just like how we’ve represented different types of data like employees, humans, tracks, etc. using custom structs, we can represent the game of snake using a collection of structs.
For this assignment, we will use several different data definitions (already included in the starter code; you do not need to define these). It comes with the following struct definitions:
-
posn: this allows us to represent (x, y) coordinates in ISL+ (and ASL) -
game: this keeps track of all the game data in the board, including the snake, obstacles, and food morsels. -
snake: this represents the snake character
These definitions are provided in your code and the
The Snake Library maintains a game object tracking all the game data. In addition, the Snake Library automatically wires up the game logic functions, set-direction, is-snake-dead? and step-game you’ll make in Part 2, and passes the game object from the library to these functions as inputs to update the game state.
More on posn
In Racket, coordinates are commonly represented using the
A
- An
x value (number) - a
y value (number)
Just like our custom structs, this means we have the following functions to create, check, and access data in structs:
make-posn : number number -> posn posn? : any -> boolean posn-x : posn -> number posn-y : posn -> number
More on The Controller of the Snake Library
When the game starts, the Snake library creates a
- Call
draw-game in the library and display the result - Call
is-snake-dead? to determine if the game should end or not - If
is-snake-dead? returns#true , terminate the game. - Otherwise, if any key is pressed, call
set-direction and set the game object to its output - Call
step-game and save its output as the new game state for the next "tick"
The Snake Library also provides two other variable definitions that you can use:
-
board-length, the length of one side of the board (measured in terms of snake body segments – by default, this is set to 25)- x-coordinates increase from 0 to
board-length- 1 (inclusive) toward the right - y-coordinates increase from 0 to
board-length- 1 (inclusive) toward the bottom
- x-coordinates increase from 0 to
-
play, a function described near the end of this assignment
The game
;; a game is...
;; - (make-game snake (listof posn) (listof posn) number)
(define-struct game (snake obstacles foods ticks))
;; foods and obstacles are both lists of posns.
- the
snakeattribute stores asnakeobject which is described in more detail below - the
obstaclesattribute stores the coordinates of the stones as a list ofposnobjects - the
foodsattribute stores the coordinates of the food morsels as a list ofposnobjects - the
ticksattribute stores the count of “turns” since the beginning game (aka how many times has the snake moved). The Snake Library automatically updates this attribute in each “tick” of the game clock.
Important: Computer Coordinate Systems
Computers tend to use an odd coordinate system where the top left corner has coordinates
The snake
;; a snake is...
;; - (make-snake direction (nonempty-listof posn))
(define-struct snake (direction body))
;; body will always be a non-empty list of posns
So a snake is made up of:
- a
direction(one of"up","left","down", or"right") - and a NON-EMPTY list of
posnobjects
The direction attribute indicates the direction in which the snake is traveling. The body attribute tracks the coordinates of the segments of the snake, from the head to the tail.
Important Note: Non-Empty Lists
Non-empty lists of
posnobjects can be captured by the following data definition. This is useful, for example, for writing recursive functions over non-empty lists. Notice, that we know the list will never be empty because of the data definition.;; A (nonempty-listof posn) is either ;; - (cons posn empty) ;; - (cons posn (nonempty-listof posn))
There are no action items for Part 0, so you’re done if you’ve read everything. If you scrolled here, having not read any of the above, I highly recommend going back and reading it now. Otherwise, you’re going to get super confused.
Part 1. Managing Game States
In this part, your job is to code the specified game boards and to write some auxiliary functions to prepare for Part 2. While you’re doing this it might be helpful to refer to our Functions Glossary.
Activity 1.1. board-small
First we want to make sure we understand the structure of a game object. Fill in board-small definition with a game object that represents the board shown in the button below.
Uncomment the provided check-expect to test if board-small represents the correct board or not. You can use draw-game to render game objects. For example, (draw-game board-small) should output the above image when run in the interactions window as you’re working.
(Optional): If you want extra practice, there are some other test boards you can define which have screenshots embedded in the RKT file. We won’t grade those, but they are good practice.
Activity 1.2. remove-food
remove-food : posn game -> game
This function takes a posn and a game, and removes the food at the specified position from the given game. You can assume that the given position is occupied by a food morsel (i.e. you don’t need to check to see if the food is there).
Hint!
Remember, in functional programming we can't update the value of a variable. If we want to "update" a game, we have to create a whole new game and update its attributes using the values from the old game. So for instance if we wanted to make a new game using some old game(make-game (update-something-in-snake (game-snake g))
(game-obstacles g)
(game-foods g)
(game-ticks g))
Activity 1.3. add-new-head
add-new-head : posn game -> game
This function takes a posn and a game, and adds the given posn to the front of the snake of the given game. For example, calling add-new-head with (make-posn 3 1) and the game on the left should output the game on the right in the below button.
You can assume that the given posn is adjacent to the head of the snake.
Screenshots
| board-snake-growing-before | board-snake-growing-after |
|---|---|
![]() | ![]() |
Activity 1.4. remove-last
remove-last : (nonempty-listof posn) -> (listof posn)
Use recursion to write remove-last that, when given a NON-EMPTY list of posn objects, removes the last one from the list. You cannot use the
Hint
From the data definition ofActivity 1.5. drop-tail
drop-tail : game -> game
Use your function from activity 1.4 to implement drop-tail that, takes a game and removes the last posn of the snake in the given game.
Part 2. Game Logic
In this part, your job is to write the following functions, which will serve as arguments to the play-game function. You're free to run the code at any point but the game will not respond to the player until the corresponding functions are completed.
For example, the eyes of the snake can only move after set-direction is written. In any case, it is a good idea to properly test your functions with check-expects.
Activity 2.1. opposite-direction?
opposite-direction?: direction direction -> boolean
This function takes two direction strings and returns #true when given two opposite directions or #false otherwise. For example, "up" and "down" are the opposite of each other. Similarly, "left" and "right" are the opposite.
Activity 2.2. set-direction
set-direction : direction game -> game
This function takes as inputs a direction string and a game object. When the given direction is NOT the opposite of the snake’s direction (use the function you just wrote in activity 2.1!), set-direction changes the direction in which the snake is traveling to the given one. Otherwise, set-direction keeps the given game unchanged (this is to make it impossible for the snake to immediately turn into itself with an errant key).
Screenshots
| board-snake-turning-before | board-snake-turning-after |
|---|---|
![]() | ![]() |
Activity 2.3. hit-wall?, hit-stone?, hit-snake?, and is-snake-dead?
is-snake-dead? : game -> boolean
hit-wall? : (nonempty-listof posn) -> boolean
hit-stone? : (nonempty-listof posn) (listof posn) -> boolean
hit-snake? : (nonempty-listof posn) -> boolean
Write a function is-snake-dead? that takes a game and returns #true if the snake is dead and #false otherwise. You should complete is-snake-dead? together with its three helper functions below. The three helper functions are also graded, so make sure they are taking the same inputs described below.
The game ends when the snake runs into:
- A wall - In other words, if any of the x and y coordinates of the HEAD of the snake is smaller than 0 or (strictly) greater than
board-length- 1, then the snake has hit the wall.hit-wall? : (nonempty-listof posn) -> booleanThe
hit-wall?function takes the non-empty list of snake bodyposns and returns#trueif the HEAD of the snake has hit the wall or#falseotherwise. - An
obstacle- see if the HEAD of the snake has collided with any of the obstacles.hit-stone? : (nonempty-listof posn) (listof posn) -> booleanThe
hit-stone?function takes the non-empty list of snake bodyposns, then the coordinates of the obstacles, and returns#trueif the HEAD of the snake equals any of the obstacles or#falseotherwise. - Or itself - the HEAD of the snake is in the same location as one of the rest of the segments.
hit-snake? : (nonempty-listof posn) -> booleanThe
hit-snake?function takes the NON-EMPTY list of snake bodyposns and returns#trueif the snake has hit itself or#falseotherwise.
Note that a “dead snake” (aka, game over) is demonstrated by the snake turning red on the game board. The Snake Library also takes care of this for you.
Activity 2.4. adjacent-posn
adjacent-posn : posn direction -> posn
The adjacent-posn function takes a posn, a direction string, and returns a posn one step in the given direction. For example, moving (make-posn 3 5) one step "up" produces (make-posn 3 4).
Activity 2.5. step-game
step-game : game -> game
This is the most challenging part of the assignment. This function advances the game clock one “tick” (though keep in mind you don’t actually need to edit the ticks attribute of the game – the engine will do that for you), moving the snake forward, and possibly causing the snake to eat and grow.
Moving the snake means that the snake gains a segment in its traveling direction and loses the last segment (unless it eats). The new segment’s coordinates are determined by the head of the snake and the direction it is traveling. If the new segment would collide with a food morsel, step-game removes the food from the game. Otherwise, the snake loses its last segment.
More precisely, step-game should implement the following game logic:
- Compute the new head coordinates using
adjacent-posn(activity 2.4) - If the new head coordinates coincide with the coordinates of any of the food morsels:
- Add the new head to the snake with
add-new-head(activity 1.3) - Remove the food on the board with
remove-food(activity 1.2)
- Add the new head to the snake with
- Otherwise:
- Add the new head to the snake with
add-new-head(activity 1.3) - Remove the last coordinate of the snake with
drop-tail(activity 1.5)
- Add the new head to the snake with
This function does not need to replace eaten food since play (in the Snake Library) handles that task. However, you do need to handle removing the eaten food from the game.
Note: As you work on this assignment, you might consider what would happen if a piece of food happens to appear in the same position as an obstacle. Please ignore this issue. That is, don’t worry about testing for this situation. A new piece of food will only appear at a currently “open” location (i.e. one that does not contain a piece of food, an obstacle, or part of the snake). This check is already implemented in
snake_lib.rkt.
Screenshot Examples
| board-eating-before | board-eating-after |
|---|---|
![]() | ![]() |
| board-moving-before | board-moving-after |
|---|---|
![]() | ![]() |
Part 3. Playing the Game
At any point you can run the game by hitting Run and then following the directions in the Interactions Window. Keep in mind that unless you define a different start board, you’ll receive the same obstacles each time.
Part 4. Double Checking your Work
Make sure you’ve followed the process outlined in the introduction for every function, and that you’ve thoroughly tested your functions for possible edge cases. Some of these functions are VERY hard to test using check-expects. So instead, you might come up with a “testing protocol” where you play the game to see if the correct outcome occurs each time.
- For example, after implementing
step-gameyou might want to test yourhit-wall?function by playing the game purposefully running the snake into all 4 walls of the game world - Or purposefully running into an obstacle from all four sides
- Or purposefully running into the tail of yourself (the snake)
- etc…
Before turning your assignment in, run the file one last time to make sure that it runs properly and doesn’t generate any exceptions, and all the tests pass. Make sure you’ve also spent some time writing your OWN check-expect calls to test your code.
Assuming they do, submit only your exercise_6.rkt file on Canvas.









