« Back to the main CS 300 website

Project 1: Snake :snake:

Part 1A checkin due by Friday, February 2nd
Parts 1 and 2 due Friday, February 9th at 6:00pm (EST)

All parts (1-3) due Friday, February 16th at 6:00pm (EST)


Introduction

Welcome to the first project of CS 300! We're excited that you're here! :smiley:

In this project, you will develop a version of Snake, a famous early computer game widely cloned and reproduced. (Fun fact: Snake in QBASIC was the first computer game Malte played as a child, and Snake was also on his first mobile phone, a Nokia 3310.)

Since we're writing the Snake game in the C programming language, this assignment requires you to work directly with memory and data representations. In the first and second portions of the project, you will set up a basic playable version of the game with customizable levels. In the third part, you will bring your implementation closer to the original snake game by supporting a growing snake (using your own linked list data structure), and explore different character representations that allow for player names from different cultures by supporting their alphabets.

Learning Objectives

In this assignment, you will:

Assignment

⚠️ Warning: It is important to get started early! This project is not trivial. It's very doable (even the hard parts), but you want to start early to have enough time to debug your code.

Assignment Installation

If you have completed Lab 0, you will already have created a private GitHub repository for your project work under the csci0300 organization.

:warning: If you have not completed Lab 0, we suggest that you do it now! :warning:

Recall, to clone your repository for the projects, you run this command (inside the course container, or on the computer you're working on):

$ git clone git@github.com:cs300-s24-projects-YOURUSERNAME.git

(You'll hopefully have done this as part of Lab 0 already.)

Now, ensure that your repository has a handout remote. Type:

$ git remote show handout

If this reports an error, run:

$ git remote add handout https://github.com/csci0300/cs300-s24-projects.git

Then run:

$ git pull
$ git pull handout main

This will merge our Project 1 stencil code with your repository.

Once you have a local working copy of the repository that is up to date with our stencils, you are good to proceed. You'll be doing all your work for this project inside the snake directory in the working copy of your project's repository.

Stencil Overview

Below is an outline of all the files we provide you with an a brief description of what they do:

In snake/src/:

File Purpose
common.h Contains definitions for the input_key enum and snake struct, declarations for global variables defined in common.c, and function headers for common.c
game_over.h Contains function headers for game_over.c
game_setup.h Contains the definition for the board_init_status enum and function headers for game_setup.c
game.h Contains function headers for game.c
linked_list.h Contains the definition for the node struct and function headers for linked_list.h
mbstrings.h Contains function headers for mbstrings.c
render.h Contains function headers for render.c
snake.c Contains support functions and skeleton version of main

In snake/test/:

File Purpose
traces.json Contains all of the traces (i.e. test cases) that a working implementation will pass
autograder.c & autograder.py Autograder program/scripting

The files that you will have to modify in the project are:

Important Note: The following Testing and Debugging sections explain how to test and debug your code. Make sure you read through these before attempting to implement any part of the project, so that you can run tests and productively debug your code. You should also take a look at our C primer on debugging. (Note: these sections have a lot of content and it is okay if you don't understand it all right away! You will become more familiar with these concepts as time goes on.)

Testing

⚠️ Make sure that you run all tests inside the course Docker container!

Even though this project may compile on your host OS, you may spuriously fail tests if your C standard library generates a different sequence of random numbers than the library version in the course container. For example, the C standard library that comes with macOS uses a different random number generator than the standard library in Linux, and consequently the food will appear in the wrong places (and you'll fail the tests) on macOS.

We've provided an automated test suite for you to check your implementation against. As you progress through the project, you will pass more and more of the tests until you pass them all! Once you pass all your tests locally, make sure that they also pass on the grading server!

To run all the tests, you can use

$ make check

To run a specific test you can use

$ make check-<n>

For instance make check-5 to run test 5. To run a range of tests, use

$ make check-<n>-<m>

For instance, make check-3-10.

A single test contains a compressed board string, a random seed that determines the order of food generation, a flag that says whether or not the snake should grow (the snake doesn't grow in part 1 or 2, but does in part 2), and a series of keyboard inputs. The autograder will use those inputs to pretend to play your snake game. The game runs for as many steps as there are key inputs. When the inputs run out, the autograder takes a snapshot of your game data and compares it against an expected output, notifying you of any discrepancies.

By default, if your board doesn't match the expected output the autograder will print your board and the expected board so you can see the errors. If you also want to see all the intermediate boards as the autograder plays snake, you can run in verbose mode:

$ make -B check V=1 # `-B` forces a rebuild and `V=1` sets verbose mode

To switch back to normal output, you can use

$ make -B check V=0

Note: The tests do not run your main function (from snake.c), they instead call your update function (from game.c) directly.

More Info on Boards and Rendering

You probably noticed that the board strings (only in part 2 and 3, e.g. "B7x10|"), the visual rendering (from running main), and the autograder output use slightly different representations for different cells on the board.

Here's a key to understand what each character represents in different board formats:

Board String Visual Rendering Autograder Output
Wall "W" "" "X"
Empty "E" " " "."
Snake "S" "S" "S"
Food N/A "O" "O"
Grass Only "G" "·" "G"
Grass and Snake N/A "S" "s"
Grass and Food N/A "0" "o"

Debugging

NOTE: Compile with the ASAN=0 flag when using GDB (e.g make -B all ASAN=0) to disable the address sanitizer! Sanitizers should not be used on binaries that are attached using GDB and may result in some unexpected errors! Make sure to remove ASAN=0 from your make command when you are done debugging.

MORE DEBUGGING INFO: For more detailed info on debugging and the address sanitizer see our C primer on debugging.

As you'll discover (or have discovered!), gdb is an invaluable tool for debugging your C programs. However, our snake program represents a challenging debugging problem: how can we run our program in gdb when we also need to see the graphical output?

It turns out gdb has an answer for this we can run the program in one terminal and attach to it with gdb from another terminal. We've provided small scripts to make this easy for you. You can run them as follows:

In one terminal, run the listen script. It takes any arguments you would normally pass to the snake program. Here, we pass 0 to indicate that the snake shouldn't grow.

./gdb-listen.sh 0

In another terminal, run the attach script

./gdb-attach.sh

The first terminal will have your snake game, and the second will have gdb. However, you won't see much in the first terminal, as the game window hasn't been set up yet. Try typing continue in gdb to start the game.

With this setup, you can use gdb how you normally would. For instance, you can set breakpoints on initialize_game, update or decompress_board_string.

Another question you may have is how to debug test cases. These are simpler since they don't require graphical output. We've included an extra make rule to run tests under gdb. You can use it like so:

make check-gdb-<n> # for instance, make check-gdb-3

By default, this will pause program execution in autograder.c:main(), which you don't need to understand. All that's important to know is that autograder.c calls many of the same functions as snake.c, so if you set breakpoints on initialize_game, update, decompress_board_string, etc. you can get to your code to start debugging.

GDB looks weird?

If you run into issues where the terminal doesn't start new lines in the correct place, try a new terminal and do not change its default size. Your terminal may sometimes report the wrong dimensions to GDB.

Part 1: Snake Movement

The rules of the Snake game are as follows:

  1. The snake moves around a rectangular playing field bordered by walls, and with some optional internal walls.
  2. The player uses arrow keys to change the snake's direction.
  3. Food appears at random locations on the playing field, and if the snake's head hits the food, the snake eats it. The player's score increases and new food appears elsewhere.
  4. When the snake runs into a wall (or into its own body), it dies and the game is over.
  5. Some cells may also contain grass, which the snake or food can hide in, causing it to appear a different color.

Your task is to take the stencil which starts out doing nothing at all and implement a game with these rules. Don't worry, we'll break this down for you!

We provide several helpful helper functions in the stencil, and will discuss these as we go along.

Task: Compile the stencil code and run it!

Hints on compiling

Make is a tool that enables us to generate executable files from source code. The Makefile defines rules and behaviors for compilation. You'll learn more about Makefiles in Lab 1.

Navigate to your project directory and run make to create an executable file (called snake). You can also run tests with make — read through the rest of the handout!

You will see that it simply prints a message and exits:

$ make
$ ./snake
usage: snake <GROWS: 0|1> [BOARD STRING]
$ ./snake 0
             ____
Hello       / . .\
CS 300      \  ---<
student!     \  /
   __________/ /
-=:___________/

Part 1A: Board and Game Setup

Like any computer program, the Snake game consists of machine code (in the "code" or "text" segment of memory) and data. The data represents the state of the game, as well as its visual representation to the player. All of this is stored in the computer's memory in the form of bytes.

Your first task is to extend the stencil with the necessary code to represent and update the game state in memory!

We have already created the local variables you need to store information about the game board in the main function of snake.c. We have also already created you the global variables you need to represent high-level game information in common.<c|h>. You will need to fill in the variables necessary to store information about the snake.

First, we'll give you an overview of the information stored on the board and game state. Then, you will come up with a representation for storing information about the snake.

Board Data

We know that the game board will have certain dimensions, may contain walls, and will have a snake. Our snake game will support user-created levels whose board dimensions may differ. We will also need to access and update the snake's position on the board based on arrow key inputs from the user.

So, what are some important details about the game board that we need to store? Additionally, what data do we need to represent the board itself? Think on these questions before reading through our explanation of the variables that we give you in the stencil!

Board Data

Here are the board variables that we give you in the stencil (located in main in snake.c):

// Board data
size_t width; 
size_t height;
int* cells;  // array of integers containing data for each cell on the board

We have included both a width variable and a height variable to store the dimensions of the board. Notice that both of these variables have type size_t. This particular type actually represents values of type unsigned long, but we use the type definition size_t to indicate that the corresponding value represents a "size" of some other kind of data. In this case, we use size_t for our width and height to indicate that we are storing two size measurements of our board. To disambiguate between width and height, we will consider width to be the number of columns in a board and height to be the number of rows.

cells is what we use to represent the actual game board. For this representation, we use an array of integers, i.e., an int[]. You may observe that we don't use an array type, though! Recall from lectures the duality between arrays and pointers: even though int* is the type of a pointer to an integer, we can set aside a region of memory large enough to represent all individual cells in the board, put its address into the int* and treat the memory as an array. Then, we can set the memory address of the starting cell of the board as the pointer, and use pointer arithmetic to access, iterate through, or update the other cells in the board. This will become clearer as you work your way through the project!

You will notice that these variables are local variables in the main function of snake.c. Read on for more details about what this means for our program.

Local Variables & Passing Pointers

In a naive approach, the function signature of initialize_default_board might look like

enum board_init_status
initialize_default_board(int* cells, size_t width, size_t height)

The problem with this is that any changes to these parameters are not reflected in the original variables passed into the function. The solution to this is to pass pointers to those values instead.

enum board_init_status
initialize_default_board(int** cells_p, size_t* width_p, size_t* height_p)

(The _p suffix is a convention used in this project to indicate that these variables are pointers to cells, width, and height.)

Pointers hold the addresses of these variables, so if we pass in the address of cells, width, and height to this new version of the function, the addresses will be copied over, not the actual values themselves. Once we dereference these addresses, we recover their original values in memory.

/* inside the initialize_default_board function */
*width_p = 20;
...

Then once the function terminates, it doesn't matter that the copies of cells_p, width_p, and height_p are deleted, because we've already made the changes to the values they point to.

Game Data

Now, we need to consider what information we might need to store about the state of the game.

Game Data

Here are the game state variables we have given you in the stencil (declared in common.h and defined in common.c):

int g_game_over;  // 1 if game is over, 0 otherwise
int g_score;      // game score: 1 point for every food eaten

You will notice that these are global variables that are defined at the top level of common.c. Read on for more information about how to use global variables in this project.

Global Variables and extern

Recall from lecture that variables declared at the top level of a file, outside of any curly braces ({ }), are global variables. You can access these variables from any function in that file and throughout the entire lifetime of the program. We want global variables like g_game_over and g_score to have these properties for every file in the project. How can we achieve this?

We can't just declare g_game_over and g_score in common.h and have other files include it. When we give a definition of a variable in C, e.g. int g_score;, two things happen: the name is made accessible and space is reserved for that variable. So every time that a file includes common.h, space will be reserved for it!

We don't want every file to have their own copy of g_game_over and g_score, we want them to be able to see the same g_game_over and g_score. C lets us do this with the extern keyword:

/* common.h */
extern int g_game_over;
extern int g_score; 

The extern keyword tells the compiler not to reserve space, and to trust that these variables exist in memory somewhere, while also allowing us to use them. We can actually reserve the space for these variables once in common.c:

/* common.c */
int g_game_over;
int g_score; 

Now every file which includes common.h can access the same g_game_over and g_score variables which live in common.c.

Snake Data

At some point, we will need to update the board based on the current direction of the snake's movement. Consider how you will represent the snake's position and its current direction of movement, so you can then use your representation to update the overall game state.

Important Note: In this part of the project, you only need to represent the head of the snake (i.e., your snake is always of length 1); in Part 3, you will need to implement snake growth.

Task: In common.h and common.c, declare the global variables you need for your representation of the snake.

Hints!
  • You'll need to keep track of the snake’s position on the board. How can you do so? (Remember that you need to be able to find the snake's cell in the cell array contained in the board.)
  • How can you represent the snake's direction of movement? (Hint: take a look at the input_key enum defined in common.h.)
  • Why do we tell you to modify both common.c and common.h? How can you use the extern keyword to define global variables in a multi-file project?

Board and Game Initialization

Now that we have our representation of game data defined, we need to initialize all of our data before we can run our game. First, take a look at the function initialize_default_board (in game_setup.c). You do not need to modify this function in this project.

enum board_init_status initialize_default_board(int** cells_p, size_t* width, size_t* height)

Function Description

Purpose: Initializes the cells array to a default board with 10 rows and 20 columns. The snake is initialized at the 3rd row down and 3rd column from the left within the cells array. You do not need to modify this function.

Arguments:

  • cells_p a pointer to the array of board cells
  • width_p a pointer to the value describing the width of the board
  • height_p a pointer to the value describing the height of the board

Output: INIT_SUCCESS (denoting that board initialization was successful)

This helper function that we've given you initializes the given board variables to a default board, upon which you can implement basic snake motions. This board has no internal walls.

What is enum board_init_status?

You may notice that the return value of initialize_default_board is a value of type enum board_init_status. This type represents possible error codes for errors that could occur during board initialization.

You will need to use this enum when you support loading custom levels in part 2. For now however, we are only initializing the default board, so the return value will always be INIT_SUCCESS.

You can find the full description of the board_init_status enum in game_setup.h.

Now take a look at the function initialize_game (also in game_setup.c):

enum board_init_status initialize_game(int** cells_p, size_t* width_p, size_t* height_p, snake_t* snake_p, char* board_rep);

Task: Your task is to implement initialize_game, which should initialize all board data.

Here's a roadmap:

  1. Call initialize_default_board to initialize cells_p, width_p, and height_p. (You should not touch the snake_p parameter until part 3!)
  2. Initialize all global variables that you declared in common.h, including general game data and the snake data you defined. Depending on how you did this, you may need to use information you obtain by reading initialize_default_board to set these values. It's okay to hard-code this information for now; later, you will customize it for custom boards.
  3. Set the return value of initialize_game to the value returned from initialize_default_board.

Notice that initialize_game is called in the main function in snake.c in the stencil. You do not need to add this call yourself.

Cleaning Up Resources

Now that we have initialized our game and board data, let's run the program as it currently stands!

Task: Compile your code using make clean all, and run the program using the following command:

$ ./snake 0

No board shows up because we haven't written the code to display it on the screen yet; instead, we still get our friendly Snake with the welcome message.

However, you should also get a colorful sanitizer error when the program exits! Expand the drop-downs below for additional details regarding the two types of sanitizers you'll be working with for this assignment.

Leak Sanitizer

The leak sanitizer detects memory leaks upon the completion of a program. A memory leak occurs when memory allocated during our program is not freed before exiting.

Address Sanitizer

The address sanitizer detects invalid memory accesses when a program runs. There are several types of invalid memory accesses (which you'll be exploring in detail in a future project), but they can all be broadly classified as either reading from or writing to memory that was not currently allocated to the program.

Note that by default, the address sanitizer comes packaged with the leak sanitizer, so compiling with fsanitize=address (enabled by default in this project) is sufficient :-).

Also see our C primer on debugging for more information on sanitizers!

The error you should just have encountered is a Leak Sanitizer error. Let's figure out why that is!

Memory is a critical resource that we must clean up when we no longer need it. This is particularly true for dynamically-allocated memory (memory with a manual lifetime), since this memory will not be automatically cleared, and a long running program could eventually use all the memory in the computer. The LeakSanitizer looks for dynamically-allocated memory that is still live when your program exists, and warns you about this "leaked" memory. In other words, once our game ends, we need to make sure that we have freed all the data we allocated to play the game! As it currently stands, we seem to be forgetting to free some memory.

Luckily, the sanitizer error gives us a good idea of where to look. Look at the backtrace from the sanitizer error—you should see a call to a function called malloc. malloc dynamically allocates memory, meaning that we as programmers are responsible for freeing that memory with a call to free. The Leak Sanitizer's message is a reminder that we have forgotten to free the memory that we had previously allocated through malloc!

Task: Now, at the end of the main function in snake.c, uncomment the line that calls end_game(cells, width, height, &snake).

The end_game function is responsible for freeing all the resources used up by our program, as well as stopping game. Click on the dropdown for more information on end_game.

void end_game(int* cells, size_t width, size_t height, snake_t* snake_p)

Function Summary

Purpose: Reponsible for cleaning up all the memory used by the game and stopping the renderer.

Arguments:

  • cells: the array of board cells
  • width: the width of the board
  • height: the height of the board
  • snake_p: a pointer to the snake struct (not used until part 3!)

Task: You will now add a call to free such that the memory that your program allocated through malloc is freed when it calls end_game. To do this, you should:

  1. Use the backtrace from the Leak Sanitizer to find out where the program calls malloc, and where the program stores the pointer it returns.
  2. Look at the definition of end_game to find out where you need to put a call to free. (Does end_game call any relevant functions that have access to cells?)
  3. Insert an appropriate call to free! (Note that free takes in one argument: a pointer to the dynamically-allocated memory for the cells array).

Once you have correctly implemented this section, you should not get any Leak Sanitizer errors when running the game with sanitizers until Part 3.

Congrats! You have now ensured that once the game ends, all allocated memory is freed before main exits (for now).

The Game Loop

But we still don't have any visual output! Let's change that.

Task: Now, at the end of the main function in snake.c, uncomment the line that calls initialize_window(width, height).

The initialize_window function is responsible for setting up the game renderer.

Many computer games are designed around the idea of a core "game loop" that repeats over and over until the game reaches a "game over" state (indicated by the g_game_over global variable in this project). For our version of Snake, the core game loop is as follows:

  1. wait (for some specified amount of time) for user input;
  2. retrieve user input;
  3. update the game state based on user input; and
  4. render the new game state to the screen.

To begin with, you will implement steps 1, 3, and 4 of the core game loop in the main function of snake.c (look for the TODO for Part 1A).

Note: You will initially update the game state without handling user input, so you will see your snake appear and move but not respond to arrow keys. This is okay! Once you have an update function that successfully updates the snake's movement, overall game state, and checks for wall collisions, you will handle user input.

For now, there are three function calls you need to make in your game loop:

  1. usleep(useconds_t usec): a library function that suspends execution for a some amount of time, measured in microseconds (we recommend 100 milliseconds, i.e., 100,000 microseconds, but you can adjust this to speed up or slow down your snake!).
  2. update(int* cells, size_t width, size_t height, snake_t* snake_p, enum input_key input, int growing): the update function you will need to implement in Part 1B that updates the game state based on user input (see the stencil for more details).
  3. render_game(int* cells, size_t width, size_t height): a function we give you as part of the stencil that renders the given game state into a visual game! You will not need to modify this function.

Since we're not handling user input yet, you will pass in INPUT_NONE as the input argument to update. Additionally, since our snake does not yet grow, you will pass in 0 as the last argument to update. Furthermore, since we are not using the snake_p argument yet, you will pass NULL as this argument. In other words, your call to update will look like this: update(???, ???, ???, NULL, INPUT_NONE, 0), where you'll have to figure out what to pass for each ???.

Read through the main function in snake.c to make sure you understand what it's doing! We don't expect you to know what every single function does, but you should understand the general structure.

Task: Implement the core game loop in the main function of snake.c!

☑️ Testing Point A: Now go and read the testing section below if you haven't already. It tells you how to run the tests!

You should pass tests 1 and 2 at this point! This means that you should be able to see a board with 10 rows and 20 columns, with walls around the edges, and a snake initialised at the third row down and third column from the left. The snake should not move.

In other words, when you run ./snake 0, you should see the following:

Checkpoint: Congratulations! You have completed the section required for the Part 1A checkin.

If you don't reach this point by the checkin deadline, please come to TA hours so that we can help you get off to a successful start!

Part 1B: Updating and Running the Game

Updating the Snake's Position

Now that you have defined a way to represent the snake in our game, you can use your representation (as well as the variables storing the game and board data) to begin implementing the update function in game.c. First, you will need to write the functionality that allows your snake to move while on the board.

Note: In most versions of Snake, the snake initially moves to the right. Make sure that you replicate this convention and that your snake starts moving right when the game is initialized!

The full description of the update function can be found in the stencil, but below is a short summary for reference.

void update(int* cells, size_t width, size_t height, snake_t* snake_p, enum input_key input, int growing)

Function Summary

Purpose: updates the cells array and other game data according to input, including the g_game_over and g_score global variables when needed, and the snake's position if the game is not over.

Arguments:

  • cells: an array of integers representing each board cell
  • width: the width of the board
  • height: the height of the board
  • snake_p: a pointer to your snake struct (not used until part 3!)
  • input: the next input
  • growing: an int representing whether we want the snake to grow upon eating food (growing = 1), or not (growing = 0).

Returns: Nothing! But the function should modify the game, board, and snake data appropriately to reflect the user's input and the snake's behavior.

For now, we will only consider the case where the user input is INPUT_NONE (i.e. there is no user input). This means that your update function should shift the snake's position by one cell to the right, because the snake's default initial direction is to the right and we aren't taking any user input yet.

Task: Implement update so that your snake's position shifts by one cell to the right whenever update is called, and the game board is updated to reflect the new snake position.

Hints!

You will need to update your variables storing snake data and the board (from the cells array passed into update) to reflect the snake's new position.

  • This is an instance of a common pattern in computer systems: metadata about the system state and the actual system state must always be updated together. This maintains an invariant that system state and metadata always match (the metadata would be rather unhelpful if this were not the case!).

When should you initialize your snake? Where on the board should you initialize your snake?

  • Hint: The board data should be initialized with bit flags set before the snake is initialized.

Bit Flags

In the stencil, the cells array is defined as an array of integers. This means that each cell on the board has an integer representing what type of cell it is (i.e., a wall, an empty cell, a food cell, a snake cell, or a grass cell). We would like our game implementation to support cells that have multiple properties, such as a snake hiding in grass, or food hidden in grass. To use a single integer to represent these properties of a cell, we use the different bits within the integer value to represent the different states a cell can be in.

A 4-byte (32-bit) integer can hold up to 32 bit flags, but we only need four for the basic game (defined in common.h and shown below). Additionally, we provide PLAIN_CELL, which is the integer corresponding to no bit flags being set (i.e., a plain cell).

// Bit flags enable us to store cell data in integers!
// The "0b" notation indicates binary numbers.
#define PLAIN_CELL  0b0000  // equals 0
#define FLAG_SNAKE  0b0001  // equals 1
#define FLAG_WALL   0b0010  // equals 2
#define FLAG_FOOD   0b0100  // equals 4
#define FLAG_GRASS  0b1000  // equals 8

In the game variant you will implement in this project, FLAG_GRASS can overlap with FLAG_FOOD or FLAG_SNAKE, but no other overlapping flags are allowed. For example, if a cell had FLAG_WALL and FLAG_SNAKE set, the snake would have walked into a wall. We will further discuss what overlapping cells look like in terms of bit flags in part 1B. Additionally, you may further exploit the fact that cells can be in multiple states at the same time in our exciting extra credit exercises ✨.

Overlapping Snake and Grass

Our version of the game allows for FLAG_GRASS to overlap with FLAG_SNAKE in a cell on the board. But how can we manipulate the indiviual binary bits within a cell? Enter: bitwise operators!

Bitwise operators allow manipulating the indiviual bits within a variable. C supports several bitwise operators, including but not limited to:

For the full list of the bitwise operators in C, see this section in part 3!

Example: Calculations with bitwise operators

How can we use bitwise operators to deduce the state of a cell? Let's imagine that a snake moves onto a block of grass. The grass is still on that block, but so is the snake. So, we need to represent the cell as having both states.

int cell = FLAG_SNAKE | FLAG_GRASS;

We know that FLAG_SNAKE = 0b0001 and FLAG_GRASS = 0b1000, so FLAG_SNAKE | FLAG_GRASS = 0b1001.

With this cell that is 0b1001, how can we check whether there is a snake on this block? If we check cell == FLAG_SNAKE, the result will be false, because 0b1001 != 0b0001. Luckily, we have the & operator!

bool has_snake = cell & FLAG_SNAKE;

Because cell = 0b1001 and FLAG_SNAKE = 0b0001, cell & FLAG_SNAKE = 0b0001. Remember that in C, 0 represents false, and any non-zero number is considered true. So, we get true, meaning that this block has a snake on it!

Now, let's imagine that the snake moves off this block. The block will still have grass on it, but will no longer have the snake. How can we update the cell state?

int new_cell = cell_state ^ FLAG_SNAKE;

Again, cell = 0b1001 and FLAG_SNAKE = 0b0001, so cell ^ FLAG_SNAKE = 0b1000. Remember that FLAG_GRASS = 0b1000 as well! We have successfully removed the snake from the cell but kept the grass.

Task: Ensure your update properly allows the snake to move into a grass cell (causing the snake to turn green).

Collisions with Walls

Now, your snake can move across the board and hide in grass! But, as you should notice, your snake doesn't stop when it hits a wall, and passes right through instead!

We don't want this to happen; if your snake hits a wall, it should stop moving and the game should end immediately (think back to the game loop you wrote in snake.c: when does your loop break?). Your next task is to modify your update function so that if your snake's new position has a wall there, then the game ends. Make sure your game ends before your snake actually moves into the wall! We don't want your snake to appear to move through walls. Make sure to think about what update should do if g_game_over is set to 1.

Note: So far, we have been using the default board to run your game, but later on, you will need to handle custom boards, which may or may not have internal walls that your snake can crash into.

Task: Modify your update function so that your snake collides with walls and the game ends if any such collision happens.

☑️ Testing Point B: You should pass tests 3-5 at this point. Your moving snake should hit the wall to the right of the board, which should then cause the game to end.

Getting User Input

You've probably watched your snake crash into the right wall of the board many times now :(

Let's give your snake a way to avoid crashing! Your next task will be to modify your game loop in snake.c and your update function so that you can get user input, and update your snake's position based on the direction specified by the user.

We've given you a function in snake.c, called get_input, that gets arrow key input from your keyboard and converts it into an enum input_key - one of the enums representing a key input. Currently, when you call update in your game loop, you pass in INPUT_NONE as input, and update defaults to shifting the snake to the right. Now, you will now need to change this so that you pass in the output of get_input as the input parameter to update.

Task: Modify your game loop in snake.c so that you can get input from the user, and pass this input into the call to update.

Now that your update function will take in different inputs (as its second to last argument), you will need to modify it once again to handle each of these different inputs, so that your snake's position is updated according to the last input by the user. Note that it is valid for a snake of length 1 to double back on itself (e.g. to go from moving right to moving left.)

Aside: For all three parts of this project, try playing the Google Maps version of Snake to figure out how to handle any edge cases you come up with! This will also be useful when trying to pass any of our tests that test for specific edge cases.

Task: Modify your update function so that it can handle every type of user input, and update the snake data and board accordingly.

☑️ Testing Point C: You should pass tests 6-10 at this point. You should now be able to control your snake's movement using the arrow keys! Your snake should still be able to crash into the wall and end the game.

Food

You almost have a fully playable game! The last component that we're missing is the food (without which, of course, you can't score). Next, you will need to modify your update function further so that if your snake "collides" with a cell on the board that contains food, update does the following:

We have provided you with the function place_food ingame.c to randomly place food on the game board. It's up to you to determine the correct place to call place_food!

For now, your snake does not need to grow when it eats food. You will implement this functionality in Part 3 of this project.

Task: Modify your update and initialize_game functions so that your snake can eat food, and new food is placed on the board whenever the snake eats.

Hint!

Why did we include initialize_game in the task? What does that function have to do with food placement?

☑️ Testing Point D: You should pass tests 11-15 at this point. Your snake should now be able to move around the board and eat food, as long as you don't crash into a wall!

Yay!!! All done with Part 1.

Part 2: Board Decompression (game_setup.c)

Introduction to Run-Length Encoding

Now that we have basic game play set up for our default board, we can now implement the functionality for setting up and running custom boards.

Because the Snake game is more fun when you can play custom levels, your game will have support for loading user-specified board layouts as a command-line argument. Let's think about how you might tell the game what board you want to run!

Humans are good at dealing with readable strings, so our first plan is to encode the state of each square of the board (wall, empty, contains snake, etc.) as a letter. For example, "E" denotes an empty square, "W" denotes a square that contains a wall, etc. If we teach our program how to read these strings, we can give it any board we want.

In this representation of a custom board, the board is represented by a looooong string that contains exactly one character for every cell on the board. For example, an 80x24 board (a typical terminal size, which goes back to the 1970s) would look like this:

WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEW
WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW

This is a 1,920 character string! It would be very tedious to type in if you found an exciting new Snake level posted in a retro gaming magazine. How can we make this representation more efficient?

Clearly, there is a lot of redundancy in this representation: many lines just consist of repeated "W" or "E" characters. To avoid this repetition, we can specify the number of instances of a character, such as "E78" to mean "78 instances of E". This is a common compression technique called a Run-Length Encoding (RLE).

A run-length encoded version of the above board (going row-by-row) yields this much shorter string:

W80W1E78W1W1E78W1W1E78W1W1E78W1W1E78W1W1E78W1W1E78W1W1E78W1W1E78W1W1E78W1W1E78W1
W1E78W1W1E78W1W1E78W1W1E78W1W1E78W1W1E78W1W1E78W1W1E78W1W1E78W1W1E78W1W1E78W1
W1E78W1W80

To make this more readable, let's introduce a delimiter between adjacent rows:

W80|W1E78W1|W1E78W1|W1E78W1|W1E78W1|W1E78W1|W1E78W1|W1E78W1|W1E78W1|W1E78W1|
W1E78W1|W1E78W1|W1E78W1|W1E78W1|W1E78W1|W1E78W1|W1E78W1|W1E78W1|W1E78W1|W1E78W1|
W1E78W1|W1E78W1|W80

As a sanity check against incorrect transcription, we can add the board size at the beginning, encoded as "B" followed by the height (number of rows) and width (number of columns), and we can use the "S" character to encode the starting position of the snake. For this project, we will use "S1" to encode a single-cell snake. While it would be more efficient to encode the snake as a single S, we'll opt to keep things consistent with the encoding scheme, which will hopefully make things easier! You should never start off with more than one snake cell.

Of course, don't forget about grass! In the board string, we use "G" to represent a block with grass. Although a snake will never start on a block with grass, remember that once the game begins, the snake or food can be on the same cell as grass.

The following example represents a 7-row, 10-column board with some internal walls and a snake at the bottom center:

B7x10|W10|W1E4W5|W2E5G2W1|W1E8W1|W1E4W1E3W1|W1E2S1E1W1E3W1|W10

On the screen, this board will look something like this:

example board

Handling Errors

As you may have thought about, there are countless ways that a RLE-encoded board representation can be incorrect. These can range from problems with the actual board, to errors when writing the string.

As a general design principle, we prefer that our programs be able to explicitly handle expected errors, rather than leaving it up to undefined behavior to dictate what our code will do. However, because there are many different errors, our tests only require that you handle a subset of them. (You are welcome to still handle other errors if you wish!)

Errors you are required to handle:

In game_setup.h, you are provided the following enum:

enum board_init_status {
    INIT_SUCCESS, // no errors were thrown
    INIT_ERR_INCORRECT_DIMENSIONS, // dimensions of board do not match dimensions specified by the string
    INIT_ERR_WRONG_SNAKE_NUM, // no snake or multiple snakes are on the board
    INIT_ERR_BAD_CHAR // string contains a character that was unexpected 
};

We guarantee the following about the strings that we will test your implementation against (although once again, you are more than welcome to cover other error cases):

INIT_ERR_INCORRECT_DIMENSIONS
Definition: INIT_ERR_INCORRECT_DIMENSIONS should be returned if the dimensions specified at the start of the encoded string do not match the dimensions of the board specified by the string.

Example:
B3x5|W5|W1E1S1E1W1|W1E3W1|W5

The above string is wrong, because the dimensions at the front specify that the board should have 3 rows, but rest of the string encodes 4 rows worth of cells.

This error also applies to mismatches between the dimensions specified and the number of columns. Think about what a string that is incorrect in this way would look like!

INIT_ERR_WRONG_SNAKE_NUM
Definition: INIT_ERR_WRONG_SNAKE_NUM should be returned if there does not exist exactly one snake cell on the board.

Example:
B3x3|W3|W1E1W1|W3

In the above example, there are no snake cells. This error also applies to boards that start with too many snake cells: try and come up with example strings that demonstrate this!

INIT_ERR_BAD_CHAR
Definition: INIT_ERR_BAD_CHAR should be returned if you encounter an "alien" character. There are many ways to define what an "alien" character is, but we will define it as characters that are not expected to be found in a correctly formatted RLE-encoded board string.

Examples:
B3x3|W3|W1S1Z1|W3
A1-4|W2S1W1

In the first example, we see an unexpected character, "Z" that does represent any valid cell type on our board. In the second example, "-" is used as a delimiter between the height and the width of the board, where we expect to see "x". Additionally, the string starts with "A" instead of "B".

Task: Implement decompress_board_str() with proper error handling. Look at the dropdown for more details (and a helpful hint)!

Function Summary
enum board_init_status decompress_board_str(int** cells_p, size_t* width_p, size_t* height_p, snake_t* snake_p, char* compressed)

Takes in a pointers to board data (cells_p, width_p, height_p), pointer to a snake struct (not used until part 3), and a RLE-encoded string containing the custom board (compressed), and should populate the cells of the board with the correct values that correspond to the string.

This function returns a value of type enum board_init_status. This type represents whether the board decompression was successful, and will be communicated to the main game loop by initialize_game. (This means that the return value of initialize_game should no longer unconditionally be the return value of initialize_default_board!) If no errors arise, then INIT_SUCCESS should be returned.

A Rough Strategy

Below, we have given you some of the steps you could take to write decompress_board_str (but there can be many other valid ways of implementing this function!)

  1. Try and first break down the compressed string into smaller pieces. Think about how you can leverage the delimiter character "|", which separates the encoding of our board by row.

  2. Figure out how to set the dimensions of your board according to what was specified in the prefix of the encoded string. Here, "x" is your magical delimiter—you know that everything between the starting "B" and "x" should be an integer that corresponds to the height of the board, and everything between the "x" and the first "|" delimiter will correspond to the width of the board.

  3. You will likely want to loop through your strings and fill in the board's cells as you go. This means that you will need to store some information regarding where in the board you are, what type of cell you are currently reading, and how many consecutive cells of that type you want to fill in.

Hints!
  1. It may be a good idea to first take a look at C's string library functions (strtok might be of particular use). Many of these functions will likely handle some of the steps you come up with to decode a compressed board string, so make sure to take a look at some of their descriptions!
strtok warning!

strtok cannot parse two different strings at once! If you need to begin parsing one string, switch to parsing another string, and then back to the first string, consider strtok_r!

  1. There are a few different ways to convert strings to integers. A helpful function to look at is atoi! Alternatively, you can also leverage the fact that the hex-encodings of numerical characters range from 0x30 (0) to 0x39 (9), and subtract accordingly.

  2. It is strongly recommended that you design and implement some helpers for decompress_board_str. Parsing and manipulating strings in C can become complicated, so having a few helpers that handle repeated subtasks will help you organize your code. We specifically suggest you write a helper that takes in a row and a column of the board, and returns the corresponding index of the board's cell array.

Task: If you have not already done so, update initialize_game to support decompression! This function should call on decompress_board_str if the string passed into it (board_rep) is not NULL. If it is, then just initialize the default board with initialize_default_board.

Notice that now, initialize_game must not always return INIT_SUCCESS, because decompression can return a number of failure statuses. If necessary, update the return value accordingly! Also make sure you handle these fail statuses in main!

☑️ Testing Point E: You should pass tests 16-28 at this point. You should also be able to create custom boards from compressed board strings, and play through them.

Note: If you're having trouble passing test 28, note that our test runner (c.f. run_test() in autograder.c) does not work by calling the main() function. Instead, it calls update() for each move until the end of the input string. What does that mean for how you write update()?

Woo! Congratulations on finishing part 2!

Part 3: Growing Snake + Game Over

Part 3A: Growing Snake

Before we add any functionality to our snake, let's reconsider how we store our snake's data. Currently, we use global variables to store our snake data. However, as the amount and complexity of the data we want to store about our snake increases, this becomes harder to keep track ofwe have to keep a mental tally of how every function in our program interacts with these global variables. To remedy this, we will move to using a struct to represent snake data, allowing us to pack our snake data together in a much more coherent and readable way.

Caution for global variables

In general, it is best practice to avoid using non-constant global variables. In part 1, we had not yet covered structs, and so we used global variables to aid as a gentler introduction to C. If you would like more details about why global variables are discouraged, see this article.

Take a look at common.h again. We have declared for you a snake struct that we will refer to as snake_t to store all your snake data. Also note that we already define a variable of type snake_t in main for you.

Task: At the bottom of common.h fill in the snake struct with the snake data you declared in 1A. We already reserve the space for a snake_t in snake.c so you can remove the definitions of your snake data in common.c and the declarations for your snake data in common.h. You should only move your snake data to the struct you do not need to move the global variables g_game_over or g_score.

Now modify initialize_game and update to use your snake struct instead of global variables.

We've now got a moving snake with a better data structure… but it is still a very very short snake. It would be nice if our snake would actually grow upon eating food.

This means we'll need to extend our current snake struct definition to handle multiple snake coordinates (one for each cell of the snake). We could do this in a number of ways:

Linked Lists

:warning: This part of the project makes use of your linked list code from Lab 2.

If you have not completed Lab 2, we suggest that you do it now! :warning:

Task: Copy your (fixed!) linked list code from Lab 2. Place it in the (currently empty) linked_list.<c|h> file in your project folder.

Additionally, now that your snake representation will need to use the structs and functions from linked_list.<c|h>, we need to introduce the linked list code to common.<c|h>, where our snake and board are defined.

To do so, add the following line to the top of common.h:

#include "linked_list.h"

Task: Update your snake struct to use a linked list to store the snake's current position, and update your update function to handle this new structure. The snake doesn't need to grow yet, but your code should still pass the same tests.

A Snake that Grows

We're almost there! Now we need to figure out how to 1) extend the snake when it eats food and 2) update the list of positions when the snake moves.

Warning: The snake should only grow if snake_grows is equal to 1. If your snake grows when snake_grows is 0, your code may fail prior tests!

Task: Implement snake growth on food consumption (and provided snake_grows is 1).

Hints on growth

Start by considering the list corresponding to a snake with a length of at least three. When the snake moves one step:

  • Which elements can you keep the same?
  • Which elements should you discard from the list?
  • What do you want to add to / update about the list?

We recommend you draw this out for yourself!

You may also run into a couple of edge cases while testing your implementation:

  • What happens if the snake hits itself?
  • What if the head of the snake moves into the space that the tail of the snake just left?
  • See the note below about the snake backing into itself!

Aside: In the actual snake game, if the snake is length 2 or longer and immediately tries to go back the way it came (backing into itself), the movement is not accepted and the snake continues to move in its previous direction. If the snake is length 1, it should be able to immediately go back the way it came. We would like you to replicate this behavior so that the game doesn't end any time the snake tries to collide with itself by moving backwards!

Last but not least, even though your game loop in snake.c should break out of the loop when the game is over, one could imagine a different implementation that just stops updating the board once the game ends. In these cases, we don't want to be able to continue responding to keystrokes and updating the board after the game has ended. In order to make your update function extensible to multiple different game loop implementations, we'll add one final feature!

Task: If g_game_over is set to true (1), make sure your update function does nothing!

☑️ Testing Point F: You should pass tests 29-39 at this point. Your snake should now grow when it eats food!

Part 3B: Game Over

You've almost got a complete game! Let's wrap it up nicely with a personalized Game Over screen featuring the player's name and their score.

Rendering the Game Over screen

We've provided a few helper functions for rendering the Game Over screen:

render_game_over is located within game_over.c (and game_over.h), which has not been included in compilation up until this point. This is because it isn't in the Makefile. Makefiles tell the compiler how to manage your source code files; you'll learn more about this in Lab 1.

Task: Look through the Makefile and find OBJS = src/game.o src/game_setup.o …. Add game_over.o to the list of objects to be compiled. Then, uncomment render_game_over in game_over.c.

Now, try compiling the project. What happens?

Oh no! It looks like one of our functions assumes that some global variables exist. Look through the code in game_over.c and see if you can figure out why they are needed, and what their types are. (Hint: How do we represent strings in C?)

Task: Add the necessary global variables to common.<c|h> so that the project compiles successfully.

Prompting and reading in user input

We now have variables for the player's name and its length. We still need a way to prompt the user to enter their name and parse the input. Head on over to the read_name function in game.c, where you'll implement this functionality for reading in user input.

First, you want to print out a prompt for the user (e.g. "Name > "). Then, use the read system call to read their response.

Once the user inputs a name with at least one character (apart from hitting enter), then store their name and return.

Task: Implement read_name as detailed in the dropdowns below. Once you have done so, uncomment the following lines in main (snake.c):

// TODO: Implement (in Part 3B)
char name_buffer[1000];
read_name(name_buffer);
void read_name(char* write_into)
Function Summary

Purpose: Prints a prompt to the console asking the user to input their name (e.g. "Name > ", and reads in the user input. If at least one character is inputted, stores the input in the string buffer, write_into. Otherwise, prints an error and reprompts.

Arguments:

  • write_into: String buffer where the user's input will be stored

Returns: Nothing, but if the user enters without typing any other characters, the function should print the following error to the console and repeat the earlier steps of the function.

  • Error: "Name Invalid: must be longer than 0 characters."
Implementation Notes!

Prompting

You may use the printf function to print out your prompt to the console.

File Descriptors

The first argument to the read system call is an integer representing a file descriptor. You'll learn more about file descriptors later in the course, but for now, you can think of it as telling the computer where it should read from. Listed below are three very common file descriptors and the locations that they correspond to. It's up to you to figure out which one you want to read input from!

FD Location Abbreviation
0 Standard Input STDIN
1 Standard Output STDOUT
2 Standard Error STDERR

Length to read

The third argument to the read system call is a value of type size_t denoting how many bytes we want to read. While we ideally do not want to be making assumptions about how long people's names are, we unfortunately need to tell the computer how much to read. To that end, set this value to 1000 in order to be fairly generous with the size of our input.

Erroring appropriately

When the user presses enter without entering any other characters, what will be the string read into the buffer? What will read return? Use this to assess how you will know to print an error and reprompt the player!

Note: At this point, you should be able to see your prompt print, wait for input, and start the game once a newline is entered after a nonzero-length string is entered.

If you do not see your text prompt, try inserting this line of code after you print out your prompt:

fflush(<file descriptor of where you are reading from>);

This is because by default, whatever you wish to write gets added to a buffer, so you sometimes need to forcefully flush the buffer before reading in input!

Socially Responsible Computing: Character Sets

The default string encoding in C, as you might recall from lectures, uses the ASCII standard, which represents each character as one byte. ASCII is the American Standard Code for Information Interchange. However, ASCII is only meant for English and it does not support accents or characters from non-Latin alphabets.

To ensure that we are able to read in all player names that a user could input, and render them correctly, we will need to support a different character encoding. Programmers often turn to extended character sets (most commonly Unicode, and specifically its UTF-8 encodiing) to create more inclusive applications.

When programming for extended character sets, some of the standard library support functions for single character (ASCII) strings will not work as intended. C provides support for multibyte characters through their wchar_t struct, which represents a character in 4 bytes. However, since many extended character sets have variable length characters (for example, UTF-8 encodes characters in 1-4 bytes), using wchar_t leads to wasted space in memory. Moreover, programs often receive UTF-8 text as input that isn't nicely laid out in 4-byte chunks, and must still be able to process it. (As an example, consider a website that processes input from a form: users may put strings like "γνωρίζω" or "❤️😺" into the form!)

UTF-8 characters follow a set byte format based on their length. 1 byte UTF-8 characters are meant to be compatible with ASCII, which means that they always begin with a 0 bit. For 2-4 byte UTF-8 characters, the number of leading 1s indicates the length of the character (in bytes). Each subsequent byte begins with the bits 10. You can find an overview of the format below:

Length (bytes) Encoding (binary)
1 0xxxxxxx
2 110xxxxx 10xxxxxx
3 1110xxxx 10xxxxxx 10xxxxxx
4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

mbslen()

Now, to ensure that we are able to read in any name that a user could input, and render them correctly, we need to implement a function that will allow us to find the number of UTF-8 characters that a user inputs.

Task: Implement mbslen() in mbstrings.c.

mbslen: Counts the number of UTF-8 code points ("characters") in a UTF-8 encoded string.

Synopsis: mbslen counts the number of code points in the UTF-8 multi-byte string stored in bytes until a null character is hit. Note that in this part of the assignment the number of "characters" does not correspond to the number of bytes, but to the number of valid UTF-8 code points; see the UTF-8 encoding table for more details.

size_t mbslen(const char *bytes);

Examples:

char *emojis = "🥳😊😄";
size_t len = mbslen(emojis);  
// len = 3
Hint: Remember bitwise operators?

While we already showed you &, |, and ^, C supports several other bitwise operators as well. Here is a full rundown of operators you may find useful:

  • & is AND, which puts a 1 bit in the result if both input numbers have a 1 in the same position, otherwise it puts 0. For example, 0b0110 & 0b1100 results in 0b0100.
  • | is OR, which puts a 1 bit in the result if either one or both input numbers have a 1 in a position, otherwise it puts 0. For example, 0b0110 | 0b1100 results in 0b1110.
  • ^ is XOR, which puts a 1 bit in the result if exactly one of the two input numbers has 1 in a specific position (but not if both do), otherwise it puts 0. For example, 0b0110 ^ 0b1100 results in 0b1010.
  • ~ is binary negation, which flips all the bits in a number. For example, ~0b0111 results in 0b1000.
  • >> shifts a binary number right by some number of places. For unsigned numbers, this means filling the top bits with zeros and dropping the bottom bits. For example, 0b0111 >> 2 results in 0b0001. For signed numbers, this means filling the top bits with the most significant bit, which you can learn more about here. For example, 0b0111 >> 2 results in 0b0001 and 0b1000 >> 2 results in 0b1110. This difference applies to all unsigned/signed types, including characters, since all values are stored as binary.
  • << shifts a binary number left by some number of places, filling the bottom bits with zeros and dropping the top bits. For example, 0b0111 << 2 results in 0b1100. This is consistent between signed and unsigned numbers.

We recommend casting to unsigned char for this assignment, specifically due to how the right shift operator differs between signed and unsigned numbers.

If you need examples on using bitwise operators in calculations, see this section in part 1!

Task: Once you have successfully implemented mbslen(), call it in snake.c on the name the user inputs and store the result in the appropriate place.

We're almost done! Let's tie it all together and present the game over screen.

Task: Edit end_game(…) and uncomment the lines around and including render_game_over(…) (which implicitly requires the name and its length) so that your snake game renders the final screen when the game is over.

☑️ Testing Point G: You should pass all the tests at this point! You should be able to prompt a user for their name, and then store their name and name's length (according to unicode characters), and see the final game over screen rendered when a game ends.

More on Unicode and Reflection Questions

relevant xkcd

Task: Write your answers to the following three questions in the SRC section of your README. Please note that you will need to consult a classmate for Question 3! You will be given the opportunity to find a partner in your section. If you have special circumstances or encounter any difficulty, please contact us and we will match you with a classmate.

The primary goal of the Unicode character encodings is to accurately represent "all characters in widespread use today." With that, the Unicode Consortium, the organization responsible for maintaining and updating the Unicode standard, must appropriately decide which characters to include.

  1. Who founded the Consortium? Who is represented among the current members, and how might that affect decisions being made for Unicode?

Accounting for every possible language is difficult; the Unicode standard supports many, but not all.

  1. Find a language that is not yet in Unicode, has only recently been supported (e.g., in the last 10 years), or is/was missing important characters. What is/was missing, and how does this affect the users of the language?

Beyond missing languages and characters in languages, Unicode has also faced controversies with representing (and unifying) similar languages. One of the biggest controversies is Han unification, an attempt to unify the Chinese, Japanese, and Korean (CJK) languages into one common encoding.

The Ideographic Research Group (IRG) is responsible for creating and detailing the principles of Han unification. The IRG is made up of experts from China, Japan, North and South Korea, Vietnam, and other regions that have historically used Chinese characters, as well as representatives from the Unicode Consortium. Here is an excerpt from version 1.0 of The Unicode Standard that explains some of the history and rationale of Han Unification. Please read sections 2.1 and 2.2 and feel free to skim the rest of the document. (Note that the IRG is the modern reincarnation of the CJK-JRG mentioned in the document.)

This Unicode Technical Note explains why CJK scripts are unified, while Latin, Greek, and Cyrillic scripts aren’t. Meanwhile, Suzanne Topping of IBM explains some of the downfalls of this decision:

The problem stems from the fact that Unicode encodes characters rather than "glyphs," which are the visual representations of the characters. While the Han root character may be the same for CJK languages, the glyphs in common use for the same characters may not be.

For example, the traditional Chinese glyph for "grass" uses four strokes for the "grass" radical, whereas the simplified Chinese, Japanese, and Korean glyphs use three. But there is only one Unicode point for the grass character (U+8349) regardless of writing system.

  1. For this question, you will need to work with a classmate! Make sure to coordinate so that you outline opposing positions for Part A (e.g. if you outline the 'for' position, your partner defends the 'against' position). Then, come together and discuss Part B!

    In your README, include your:

    • Partner’s CS login
    • Your individual answer to Part A
    • A summary of your discussion for Part B. You should be summarizing both the conclusion you came to, and the process by which you got there (e.g. disagreements you had, ideas you had in common, convincing evidence you encountered, etc.)
      • Feel free to represent this discussion however you think is most effective (e.g. bullet points, as a dialogue, standard written paragraph(s), etc.).
      • This summary can be identical to your partner's.

    a. Outline either the position for Han Unification, or the position against Han Unification. (~2 short paragraphs)

    b. Describe the tradeoff that Unicode has made with this decision. Which "goods" (e.g. accuracy, efficiency, fairness, sustainability) did they choose to prioritize?

You did it!! 😃 Go get some high scores (and post them on Ed) 🎮

Extra Credit

If you're feeling adventurous, try to extend your game with some additional fun features! Make sure to gate your extra credit with a command line flag, special optional characters in board strings, or a #define so that existing tests don't break.

The amount of TA help you can get here may be limited. Please describe your implementation and how to test it in the "Extra Credit Attempted" section of your README.

Extra credit is in no sense required to get an A, except if you’re a graduate student taking CSCI 1310, in which case we require you to achieve 3 extra credit points on this project.

Extra credit's contribution to your grade is limited to 10 points. You're very welcome to attempt more, though, and impress us 😊.

Gameplay

Grow more than one length on each food

Make a bot

Powerups!

Set your creativity free with the following freestyle ideas! Figure out what thesy could mean and add support to the game. There is no right or wrong design here, just FUN 🥳!

Wraparound

Visuals

These ideas will likely involve working with the renderer (render.c), which uses the ncurses and ncursesw libraries (for wide character support).

Better Snake visuals

Bell character on food 🔔

Better UI

Misc

Interesting Level Design

Handing In Your Code

You will hand in your code using Git. In the snake/ subdirectory, you MUST fill in the text file called README.md given in the stencil. The README.md file will include the following:

  1. Any design decisions you made and comments for graders, under "Design Overview". If there's nothing interesting to say, just list "None".
  2. Any collaborators and citations for help that you received, under "Collaborators". CS 300 encourages collaboration, but we expect you to acknowledge help you received, as is standard academic practice.
  3. Your answers to the SRC reflection questions under "Responsible Computing Questions".
  4. A description of your extra credit work (if any) under "Extra Credit Attempted".
  5. Notes on approximately how long it took you to complete the project. We won't use this for grading, but we collect the data to calibrate for future years.

Here's a Markdown template for your README. We strongly recommend that you use this template! (This template is already given to you in the stencil.)

CSCI 0300 Project 1 - Snake
===========================

## Design Overview:

*TODO!*

## Collaborators:

*TODO!*

## Responsible Computing:

*TODO!*

## Extra credit:

If you choose to do extra credit, detail it here.

## How long did it take to complete Snake?

*TODO!*

Grading breakdown:

Your submission will not be graded until you configure the grading server. You should have used the grading server for Lab 0 already.

If you were registered for CS 300 on the first day of classes, you should have received credentials by email.

If you only recently registered or you're shopping the course but haven't registered, you won't have an account yet. In that case, please complete this form and wait until you receive your credentials. We will create accounts based on the form entries at 8pm every day.

Step 1: Log into the grading server with the password you received. Once you're logged in, enter your GitHub username (at the top).

Step 2: Add the repository URL under the "Project 1: Snake" category. Click "Project 1: Snake" to initiate a fetch of your repository from GitHub.

Note: If your GitHub account is not linked to your Brown email address, the grading server will give you a command to run to verify your project repository.

Step 3: You should see your commit listed on the "Project 1: Snake" page. You can use the buttons below the commit list to test your code in our grading environment. Make sure it still passes all the tests!

Note: If the commit you want graded is not your most recent one, you should flag the one you want graded and the grading commit will be updated after the autograder finishes.

Congratulations, you've completed the first project! :clap:


Acknowledgements: This project was developed for CS 300 by Benjamin Lee, Casey Nelson, Livia Zhu, Rhea Goyal, Sreshtaa Rajesh, Paul Biberstein, Richard Tang, Sophia Liu, Vic (Shihang) Li, and Malte Schwarzkopf.