5
\$\begingroup\$

I recently started learning C++ and have made a very simple Console-based Snake Game, I would like to have some feedback on improvements.

You might notice the obvious delay in clearing and printing of the screen, as I did not use system("cls") since it was stated as 'evil' and 'bad'.
I printed a bunch of newlines instead as suggested in a forum that I had read.
(Didn't use NCurses since it was an overkill for my simple snake game)

#include <iostream>
#include <string>
#include <limits>
#include <conio.h>

#include <thread>
#include <chrono>

enum Direction2D {
    UNDEFINED = 0,
    LEFT,
    RIGHT,
    UP,
    DOWN
};

bool IsOppositeOf(const Direction2D& first, const Direction2D& second) {
    // Feedback for a more elegant approach, thanks.
    bool isOpposite = false;
    if (first == LEFT && second == RIGHT) {
        isOpposite = true;
    }
    else if (first == RIGHT && second == LEFT) {
        isOpposite = true;
    }
    else if (first == UP && second == DOWN) {
        isOpposite = true;
    }
    else if (first == DOWN && second == UP) {
        isOpposite = true;
    }

    return isOpposite;
}

struct Vector2D
{
    int x;
    int y;
};

// Since we are printing downwards, 'up' would technically be down.
Vector2D const Up = { 0, -1 };
Vector2D const Down = { 0, 1 };

Vector2D const Left = { -1, 0 };
Vector2D const Right = { 1, 0 };

Vector2D const Zero = { 0, 0 };

#pragma region Vector2D_Operators

bool operator ==(const Vector2D& first, const Vector2D& second) {
    return (first.x == second.x) && (first.y == second.y);
}

Vector2D operator +(const Vector2D& first, const Vector2D& second) {
    return { (first.x + second.x), (first.y + second.y) };
}

Vector2D operator -(const Vector2D& first, const Vector2D& second) {
    return { (first.x - second.x), (first.y - second.y) };
}

#pragma endregion

bool gameOver;

int playerScore;

int width;
int height;

Vector2D snakeHeadPosition;
Vector2D currentFruitPosition;

Direction2D currentSnakeMovingDirection;

// I'm assuming the length of the tail doesn't go beyond 256.
// Accepting feedback on how I could do this more elegantly.
Vector2D tailPositions[256];

int tailLength;

#pragma region Util

void ClearCinInput() {
    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

template <typename T>
void RequestForValidInputValue(T &receivingArg, std::string errorMsg) {
    // Ask for input while the input is not of the correct type.
    while (!(std::cin >> receivingArg)) {
        std::cout << errorMsg << std::endl;
        ClearCinInput();
    }
}

#pragma region Request_For_YesNo

void PrintRespectiveYesNoTextByFirstRun(bool firstRun) {
    if (firstRun) {
        std::cout << "(Y/N)" << std::endl;
    }
    else {
        std::cout << "Please input either 'Y' or 'N'." << std::endl;
    }
}

bool CheckIfValidYesNoInput(std::string input) {
    return (input == "Y" || input == "N");
}

bool AskUserForYesNo() {
    std::string userInput = "";

    bool loopFirstRun = true;

    // Ask for input again while the input was not a confirmation.
    do {
        PrintRespectiveYesNoTextByFirstRun(loopFirstRun);

        RequestForValidInputValue(userInput, "ERROR: Please input 'Y' or 'N' ");
        loopFirstRun = false;
    } while (!CheckIfValidYesNoInput(userInput));

    return userInput == "Y";
}

#pragma endregion

void ClearScreen() {
    // SLOW
    std::cout << std::string(100, '\n');
}

void PlaceFruitRandomlyInPlayingField() {
    currentFruitPosition = { rand() % width, rand() % height };
}

#pragma endregion

void HandleFruitEating() {
    ++tailLength;
    ++playerScore;
    PlaceFruitRandomlyInPlayingField();
}

bool SnakeEatingFruit() {
    return snakeHeadPosition == currentFruitPosition;
}

bool SnakeHeadTouchedTail() {
    bool touchedTail = false;

    for (int i = 0; i < tailLength; ++i) {
        if (tailPositions[i] == snakeHeadPosition) {
            touchedTail = true;
            break;
        }
    }
    return touchedTail;
}

bool SnakeHeadTouchedBorder() {
    int x = snakeHeadPosition.x;
    int y = snakeHeadPosition.y;

    return (x > width || x < 0 || y > height || y < 0);
}

void UpdateSnakePositionByCurrentMoveDirection() {
    switch (currentSnakeMovingDirection) {
    case LEFT:
        snakeHeadPosition = snakeHeadPosition + Left;
        break;
    case RIGHT:
        snakeHeadPosition = snakeHeadPosition + Right;
        break;
    case UP:
        snakeHeadPosition = snakeHeadPosition + Up;
        break;
    case DOWN:
        snakeHeadPosition = snakeHeadPosition + Down;
        break;
    default:
        break;
    }
}

void UpdateSnakeTailPositions() {
    Vector2D prevPosition = tailPositions[0];
    Vector2D temp;
    tailPositions[0] = snakeHeadPosition;

    for (int i = 1; i < tailLength; ++i) {
        temp = tailPositions[i];
        tailPositions[i] = prevPosition;
        prevPosition = temp;
    }
}

void UpdateGameLogic() {

    UpdateSnakeTailPositions();
    UpdateSnakePositionByCurrentMoveDirection();

    if (SnakeHeadTouchedBorder() || SnakeHeadTouchedTail()) {
        gameOver = true;
    }
    else if (SnakeEatingFruit()) {
        HandleFruitEating();
    }
}

#pragma region Handling_Inputs

void MoveSnakeIfValid(Direction2D newDirection) {
    if (tailLength > 0) {
        // To prevent suicidal acts by moving backwards into a tail.
        if (!IsOppositeOf(newDirection, currentSnakeMovingDirection)) {
            currentSnakeMovingDirection = newDirection;
        }
    }
    else {
        currentSnakeMovingDirection = newDirection;
    }
}

void MoveSnakeByUserInput(char inputValue) {
    Direction2D newMoveDirection;
    switch (inputValue) {
    case 'a':
        newMoveDirection = LEFT;
        break;
    case 'd':
        newMoveDirection = RIGHT;
        break;
    case 'w':
        newMoveDirection = UP;
        break;
    case 's':
        newMoveDirection = DOWN;
        break;
    default:
        newMoveDirection = UNDEFINED;
        break;
    }

    if (newMoveDirection != UNDEFINED) {
        MoveSnakeIfValid(newMoveDirection);
    }
}

bool GetKeyPressInputIfExists(char &keyPressValue) {
    if (_kbhit()) {
        keyPressValue = _getch();
        return true;
    }
    else {
        return false;
    }
}

#pragma endregion

void HandleInput() {
    char keyPressValue;
    if (GetKeyPressInputIfExists(keyPressValue)) {
        MoveSnakeByUserInput(keyPressValue);
    }
}

#pragma region Drawing_PlayField

bool PrintTailOnPositionIfNeeded(Vector2D position) {
    bool tailPrinted = false;
    for (int i = 0; i < tailLength; ++i) {
        if (position == tailPositions[i]) {
            std::cout << "o";
            tailPrinted = true;
            break;
        }
    }
    return tailPrinted;
}

void PrintFruitOrSnakeOrEmptyByPosition(Vector2D position) {
    if (snakeHeadPosition == position) {
        std::cout << "O";
    }
    else if (currentFruitPosition == position) {
        std::cout << "X";
    }
    else if (!PrintTailOnPositionIfNeeded(position)) {
        std::cout << " ";
    }
}

void DrawHorizontalBorderByWidth() {
    for (int i = 0; i < width + 2; ++i) {
        std::cout << "#";
    }
    std::cout << std::endl;
}

void DrawMiddleSection() {
    for (int i = 0; i < height; ++i) {
        for (int j = 0; j < width; ++j) {
            if (j == 0) {
                std::cout << "#";
            }

            PrintFruitOrSnakeOrEmptyByPosition({ j, i });

            if (j == width - 1) {
                std::cout << "#";
            }
        }
        std::cout << std::endl;
    }
}

#pragma endregion

void DrawPlayingField() {
    // system("cls");
    ClearScreen();

    // Top
    DrawHorizontalBorderByWidth();

    DrawMiddleSection();
    // Bottom
    DrawHorizontalBorderByWidth();
}

void UpdateGame() {
    DrawPlayingField();
    HandleInput();
    UpdateGameLogic();
}

#pragma region Request_PlayField_Width_And_Height_From_User

#pragma region Request_For_Valid_Play_Field_Size

bool PlayingFieldSizeIsValid() {
    return (width >= 10) && (height >= 10);
}

void RequestForValidPlayingFieldSize() {
    bool loopFirstRun = true;

    // Keep asking for width and height while the given playing field is not of valid size.
    do {
        if (!loopFirstRun) {
            std::cout << "The width and height of the playing field must be at least 10 or more!";
        }

        std::cout << "Enter the gamearea's width: " << std::endl;
        RequestForValidInputValue(width, "ERROR: width must be an integer!");
        std::cout << "Enter the gamearea's height: " << std::endl;
        RequestForValidInputValue(height, "ERROR: Height must be an integer!");

        loopFirstRun = false;
    } while (!PlayingFieldSizeIsValid());
}

#pragma endregion

void RequestForPlayingFieldSize() {
    bool confirmed = false;
    do {
        RequestForValidPlayingFieldSize();

        std::cout << "Your game area will be (" << width << ", " << height << "), Confirm?" << std::endl;
        confirmed = AskUserForYesNo();

    } while (!confirmed);
}

#pragma endregion

void ClearAndInitalizeVariables() {
    tailLength = 0;
    playerScore = 0;
    gameOver = false;
    currentSnakeMovingDirection = UNDEFINED;
}

void InitializeGame() {
    ClearScreen();
    ClearAndInitalizeVariables();

    RequestForPlayingFieldSize();
    // Start in the center
    snakeHeadPosition = { (width / 2), (height / 2) };
    tailPositions[0] = snakeHeadPosition;

    PlaceFruitRandomlyInPlayingField();
}

int main() {
    // Play the game while the user wants to play again.
    do {
        InitializeGame();

        while (!gameOver) {
            UpdateGame();
            std::this_thread::sleep_for(std::chrono::milliseconds(400));
        }

        std::cout << "GAMEOVER, Your Score: " << playerScore << std::endl;

        std::cout << "Play again? ";

    } while (AskUserForYesNo());

    return 0;
}

Also, would anyone give me any directions on how to properly overload and make a += operator for Vector2D?
Currently I'm doing addition as Vector1 = Vector1 + Vector2, as I tried to do a += operator but the implementation I did had issues.

\$\endgroup\$
1
\$\begingroup\$

Small Bug Fix

Noticed a small bug in my game, where the fruit might have a chance to spawn inside the snake.
Did a quick fix for it:

Renamed PlaceFruitRandomlyInPlayingField into PlaceFruitInPlayingField, added and changed codes:

bool ValidPositionForFruit(Vector2D position) {
    bool isValidPosForFruit = true;

    if (position == snakeHeadPosition) {
        isValidPosForFruit = false;
    }
    else if (PositionIsTouchingTail(position)) {
        isValidPosForFruit = false;
    }

    return isValidPosForFruit;
}

void PlaceFruitInPlayingField() {
    // Change the fruit position while the fruit's current position is not valid.
    do {
        currentFruitPosition = { rand() % width, rand() % height };
    } while (!ValidPositionForFruit(currentFruitPosition));
}

Finally, I renamed and refactored SnakeHeadTouchedTail into PositionIsTouchingTail(Vector2D) since my I do need to check if my new fruit's position would touch the head, and I didn't want to repeat myself:

bool PositionIsTouchingTail(Vector2D position) {
    bool touchedTail = false;

    for (int i = 0; i < tailLength; ++i) {
        if (tailPositions[i] == position) {
            touchedTail = true;
            break;
        }
    }
    return touchedTail;
}

And I placed inside the #pragma region Util since C++ is picky about function placements.
(The region should be near the top)

Also, in UpdateGameLogic() function, as you might have guessed, I'm doing PositionIsTouchingTail(snakeHeadPosition) instead of SnakeHeadTouchedTail() to check if the snake's head had touched the tail.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.