4

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.

Aglahir
  • 41
  • 3
  • `_kbhit()` (user input) needs to run parallel (use thread) for quick response, presently it cascaded with other codes in the main loop – seccpur Oct 23 '18 at 10:24
  • 4
    It is important that you understand the following: console is not meant to be used as interactive graphics device, with real-time input and output. There will be several obstacles on your way to make an action game in console. The knowledge that you get from writing a console action game is not applicable in real world. I suggest to you that you create an actual GUI game (Windows), and draw everything in there. – Dialecticus Oct 23 '18 at 10:34
  • 1
    @seccpur NOOOOOO. I know threads are fancy, but this is definitely not a place to use them. Newbie should never use them, since they are hard and lead to bugs which are hard to understand and fix. – Marek R Oct 23 '18 at 10:35
  • 4
    @Dialecticusthat's not the problem here (also, there were hundreds of games that do exactly this? some platforms do not have GUI per se at all. Even linux with frame-buffer render instead of X11). The actual problem is that while game is running in single thread it doesn't check key state before moving snake forward. So one can't make a tight u-turn simply because of that. – Swift - Friday Pie Oct 23 '18 at 10:37
  • @MarekR: Agreed. But the `Sleep()` just before `kbhit()` may curtail user responses and the snake may overshoot – seccpur Oct 23 '18 at 10:43
  • I compiled the code in Visual Studio 2012. Had to change just how the some things are initialized. And the game works just fine. I can make the "stairs" with the snake. I can turn and then turn again (U-turn). It works. – Dialecticus Oct 23 '18 at 10:52

2 Answers2

0

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.

Swift - Friday Pie
  • 12,777
  • 2
  • 19
  • 42
0

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();
   }
}
serge
  • 992
  • 5
  • 8
  • I do want it to sleep for the full amount of sleeptime even if a key is pressed. otherwise you could mash buttons and make the game run faster. The issue isn't that I want the snake to react immediately to a button press. – Aglahir Oct 23 '18 at 15:37