Submission Details

DrRacket Cheat Sheet
Checking for Errors
Submission FAQ
Autograder FAQ
Late Penalty Waiver

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:

  1. have a summative assignment of nearly all the topics we’ve covered in the first 7 weeks of class
  2. 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
  3. gain experience in an environment where “testing” involves both written tests and user tests
  4. 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 Screenshot of finished 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.

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.

Exercise 6 Starter Files

DO NOT MAKE ANY CHANGES TO snake_lib.rkt OR THE FIRST 3 LINES OF exercise_6.rkt AT ANY POINT.

  1. exercise_6.rkt MUST START WITH #lang htdp/isl+ AND (require "./snake_lib.rkt"); don't add any code before these two lines. Do NOT change to asl as this assignment DOES NOT use imperative programming.
  2. You will do your ALL YOUR WORK in exercise_6.rkt. Make sure your code passes the required check-expects and write additional check-expects to ensure you’ve covered all possible situations.
  3. 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:

These definitions are provided in your code and the define-struct lines must NOT be changed as they are needed by snake-lib.

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 posn struct (examples and documentation)

A posn is made up of two attributes:

  • 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 game object to track all objects on the board. In each "tick" of the game clock, the Snake library executes the following steps:

  1. Call draw-game in the library and display the result
  2. Call is-snake-dead? to determine if the game should end or not
  3. If is-snake-dead? returns #true, terminate the game.
  4. Otherwise, if any key is pressed, call set-direction and set the game object to its output
  5. 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:


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.
Important: Computer Coordinate Systems

Computers tend to use an odd coordinate system where the top left corner has coordinates (0, 0) and the y-coordinate increases in value as you move down. Therefore, the bottom-left corner is at coordinates (0, SIZE_OF_COORDINATE_SYSTEM - 1), and the bottom-right corner is at (SIZE_OF_COORDINATE_SYSTEM - 1, SIZE_OF_COORDINATE_SYSTEM - 1). This means that you will want to DECREASE the y-coordinate when the snake is MOVING UP and INCREASE it when the snake is MOVING DOWN.


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:

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 posn objects 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.

Screenshot of Game Board for Activity 1.1 Screenshot of the game board of activity 1
(Open in a Separate Window)

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 g...and all we wanted was to modify the g's snake, we would have to use something like...
(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
Game board before calling add-new-head Game board after calling add-new-head

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 reverse function so you’ll need to use recursion!

Hint From the data definition of (nonempty-listof posn), the test for the base case of recursion should be (empty? (rest <SOME-INPUT>)).

Activity 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.

Screenshot
board-snake-droptail-before board-snake-droptail-after
Game board before calling drop-tail Game board after calling drop-tail

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
Game board before calling set-direction Game board after calling dset-direction

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:

  1. 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) -> boolean
    

    The hit-wall? function takes the non-empty list of snake body posns and returns #true if the HEAD of the snake has hit the wall or #false otherwise.

  2. An obstacle - see if the HEAD of the snake has collided with any of the obstacles.
    hit-stone? : (nonempty-listof posn) (listof posn) -> boolean
    

    The hit-stone? function takes the non-empty list of snake body posns, then the coordinates of the obstacles, and returns #true if the HEAD of the snake equals any of the obstacles or #false otherwise.

  3. 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) -> boolean
    

    The hit-snake? function takes the NON-EMPTY list of snake body posns and returns #true if the snake has hit itself or #false otherwise.

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:

  1. Compute the new head coordinates using adjacent-posn (activity 2.4)
  2. 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)
  3. Otherwise:
    1. Add the new head to the snake with add-new-head (activity 1.3)
    2. Remove the last coordinate of the snake with drop-tail (activity 1.5)

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
The snake eats a food morsel
board-eating-before board-eating-after
Game board before eating Game board after eating
The snakes moves forward without eating
board-moving-before board-moving-after
Game board before moving Game board after moving

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.

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.