Back to Gallery
Prev

Console-Based Snake Game

next

December 5th, 2023

The Project

The objective of this project was to develop a dynamic console-based game using C++ and object-oriented design principles. The game is inspired by the classic snake game, where a controllable "snake" character moves around a game board, collecting food and growing with each item consumed. The aim is to collect the maximum amount of food while avoiding self-collision.

My Role

This project was primarily an independent effort. I was responsible for developing various aspects of the game and conducting testing. In the later stages of development, a partner joined to assist with player movement functionalities.

Game Development

Adhering to object-oriented programming principles, the project was divided into distinct modules: object data, player data, food data, and game mechanics. While most classes contain simple getter functions to facilitate development, some are more complex.

Player

The Player class handles player movement, checks interactions between food and the player, and increases the snake's body length. The initial steps involved creating constructors and destructors. Two key functions in this class are:

  • movePlayer(): This function takes input from the game mechanics class and updates the snake's head position accordingly.

    void Player::movePlayer()
                    {
                        // PPA3 Finite State Machine logic
                        objPos currHead;
                        playerPosList->getHeadElement(currHead);
                    
                        if (myDir == UP){
                            currHead.y--;
                            if (currHead.y < 0){
                                currHead.y = (mainGameMechsRef->getBoardSizeY())-1; 
                            }
                        }
                        if (myDir == LEFT){
                            currHead.x--;
                            if (currHead.x < 0){
                                currHead.x = (mainGameMechsRef->getBoardSizeX())-1;
                            }
                        }
                        if (myDir == RIGHT){
                            currHead.x++;
                            if (currHead.x > (mainGameMechsRef->getBoardSizeX())-1){
                                currHead.x = 0;
                            }
                        }
                        if (myDir == DOWN){
                            currHead.y++;
                            if (currHead.y > (mainGameMechsRef->getBoardSizeY())-1){
                                currHead.y = 0;
                            }
                        }
                        if (myDir != NONE){ // If the player starts moving the game has started
                            mainGameMechsRef->setStartFlag();
                        }
                    
                        // When player moves, add new head and remove tail to maintain snake length
                        playerPosList->insertHead(currHead);
                        playerPosList->removeTail();
                    
                        // If player collides with itself, game is lost
                        if ((checkSelfCollision() == true) && (mainGameMechsRef->getWinFlagStatus() != true)){
                            mainGameMechsRef->setLoseFlag();
                        }
                    }
                    
  • checkFoodConsumption(): This function compares the snake's position with all food items on the board, returning different values based on the type of food encountered.

    int Player::checkFoodConsuption()
                    {
                        // returns 0 if no collision, 1, if collision with regular food, 2 if special food
                        objPos currHead;
                        objPos foodpos;
                        objPosArrayList* foodbucketlist = mainFoodRef->getFoodPos();
                        playerPosList->getHeadElement(currHead);
                        for (int i = 0; i < foodbucketlist->getSize(); i++){
                            foodbucketlist->getElement(foodpos, i);
                            if ((foodpos.x==currHead.x)&&(foodpos.y==currHead.y)){
                                if (foodpos.symbol == '$'){
                                    return 2;
                                } else {
                                    return 1;
                                }
                    
                            }
                        }
                        return 0;
                    }

Food

The Food class is the most intricate class in the game. Its constructor creates an array list of objects called foodBucket and populates it with temporary food items. The most complex function in this class, generateFood(), places up to five food objects on random game board spaces, excluding those occupied by the player. It also generates two different types of food items. Here is the function along with comments for clarity.

void Food::generateFood(objPosArrayList* blockOff) // changed this
            {
                srand(time(NULL)); // Seeding with time to randomize intial food position
                int xrand, yrand;
                bool matchflag = false;
                bool unique;
                objPos tempBody; // Temporary object to hold current player body
                objPos tempFood; // Temporary object to hold current food item
                objPos foodComp; // Temporary object to compare food position
            
            
                // Check if there is valid space for displaying 5 food items
                // If not 5 spots are not available: create max possible food items
                int available = ((mainGameMechsRef->getBoardSizeX())*(mainGameMechsRef->getBoardSizeY())) - (blockOff->getSize());
                if (available < 5){
                    tempFood.setObjPos(0, 0, 'o');
                    for (int i = 0; i < 5; i++){
                        foodBucket->removeTail();
                    }
                    for (int i = 0; i < available; i++){
                        foodBucket->insertHead(tempFood);
                    }
                }
                
                // For each food item in bucket, get and set valid random coordinates
                for (int i = 0; i < foodBucket->getSize(); i++){
                    foodBucket->getElement(tempFood, i);
                    while (true){
                        unique = true;
                        xrand = rand() % (mainGameMechsRef->getBoardSizeX()); // Get random number excluding border in x axis
                        yrand = rand() % (mainGameMechsRef->getBoardSizeY()); // Get random number excluding border in y axis
                        // Check if coodinates are valid compared to player body
                        for (int k = 0; k < blockOff->getSize(); k++){
                            blockOff->getElement(tempBody, k);
                            if ((tempBody.x == xrand)&&(tempBody.y == yrand)){
                                unique = false;
                                break;
                            }
                        }
                        // Check if coordinates are valid compared to the other food items
                        for (int k = 0; k < i; k++){
                            foodBucket->getElement(foodComp, k);
                            if ((foodComp.x == xrand)&&(foodComp.y == yrand)){
                                unique = false;
                                break;
                            }
                        }
                        // If passes the above checks, set temporary object to these new coordinates
                        if (unique == true){
                            tempFood.x = xrand;
                            tempFood.y = yrand;
                            // Set two items to special food characters
                            if (i == 0 || i == 1)
                                tempFood.symbol = '$';
                            break;
                        }
                    }
                    // Add the temporary food item to the item list and remove the refrence item
                    foodBucket->insertHead(tempFood);
                    foodBucket->removeTail();
                }
            }

Main Project

The main.cpp file integrates all the classes. The game board is rendered using a 2D char array, which was chosen for its simplicity and clarity in pointing out board locations. The main function consists of six elements: Initialize(), GetInput(), RunLogic(), DrawScreen(), LoopDelay(), and CleanUp(). The RunLogic() function is the largest, encompassing food generation, player movement, food interactions, win and lose conditions, and scorekeeping. Here is that function.

void RunLogic(void)
            {
                objPos foodpos;
                objPosArrayList* foodbucketlist = myFood->getFoodPos();
                objPosArrayList* playerBody = myPlayer->getPlayerPos();
                
                // Clear start screen and generate first food
                if (hasrun == 0 && gmpointer->getStartFlagStatus() == true){
                    for (int i = 1; i < 16; i++){
                        disp[1][i] = ' ';
                    }
                    
                    myFood->generateFood(playerBody); // Generate new positions for food items
                    for (int k = 0; k < foodbucketlist->getSize(); k++){
                        foodbucketlist->getElement(foodpos, k);
                        disp[foodpos.y][foodpos.x] = foodpos.symbol; // Display all food items on the board
                    }
                    
                    hasrun = 1; // Start screen has run
                }
            
                // Clear previous player position
                objPos tempBody;
                for (int k = 0; k < playerBody->getSize(); k++){
                    playerBody->getElement(tempBody, k);
                    disp[tempBody.y][tempBody.x] = ' ';
                }
            
                // Update player position
                myPlayer->updatePlayerDir();
                myPlayer->movePlayer();
            
                // Interact with food
                int foodcon = myPlayer->checkFoodConsuption();
                if (foodcon == 1 || foodcon == 2){
                    for (int k = 0; k < foodbucketlist->getSize(); k++){
                        foodbucketlist->getElement(foodpos, k);
                        disp[foodpos.y][foodpos.x] = ' '; // Clear old food positions
                    }
                    myFood->generateFood(playerBody); // Generate new positions for food items
                    for (int k = 0; k < foodbucketlist->getSize(); k++){
                        foodbucketlist->getElement(foodpos, k);
                        disp[foodpos.y][foodpos.x] = foodpos.symbol; // Display all food items on the board
                    }
                    if (foodcon == 1){
                        // Normal food conditions
                        gmpointer->incrementScore(1);
                        myPlayer->increasePlayerLength(1);
                    } else {
                        // Special food conditions
                        gmpointer->incrementScore(3);
                        myPlayer->increasePlayerLength(5);
                    }
                    
                }
            
                // Display new player position
                for (int k = 0; k < playerBody->getSize(); k++){
                    playerBody->getElement(tempBody, k);
                    disp[tempBody.y][tempBody.x] = tempBody.symbol;
                }
            
                // Debugging inputs
                // Input h sets to win state
                if (gmpointer->getInput() == 'h'){
                    gmpointer->setWinFlag();
                }
                // Input j sets to lose state
                if (gmpointer->getInput() == 'j'){
                    gmpointer->setLoseFlag();
                }
            
                // Win Screen
                if ((gmpointer->getWinFlagStatus() == true)){
                    char msg[37] = "  Congratulations      you WON!     ";
                    for (int i=0; i<8; i++){
                        for (int j=0; j<18; j++){
                            disp[i][j] = ' ';
                        }
                    }
                    for (int i=0; i < 18; i++){
                        disp[2][i] = msg[i];
                        disp[4][i] = msg[i+18];
                    }
                }
                // Lose Screen
                if ((gmpointer->getLoseFlagStatus() == true)){
                    char msg[37] = "      Sorry           you LOST!     ";
                    for (int i=0; i<8; i++){
                        for (int j=0; j<18; j++){
                            disp[i][j] = ' ';
                        }
                    }
                    for (int i=0; i < 18; i++){
                        disp[2][i] = msg[i];
                        disp[4][i] = msg[i+18];
                    }
                }
            
                // Game end condition:  if exit flag is true, end game. 
                if ((gmpointer->getExitFlagStatus() == true)){
                    gmpointer->setExitTrue();
                }
                
                // Game end condition: if you score 2000, you win (purposely unreachable)
                if (gmpointer->getScore() == 2000){ 
                    gmpointer->setWinFlag();
                }
            
                // Clear input
                gmpointer->clearInput();
            }

Final Product

Here is a recording of the game in action.