7
\$\begingroup\$

I just completed my own implementation of the classic snake using what I consider to be unique methods. Rather than having a map be cleared over and over again using system("cls"), which creates an ugly flickering effect that hurts my eyes, I chose to fall back on the gotoxy() function that allowed be to print an object wherever I want in the output. The program runs flawlessly, but I am open to any improvements that can be done to make my game more efficient/program shorter. Do you guys have any suggestions, or opinions?

#include <iostream>
#include <conio.h>
#include <vector>
#include <windows.h>
#include <cstdlib>
#include <ctime>
using namespace std;

struct SnakeHead{
    int length;
    int x;
    int y;
    char head;
    char movingDirection;
    int score;
};

struct food {
    int rowPosition;
    int columnPosition;
    char symbol;
};
//controls the flow of the game, and when it ends
bool gameOver = true; 
//these variables will control how wide and how long the board is. All you have to do change these variables, and the board size 
//in the output will adjust accordingly
const int NUM_COLUMNS = 40, NUM_ROWS = 20;
int prevSnakeSegX, prevSnakeSegY;//these variables will hold the previous x and y position of each Snake segment
void printBoard(SnakeHead &Snake, food &foodItem);
void placeFood(food &foodItem);
void playGame(SnakeHead &Snake, food &foodItem, vector<int> &tailx, vector<int> &taily);
void drawLegend(const SnakeHead &Snake, const food &FoodItem);
void movement(SnakeHead &Snake);
void readMovement(SnakeHead &Snake, food &foodItem, vector<int> &tailx, vector<int> &taily);
void XYCoordinatesBox();
void gotoxy(int x, int y);
void setData(SnakeHead &Snake, food &foodItem, vector<int> &tailx, vector<int> &taily);
bool selfCollision(SnakeHead &snake, vector<int> &tailx, vector<int> &taily);
void createSnakeBody(SnakeHead &Snake, vector<int> &tailx, vector<int> &taily);

int main()
{
    food foodItem;
    SnakeHead Snake;
    vector<int> tailx(20), taily(20);//these arrays will hold the x and y position of each snake segement, and will grow
    //accordingly each time a food object is eaten
    playGame(Snake, foodItem, tailx, taily);

    system("pause");
    return 0;
}

void playGame(SnakeHead &Snake, food &foodItem, vector<int> &tailx, vector<int> &taily) {
    setData(Snake, foodItem, tailx, taily);
    printBoard(Snake, foodItem);
    placeFood(foodItem);
    while (gameOver) {
        movement(Snake);//read the keyboard input the user entered at the console
        readMovement(Snake, foodItem, tailx, taily);//analyze which key was pressed, and act accordingly
        Sleep(60);//wait fifty milliseconds before going to the next iteration of the loop
    }
}

void setData(SnakeHead &Snake, food &foodItem, vector<int> &tailx, vector<int> &taily) {
    Snake.score = 0;
    Snake.length = 1;
    Snake.x = NUM_COLUMNS/2, Snake.y = NUM_ROWS/2;
    tailx[0] = Snake.x, taily[0] = Snake.y;
    Snake.head = 'o';
    foodItem.symbol = 'f';
}

//print the borders of the game that will cause a game over if the snake strikes it
void printBoard(SnakeHead &Snake, food &foodItem)
{
    for (int i = 0; i < NUM_ROWS; i++) {
        if (i > 0 and i < NUM_ROWS - 1) {//creates the borders on the left and right
            cout << "*";
            for (int k = 1; k < NUM_COLUMNS - 1; k++)
                cout << " ";
            cout << "*\n";
            continue;
        }
        for (int j = 0; j < NUM_COLUMNS; j++)//creates top most and bottom most borders
            cout << "*";
        cout << endl;
    }
    drawLegend(Snake, foodItem);
    XYCoordinatesBox();
}

//draw the legend over on the side to briefly guide the user on how the game is played
void drawLegend(const SnakeHead &Snake, const food &FoodItem)
{
    const int BOX_WIDTH = 29, BOX_LENGTH = 19; //these variable determine how big the "legend" will be in the output

    gotoxy(NUM_COLUMNS + 15, 2); cout << "LEGEND:";

    gotoxy(NUM_COLUMNS, 1);
    for (int i = 0; i < BOX_WIDTH; i++)
        cout << "-";

    gotoxy(NUM_COLUMNS, BOX_LENGTH);
    for (int k = 0; k < BOX_WIDTH; k++)
        cout << "-";

    gotoxy(NUM_COLUMNS + 5, 4); cout << Snake.head << " -> SnakeHead\n";
    gotoxy(NUM_COLUMNS + 5, 6); cout << FoodItem.symbol << " -> Food\n";
    gotoxy(NUM_COLUMNS + 5, 8); cout << "#  -> Border\n";
    gotoxy(NUM_COLUMNS + 5, 10); cout << "w  -> Move Up\n";
    gotoxy(NUM_COLUMNS + 5, 12); cout << "s  -> Move Down\n";
    gotoxy(NUM_COLUMNS + 5, 14); cout << "a  -> Move Left\n";
    gotoxy(NUM_COLUMNS + 5, 16); cout << "d  -> Move Right";
}

//create the box that will print out the x and y coordinates of the Snake head
void XYCoordinatesBox()
{
    gotoxy(NUM_COLUMNS, 20);
    for (int i = 1; i < 10; i++) {
        cout << "|\n";
        gotoxy(NUM_COLUMNS, 20 + i);
    }       
}

//function to check to see if the user has inputted one of the four valid keys (w s a d), and will assign the appropiate value
//to the snakes current direction
void movement(SnakeHead &SnakeHead)
{
    if (_kbhit()) {//check to see if a key is pressed. IF true, execute the block of code below
        char input = _getch();//save the key the user pressed

        //if w is pressed, and the snake is not moving down, set the direction to up!
        if (input == 'w' and SnakeHead.movingDirection != 's') SnakeHead.movingDirection  = 'w';
        //if s is pressed, and the snake is not moving up, set the direction to down!
        else if (input == 's' and SnakeHead.movingDirection != 'w') SnakeHead.movingDirection = 's';
        //if a is pressed, and the snake is not moving right, set the direction to left!
        else if (input == 'a' and SnakeHead.movingDirection != 'd') SnakeHead.movingDirection = 'a';
        //finally, if d is pressed, and the snake is not moving right, set the direction to left!
        else if (input == 'd' and SnakeHead.movingDirection != 'a') SnakeHead.movingDirection = 'd';
    }
}

/*
this function is where all the magic happens. It will accomplish the following tasks: 
1) print out the snake head, and the snake body
2)fill the tailx and taily arrays with the x and y positions of each snake segment of the snake body
3)Increment the x or y position of the snake head based on what key the user pressed
4)Determine whether or not the snake head hit a wall, its own body, or a food object*/
void readMovement(SnakeHead &Snake, food &foodItem, vector<int> &tailx, vector<int> &taily)
{
    //if the snake head has not hit a food object yet, go to position of the previous instance of the snake head. Otherwise go
    //to the previous instance of the very last snake segment of the snake body
    (Snake.length == 1) ? gotoxy(tailx[0], taily[0]) : gotoxy(prevSnakeSegX, prevSnakeSegY);
    cout << " ";//print out a blank to prevent the snake from leaving copies of itself from previous iteration of this function
    for (int i = 1; i < Snake.length; i++) { //print out the snake body
        gotoxy(tailx[i], taily[i]); cout << Snake.head;
    }
    gotoxy(Snake.x, Snake.y); cout << Snake.head;//print out the snake head

    createSnakeBody(Snake, tailx, taily);

    if (Snake.movingDirection == 'w') Snake.y--;
    else if (Snake.movingDirection == 's') Snake.y++;
    else if (Snake.movingDirection == 'a') Snake.x--;
    else if (Snake.movingDirection == 'd') Snake.x++;

    if ((Snake.x == 0 or Snake.x == NUM_COLUMNS -1) or (Snake.y == 0 or Snake.y == NUM_ROWS - 1) or selfCollision(Snake, tailx, taily)){
        gotoxy(30, 22); cout << "GAME OVER BITCH!!";
        gameOver = false;
    }else if (Snake.y == foodItem.rowPosition and Snake.x == foodItem.columnPosition) {
        placeFood(foodItem);
        Snake.score += 10;
        Snake.length += 3;
    }

    gotoxy(NUM_COLUMNS + 5, 20); 
    cout << "X: " << Snake.x << " Y: " << Snake.y;
    cout << "\nscore: " << Snake.score << endl;
    cout << "Snake Length: " << Snake.length << endl;
}

void placeFood(food &foodItem){
    srand(time(0));
    foodItem.rowPosition = rand() % 18 + 1, foodItem.columnPosition = rand() % (NUM_COLUMNS-2) + 1;

    gotoxy(foodItem.columnPosition, foodItem.rowPosition); cout << foodItem.symbol;
}

//assigns the x and y positions of each snake segment to the tailx and taily array
void createSnakeBody(SnakeHead &Snake, vector<int> &tailx, vector<int> &taily) {
    int prevHeadX = tailx[0];//save the previous x position of the Snake head
    int prevHeadY = taily[0];//save the previous y position of the Snake head
    tailx[0] = Snake.x;//update the current x position of the Snake head for the next iteration of this function
    taily[0] = Snake.y;//do the same for the y position
    for (int i = 1; i < Snake.length; i++) {
        prevSnakeSegX = tailx[i];//save the "ith" position of the tailx array
        prevSnakeSegY = taily[i];//save the "ith position of the taily array
        tailx[i] = prevHeadX;//give tailx a new value from the previous position of the snake head (will be shifted down to last position)
        taily[i] = prevHeadY;//do the same thing for the taily array
        prevHeadX = prevSnakeSegX;//assign a new value from the previous x position of a segment to the x pos of the snake head
        prevHeadY = prevSnakeSegY;//do the same for the previous y position of the y head
        tailx.push_back(int());//increase the size of the tailx array
        taily.push_back(int());//do the same from the taily array
    }
}

bool selfCollision(SnakeHead &snake, vector<int> &tailx, vector<int> &taily) {
    int len = snake.length;

    for (int i = 0; i < len; i++) 
        if (snake.x == tailx[i] and snake.y == taily[i] and snake.length > 1)
            return true;
    return false;
}

void gotoxy(int x, int y){
    COORD coord;

    coord.X = x;
    coord.Y = y;

    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);
}
\$\endgroup\$
3
  • \$\begingroup\$ Note that you're using ancient, non portable stuff. conio.h won't be supported out of the box in newer windows versions. Rather look to implement that using some portable 3rd party lib, like ncurses. \$\endgroup\$ Feb 19 '18 at 22:08
  • \$\begingroup\$ Advice taken. When I made this program last month., I wasn't aware of another library that allowed you to use functions like _kbhit() or getch() \$\endgroup\$ Feb 19 '18 at 22:10
  • \$\begingroup\$ @DarienMiller You can use PDCurses, which is portable with NCurses. \$\endgroup\$ Feb 20 '18 at 14:29
3
\$\begingroup\$

Naming

Currently, very quickly looking over your code, it seems that you are inconsistent with your naming conventions. For example:

struct SnakeHead{
    //...
};

struct food {
    //...
};

Both of the above are classes, yet they use different naming conventions. I suggest you stick with PascalCase, as that creates a separation from functions and variable names, which should use camel case.

You have a similar problem with variable names:

food foodItem;
SnakeHead Snake;

Like before, be consistent with variable names. Even with the syntax highlighting here, it seems that Snake is the name of a class. Prefer to stick to camelCase here. (Also with your function XYCoordinates())

General Suggestions

  • As you are using C++, an OOP language, you might want to consider breaking up your code into multiples classes and objects, such as a class SnakeGame that takes care of gameplay and handles the whole game world. You would then be able to move many of the global functions you are using as member functions of this class. You could implement a similar class for the snake itself.
  • <conio.h> is nonstandard and not portable, so consider switching to PDCurses, which is compatible with NCurses.
  • Re: system("pause");. Usage of this is not recommended, and is not portable. I would replace this with something like std::cin.get, or std::cin.ignore. (More details)
  • bool gameOver = true; If this variable is supposed to act as a flag until the game is over, then you should use it as such. Code such as while (gameOver) doesn't make sense currently, since you wouldn't update the game while it is over.
  • I would replace your usage of endl with \n, as you are unnecessarily flushing the stream every time you print a newline otherwise.
  • One thing I notice a lot in your code are pairs of vectors for x and y coordinates. I suggest you create a struct Point that contains a x and y coordinate and create a vector of these. (You could even use the builtin COORD)
\$\endgroup\$
5
  • \$\begingroup\$ (Welcome to CR!) Good enough for me (more so if you removed that first sentence or turned it into a comment). (My spelling checker does not flag you are unnecessary flushing…) \$\endgroup\$
    – greybeard
    Feb 20 '18 at 14:56
  • \$\begingroup\$ @greybeard Thanks! Yes, I have fixed that sentence. \$\endgroup\$ Feb 20 '18 at 14:57
  • \$\begingroup\$ I really appreciate the advice, and i'll try to implement as best as I can. I am worried about PDCurses though. Will it have similar functions to the <conio.h>, or will I have to drastically alter my program? Also, can you briefly explain why \n is preferable to endl? Thanks again! \$\endgroup\$ Feb 20 '18 at 18:56
  • \$\begingroup\$ @DarienMiller PDCurses, which is basically NCurses for Windows, will have mostly the same functions as conio.h, with more features and some differences. For example, the _kbhit() function will most likely just be to get rid of the underscore. This question has many answers that will explain the difference for your second question. \$\endgroup\$ Feb 20 '18 at 19:54
  • \$\begingroup\$ @DarienMiller There is a caveat; You will have to change your output functions to use the NCurses equivalent, so no more cout. \$\endgroup\$ Feb 21 '18 at 11:40

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.