Snake in C++ won't turn twice in a row

Viewed 323

I've just recently started to learn C++. I decided to program a little Snake game that runs in the console. It is relatively simple and doesn't look amazing, but it does it's thing as it's supposed to.
The only issue I am having, is that my Snake won't turn twice in a row. In other words you can't do tight U-turns with it. It will however turn immediately after pressing the button. (Unless you just turned that is).
My code is 120 lines long so here it is:

First my includes and namespace:

#include <iostream>
#include <vector>
#include <conio.h>
#include <windows.h>
#include <random>

using namespace std;

This function draws the whole field in the console:

void drawGrid(vector<vector<char>> &g, int height, int width, int score, int time)
{
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), { 0,0 });
    for (int r = 0; r < height; r++)
    {
        for (int c = 0; c < width; c++) std::cout << g[c][r] << " ";
        std::cout << '|' << endl;
    }
    std::cout << "Current score: " << score << "     ";
    std::cout << "\nCurrent speed: " << time << "      ";
}

This function checks whether the food is under the snake:

bool foodSnake(vector<int> &f, vector<vector<int>> &t, int l)
{
    for (int i = 0; i < l; i++) 
        if (f[0] == t[i][0] && f[1] == t[i][1]) return true;
    return false;
}

And this is the big poobah:

int main(void)
{
    int sleeptime = 1000;    // how long the break is between each frame
    bool foodExists = 0;
    int width = 20;    //width and height of the field
    int height = 15;
    mt19937_64 engine;    //random distributions for food generation
    uniform_int_distribution<int> heightDist(0, height - 1);
    uniform_int_distribution<int> widthDist(0, width - 1);
    int tailLengthstart = 4;
    int tailLength = tailLengthstart;
    char movementDirection = 'u';    //starts moving upwards
    char input;
    bool alive = 1;   //keeps the program running on death = 0
    vector<int> pos = { (width - 1) / 2,(height - 1) / 2 };    //starts in the middle of field
    vector<int> foodPos = pos;  // so that the food generates at the beginning
    vector<vector<int>> tail(tailLength, pos);
    vector<vector<char>> emptyGrid(width, vector<char>(height, ' '));
    vector<vector<char>> grid = emptyGrid;
    while (alive)  //runs main program until alive == 0
    {
        grid = emptyGrid; // clear grid
        grid[pos[0]][pos[1]] = 'Q';  //place head in grid
        if (!foodExists) //generates food if it was eaten
        {
            while (foodSnake(foodPos, tail, tailLength) || foodPos == pos)
            { // keeps regenerating until it isn't under the snake
                foodPos[0] = widthDist(engine);
                foodPos[1] = heightDist(engine);
            }
            foodExists = 1;
        }
        grid[foodPos[0]][foodPos[1]] = 'X'; //place food in grid
        for (int i = 0; i < tailLength; i++) grid[tail[i][0]][tail[i][1]] = 'O'; // place tail in grid
        drawGrid(grid, height, width, tailLength - tailLengthstart, sleeptime); //call above function to draw the grid
        input = '_';
        Sleep(sleeptime);
        if (_kbhit()) { //this was the best way I found to wait for input
            input = _getch();
            switch (input)
            { //disallows moving in opposite direction otherwise changes direction
                case 'w':
                    if (movementDirection == 'd') break;
                    movementDirection = 'u';
                    break;
                case 'a':
                    if (movementDirection == 'r') break;
                    movementDirection = 'l';
                    break;
                case 's':
                    if (movementDirection == 'u') break;
                    movementDirection = 'd';
                    break;
                case 'd':
                    if (movementDirection == 'l') break;
                    movementDirection = 'r';
                    break;
                case '_':
                    break;
            }
        }
        for (int i = tailLength - 1; i > 0; i--)
            tail[i] = tail[i - 1]; 
        tail[0] = pos; //move the tail along
        if (movementDirection == 'u') pos[1]--;
        if (movementDirection == 'l') pos[0]--;
        if (movementDirection == 'r') pos[0]++;
        if (movementDirection == 'd') pos[1]++; // move the head
        if (pos[0] < 0 || pos[0] > width - 1 || pos[1] < 0 || pos[1] > height - 1) 
            alive = 0; // if head is out of bounds -> dead
        for (int i = 0; i < tailLength; i++) 
            if (pos == tail[i]) 
                alive = 0; // if head is on tail -> dead
        if (foodPos == pos)
        { // if head is on food -> eat
            foodExists = 0; // food needs to be generated
            tail.push_back(tail[tailLength - 1]); //tail adds a link
            tailLength++; // tail is now longer
            if (tailLength % 5 == 0) sleeptime *= 0.75; // at certain lengths game speeds up
        }
    }

this next part happens once you are dead or alive == 0

    std::system("cls");
    std::cout << endl << endl << endl << endl << "\tYou have died" << endl << endl << endl << endl;

    std::cout << endl;
    std::system("pause");
    return 0;
}

So if anyone has an idea why it's not turning quickly, please help. Or any other improvement ideas are welcome as well.

2 Answers

The problem with movement is caused by fact that you advance tail and changing direction causes head advance immediately. That means that there is always a step before snake would actually turn.

The state set as I see it:

  1. Setup scene.
  2. Check key.
  3. Change direction of movement if key pressed and it isn't opposite of current direction.
  4. Make old head tail and add a head element in set direction from old head.
  5. Check for death
  6. Check for food.
  7. If food found, grow tail by changing TailLength.
  8. If snake length is greater than TailLength, remove tail elements until they are equal.
  9. Render scene
  10. Sleep and go to 2.

It is better to represent snake by a list, not by a vector. It won't reallocate memory on size change or if you will cut first elements out.

If your platform is Windows (obviously, you're using Sleep() function instead of usleep), the answer to this question offers better solution for key detection. Get key press in windows console

Similar solutions exist for POSIX platforms.

There is a little problem that you're waiting sleeptime even the key was pressed

Sleep(sleeptime);

The main cycle may be like this pseudo code

while (IsAlive())
{
   currtime = 0;
   DrawState();
   while (currtime++ < sleeptime)
   {
      if (CheckAndProcessKeyboardInput())
         break;
      SleepOneMilliSecond();
   }
}
Related