Project: 'snek.c'

"snek.c" is an ambitious console based snake game implementation using networking, systems calls, data structures, standard I/O, and more in unified human-centered effort. This page will serve as your guide through this challenging assignment.

My "snek.c" example implementation uses 373 lines of C code (loc) drafted over 5 real-time days and a total of about 4 hours of development. If I were to completely adhere to my personal coding style, I would've factored the project into multiple .c and .h files, but 373 lines is a manageable length and I will leave the file structure to student discretion. Students should budget 40+ hours for each of two partners, and expect greater than 500 loc.

For those of you not familiar with snake, there is a much cuter version available from Google and I will additionally distribute (and maintain as bugs are discovered) a reference binary so you can see how an example solution could run without being "spoiled" with source access.

Components

Components

I recommend constructing independent executable programs that implement, loosely speaking, the functionalities of each of these sub-tasks, then composing them into a comprehensive program that implements the snake game. These are outlined as "check-ins" within each section.

Main

We have already shown a main function handler for a .c file to implement server and client operations.
int main(int argc, char const *argv[])
{
    printf("%s, expects (1) arg, %d provided", argv[0], argc-1);
    if (argc == 2)
    {
        printf(": \"%s\".\n", argv[1]);
    } else {
        printf(".\n");
        return 1;
    }
    
    if (argv[1][1] == 's')
    {
        printf("Starting server...\n");
    }
    
    if (argv[1][1] == 'c')
    {
        printf("Starting client...\n");
    }
    
    if (argv[1][1] == 'h')
    {
        printf("HELP: Usage:  -s for server, -c for client\n");
    }
    
    return 0;
}

This main is fairly full featured, but you may wish to include some additional operations in it. For example, in the traditional variant of snake, while the snakes position is completely deterministic based on user inputs, the location of snacks that the snake must eat to grow larger and yield a correspondingly high in game score are placed randomly. Customarily, random number generation in C is performed using rand() after setting a random 'seed' using the current time. Read more here.

DO NOT USE rand() FOR SECURITY BASED APPLICATIONS.

#include <time.h>
#include <stdlib.h>

srand(time(NULL)); // Initialization, should only be called once
int r = rand();    // A pseudo-random integer in [0,RAND_MAX]

You may wish to initialize random number generation in main() to ensure it only runs once, and the reference implementation does so, though there are many other implementation options, including implementations that don't use rand() at all.

Speaking of including libraries, the reference implementation uses quite a few of them, and most would be difficult to do without, as well as defining a number of constants.

#include <sys/socket.h> // for socket()
#include <arpa/inet.h>  // for add6
#include <stdio.h>      // for printf()
#include <unistd.h>     // for read()
#include <stdlib.h>     // for malloc()
#include <string.h>     // for strlen()
#include <time.h>       // for time()
#include <fcntl.h>      // for fcntl()

// sockets
#define PORT 0xC271     // get it? CS 271?
#define DOMAIN AF_INET6 // ipv6
#define LOOPBACK "::1"  // "loopback" ipv6 - network locally

// debug
#define VERBOSE 1
#define BUFF 16

// booleans
#define TRUE 1
#define FALS 0
typedef int bool;

// gameplay
#define HIGH 23
#define WIDE 80

#define SNAK '&'
#define SNEK 'O'

#define REDO 'r'
#define QUIT 'q'

#define FORE 'w'
#define BACK 's'
#define LEFT 'a'
#define RITE 'd'

// shorter names for addresses and casts
typedef struct sockaddr_in6 *add6;
typedef struct sockaddr *add4;

Of these, <string.h> is probably the most optional, but offers a variety of niceties when trying to read user input. <fcntl.h> is the only one we haven't used before, and will play a critical role in the client implementation, though this role can be accomplished by alternative implementations (most likely with regard to sockets). Example code is provided for using <fcntl.h>

These defines are fairly well decomposed into networking and gameplay roles, though some features, like booleans, are just generally useful.

Check-in 0

To test that your main is ready to support all critical tasks, just configure it to call simple wrapper functions with print statements as stand-ins for later networking and gameplay tasks. Once you have a main function that you can demonstrate does the required work of conditionally calling different functions based on command line arguments, you have a basis from which to develop the remaining functionalities of snek.c, and can always return to include, define, or initialize anything else you may need. In the reference implementation, this was implemented as a single main function with the following declaration:

int main(int argc, char const *argv[]);

Client

For this project, the role of the client is to capture user input in separate console window from where the game is displayed and transmit this user input to the gameplay server. Doing so offers a number of synchronization challenges, and doing so well can greatly reduce the challenges of implementing the gameplay server.

Networking (Client-side)

The primary role of networking on the client side is to send the appropriate inputs to the server. There are a variety of ways to implement this task, but the reference implementation sends a messages to the server as close to one second after the previous message as possible that contains a single byte encoding the ascii value of the most recently input valid command. This is similar in spirit to the provided sample code in client.c:
int sock;
struct sockaddr_in6 address; 

sock = socket(DOMAIN, SOCK_STREAM, 0);

address.sin6_family = DOMAIN; 
// "...only the port field needs to be formatted with htons()"
address.sin6_port = htons(PORT); 
// "::1" is IPv^ "loopback" address
inet_pton(DOMAIN, "::1", &address.sin6_addr); 

connect(sock, &address, sizeof(address));

write(sock, argv[1], strlen(argv[1]));

This code implements a single write-to-socket event, and writes a string of arbitrary length. However, it is otherwise similar to the required tasks of the client.

While this code is provided as is, it could be improved in many ways - in the reference implementation, the socket, once created, is passed to a second function that handles the repeated sends in a loop, in which case the socket either needs to be malloc'ed or the looping function must be nested. Similarly, many of the setup operations here are common with the server and may be suitable for code factorization. Further, this code contains a number of lines triggering gcc warnings that should be addressed with casts. Lastly, many of the socket library functions can return error cases and ought to be error checked, to exit gracefully rather than drawing a segmentation fault. While a suitable base implementation, this sample code should be taken more as inspiration than as a partial solution.

Check-in 1

To test that your client is ready to support networking independently of the gameplay implementation, just configure it to send messages to another socket once per second. You can use the sample server.c socket provided for the course as a way to show information is being passed over a socket. In the reference implementation, this was implemented as a single client-specific function with the following declaration:

int client();
Additionally, the client and server networking tasks shared a common utility function:
void get_sock(int *sock, add6 address, bool is_server);
The role of the client() function is primarily to configure and connect a socket that is passed as the sole argument to the client gameplay loop function cloop().

Gameplay (Client-side)

The primary role of gameplay on the client side is to read in user input and prepare it to be transmitted over the network. There are a variety of ways to implement this task, but all must contend with the central challenge that a message must be sent once per second, so the system may not naively block on a fgets() or similar blocking input call, and at some point the user input must compared against some acceptable list of inputs. In the reference implementation, the most recent valid input is maintained, and updated if a new valid input is provided. This is the single byte that is sent over the network every second. Of the valid inputs, only one, the quit input, yields any specific actions on the client.

You are welcome to choose what inputs you would like to use for snek, but at a minimum must support four directional inputs. The reference implementation supports six: the four directions, quit, and restart.

The core tension in the client is that gameplay wants to be interrupt (that is, user) driven, and networking wants to be timer driven. We can resolve this in part by configuring reads from stdin to be non-blocking - that is, to always return within one second of being called, and simply return with no new data if there is no new data to return. There are plenty of ways to do this. The fnctl() function is used in the reference implementation to configure stdin to be non-blocking. Read more here.

char buf[20];
fcntl(0, F_SETFL, fcntl(0, F_GETFL) | O_NONBLOCK);
sleep(4);
int numRead = read(0, buf, 4);
if (numRead > 0) {
	printf("You said: %s", buf);
}

Synchronization could be handled in networking components of either the client or the server, or handled at display time on the server, but client side gameplay is the simpliest and earliest site to resolve the interrupt/timing conflict, and allows the rest of the code to proceed using standard blocking calls. An early version of the reference implementation used setsockopt() to configure the socket used by the server to capture commands from the client, but this implementation yielded undesirable complexity on server side.

Check-in 2

To test that your client is ready to support gameplay independently of the networking implementation, just configure it to print to console, once per second, the most recent input character. Play around with it a bit and get a sense for how the console works, especially with respect to input lines of text and the enter key. Once you can accept input at any time, and generate output on a timer, you can simply connect the gameplay and networking elements and have a client that transmits game inputs to the game server. In the reference implementation, this was implemented as a single client-specific function with the following declaration:

int cloop(int *sock);
Additionally, one small helper function was used to maintain the input buffer in cloop(), but a string library function such as memset() would also be suitable. cloop() contains a single while(1) loop containing two main if statements. Using functions from <time.h>, the loop ran at least once per second and whenever receiving new user input. If the loop ran one second or more since last sending a message to the client, a new message would be sent to the server.

Server

For this project, the role of the server is to accept network inputs sent by the client and to maintain, update, and display the game state. Doing so offers a number of programming challenges.

Networking (Server-side)

The primary role of networking on the client side is to receive pre-processed inputs from the client. There are a variety of ways to implement this task, but the reference implementation simply waits to receive a message from the client then implements a single gameplay update before blocking again and waiting for another message. This is similar in spirit to the provided sample code in server.c, which is markedly more complex than client.c:
int sock; 
struct sockaddr_in6 address; 

sock = socket(DOMAIN, SOCK_STREAM, 0);

address.sin6_family = DOMAIN; 
// "...only the port field needs to be formatted with htons()"
address.sin6_port = htons(PORT); 
address.sin6_addr = in6addr_any; 

int opt = 1, s = sizeof(opt); // True
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, s);
	
bind(sock, &address, sizeof(address));
listen(sock, 1);
int addrlen = sizeof(address); 
int connection = accept(sock, &address, &addrlen); 
char buffer[BUFF_SIZE] = {0}; 
read(connection, buffer, BUFF_SIZE); 

printf("%s\n",buffer ); 

This code implements a single read-from-socket event, and reads a string of arbitrary length. However, it is otherwise similar to the required tasks of the client. Compositionally, this code assembles a socket, sets socket options using helpful boilerplate to enable socket reuse on the same ports for testing, then completes the bind-listen-connect process to create a connection from which data may be read.

All the considerations of the provided client.c code also apply to this code.

Check-in 3

To test that your server is ready to support networking independently of the gameplay implementation, just configure it to print received messages and verify that it can do so at approximately a rate of once per second. You can use the sample client.c socket provided for the course as a way to show information is being passed over a socket. In the reference implementation, this was implemented as a single server-specific function with the following declaration:

int server();

Additionally, the client and server networking tasks shared a common utility function:

void get_sock(int *sock, add6 address, bool is_server);

The role of the server() function is primarily to configure a socket and receive a connection that is passed as the sole argument to the client gameplay loop function sloop().

Gameplay (Network-side)

The gameplay server is perhaps the most intensive part of this project as it requires maintaining an internal state and responding to input over network. To make it more manageable, I have decomposed it into three sub-tasks of roughly equal intensity, that are then composed within the sloop() function called from server() which we can think of as producing the gameplay window given both user commands and internal state.

In brief, the snake is directed by the player to "eat" the food on the screen, which causes the snake to increase in size. The goal of the game is for the player to direct the snake to increase in size until filling the entire game window without causing the snake to collide with either a boundary or the snake's own tail.

The standard snake gameplay requires maintaining of elements of gameplay state in order to latter determine the effect of an command and to render the current gameplay state to console which displays a number of gameplay elements:

Each of these elements is subject to certain requirements.

Bounding elements are statically maintained in the highest and lowest indexed locations of both rows and columns, and are unalterable through gameplay. Food item and snake elements may not occupy any space wherein a bounding element is present. Food item elements, placed by the gameplay server, must be restricted not to be placed in the same grid location as a bounding element. Snake elements, as they are directed by user input, may be directed into grid locations in which bounding elements are present, but doing so results in the game terminating in a loss for the player.

Food item elements are placed randomly at any point within the grid unoccupied by another gameplay element, whether a bounding elements or a snake element. Snake elements, as they are directed by user input, may be directed into grid locations in which a food item element is present. If a snake element is moved onto the grid location of a food element, the gameplay state is updated in the following way: a new snake element is created at the location of the food element, and all other snake elements are maintained in their current location. Additionally, a new food element is created at a randomized location where no other gameplay elements are present. If there are no locations in the grid that are unoccupied by either bounding or snake elements, the game concludes with a victory for the player.

Snake elements placed initially by the gameplay server in either a fixed or randomized starting location at from that point on set according to user input and then translate through the space in accordance with the user input direction. While moving, the snake may move through grid spaces with food items in order for the snake to grow larger (by adding a single snake element) but may not move through spaces already occupied by snake or boundary elements.

Game State

As bounding elements are static, in the reference implementation their location is only considered during rendering, and all gameplay is modelled as if occuring in a 78-by-21 sized grid with no bounding elements present.

In the reference implementation, the food item location is maintained as a single integer array of length two capturing the coordinates of the food item in the game space.

In the reference implementation, the location of all snake elements is maintained in a linked list where the most recently added element is at the head of the list and the least recently added element is at the tail of the list. Doing so enables tracking which elements should be updated. Like the food item element, snake elements are tracked as an integer array of length two.

With bounding elements untracked, the reference implementation maintains the entire gameplay state in a single data structure holding both the location of the food item as an integer array of length two and the location of the snake as a custom linked list structure. For the snake, this means all state updates can be performed with head append and tail pop operations.

Check-in 4

To test that your server is ready to maintain gamestate, implement some data structure capable of maintaining all required gameplay elements. A list that implements head append, tail pop, and contains checks meets all the needs for game state storage. This is a subset of the operations supported by Pythonic Lists, so the function templates and testing functions used for <plist.h> may all see reuse here, though the contains check in plist would have to be modified not to exit on failure or other gameplay stages could be modified not to rely on contains checks.

#include <plist.h> // for insert(0) (head append), pop(), index() (contains)

Of note, an integer array of length two is the same size as a void pointer on most modern computers. If you do not wish to use <plist.h>, here are the function declarations from the reference implementation:

typedef struct list_struct *list;
struct list_struct
{
	int pair[2];
	list rest;
};

// head append
list cons(int *pair, list rest);

bool isin(int x, int y, list head);

// game state is the x,y's of a snek and the x, y value of a snak
// this would be the same thing as the list
typedef list game;

// pop tail
void popt(game curr);

Commands

The reference snek implementation supports six total commands: four directional commands, a quit command, and a restart command. By default, these are mapped to "wasdqr" but are implemented using #define for customizability. The directions correspond to changes within the game state, and the other correspond to changes to the program state.

If the game state is not initialized, the previous game state was a player win or loss, or a restart command is received, the existing game state is discarded and random locations for the food item element and a single snake item element are set. In the reference implementation this is factored into an init() function. As a nicety, in the reference implementation the food item is placed randomly of all game spaces and the snake is placed randomly within the subset of the gamespace closer to the center point than to the boundary elements.

If the quit command is received, the program exits gracefully by returning up the call stack to main, which should return 0. When this occurs, the client should already have exited gracefully in the same manner immediately after transmitting the quit code.

Each time the gameplay server updates, the snake elements should be updated as follows: a new snake element should be created relative to the most recently added snake element, either in the same row and an adjacent column, or the same column and an adjacent row, according to whether the snake is directed to move up, down, left, or right. There are three cases that then must be checked:

Check-in 5

To test that your server is ready to process commands is the most non-trivial of the standalone tasks, but can be implemented using pl_print as a temporary rendering solution prior to implementing the gameplay display if using the plist implementation. In this case, commands can be captured at the command line directly in the gameplay loop, rather than from a network socket, and the internal gamestate can be monitored in response to certain input directions. The command processing task is complete when internal data structures can be updated conditionally based on both internal game state and input commands.

In the reference implementation, the main body was implemented in a single move() with a helper init() function.

game init();
game move(char dirc, game curr);

init() contains only random number generation and two calls to cons() to return a randomized starting game state. move() contains two if blocks: the first which calculates the new head location of the snake given inputs, and the second which calculates the updates to snake and food item elements (including gameplay loss or victory) based on this new location.

Display

The reference snek implementation renders the gamestate in a console window of the historical default console size, 80 characters wide by 24 characters tall.

|------------------------------------------------------------------------------|
|                                                                              |
|                                                                              |
|                                                                              |
|                                                                              |
|                                                                              |
|                                                                              |
|                                                             O                |
|                                                                              |
|                                                                              |
|                                                                              |
|                                                                              |
|                                                                              |
|                                                                              |
|                               &                                              |
|                                                                              |
|                                                                              |
|                                                                              |
|                                                                              |
|                                                                              |
|                                                                              |
|                                                                              |
|------------------------------------------------------------------------------|

The traditional snake implementation proceeds on simple two-dimensional grid. This grid can initially be thought of as a sparse n-by-m matrix which for this project is intended to be traditional console size is 80-by-23 with the last, 24th line left unpopulated so as not to clip the gameplay with a final, terminating new line. In the reference implementation, these gameplay elements are displayed as follows:

Check-in 6

To test that your server is ready to display game state, it suffices to implement a single function that takes as input a gamestate and renders it to the console with one or more calls to printf(). This can be implemented in part before considering gamestate storage as a NULL gamestate should be renderable, simply without displaying any snake and food item elements.

In the reference implementation, a single function used a doubly nested loop that assembled and then printed the game window line-by-line to console with a single internal working buffer large enough to hold one line at a time. However, many alternative implementations would also suffice and could make other aspects of the gameplay server implementation simpler.

void show(game curr);

Completion

With all functionalities of the snake game implemented, the final stages are to compose the elements together to create a single cohesive gameplay experience.

In the reference implementation, the client proceeds from main() to client() which uses void get_sock(); to create a socket that is passed to cloop(). In the reference implementation, these are nested as return calls, such that cloop() returns when encountering a quit command, client() returns the return value of cloop(), and main() returns the return value of client(). This also allows non-zero return values, denoting errors, to be passed up the call stack.

In the reference implementation, the server proceeds similarly, from main() to server() which uses void get_sock(); to create a connection that is passed to sloop(). These are nested as return calls as well. sloop() calls an intermediate helper function:

game play(char dirc, game curr);

And play() organizes the calls to init(), move(), and show().

This project is well suited to extensions, such as adjustable framework, multiplayer support, multi-food / power-up items, non-rectangular game worlds, non-euclidean spaces, and more. Innovate to your hearts desire!

|------------------------------------------------------------------------------|
|                                                                              |
|                                                                              |
|                                                                              |
|                                            &                                 |
|                         O                                                    |
|                         O                                                    |
|                         O                                                    |
|                         O                                                    |
|                         O                                                    |
|                         O                                                    |
|                         O                                                    |
|                         O                                                    |
|                         O                                                    |
|                         O          O                                         |
|                         O          O                                         |
|                         O          O                                         |
|                         O          O                                         |
|                         O          O                                         |
|                         O          O                                         |
|                         O          O                                         |
|                         OOOOOOOOOOOO                                         |
|------------------------------------------------------------------------------|