4
\$\begingroup\$

Based on this Framework Terminal based game: Part 2

A game that uses std::cout to print the board and std::cin to get keyboard input dynamically (using the framework linked). Allowing absolute beginners to quickly correct a simple text based animated game.

Here is my version of the snake game:

#include "ThorsAnvil/Game/Game.h"

struct Location
{
    int x;
    int y;

    bool operator==(Location const& rhs) const {
        return x == rhs.x && y == rhs.y;
    }
    friend std::ostream& operator<<(std::ostream& str, Location const& data) {
        return str << data.x << " , " << data.y;
    }
};

enum Direction {Stop, Up, Down, Left, Right};

class Snake
{
    using SLocation = std::vector<Location>;

    Direction   direction;
    SLocation   location;
    public:
        Snake(Location const& start)
            : direction{Stop}
            , location{1, start}
        {
            location.reserve(100);
        }
        char check(Location pos)
        {
            auto find = std::find(std::begin(location) , std::end(location), pos);
            if (find == std::begin(location)) {
                switch (direction)
                {
                    case Stop:  return '$';
                    case Up:    return 'A';
                    case Down:  return 'V';
                    case Left:  return '<';
                    case Right: return '>';
                }
            }
            if (find != std::end(location)) {
                return '@';
            }
            return ' ';
        }
        void changeDirection(Direction d)
        {
            direction = d;
        }

        bool move()
        {
            if (direction != Stop) {
                int s = std::size(location);
                for (int loop = s; loop > 1; --loop) {
                    location[loop - 1] = location[loop - 2];
                }
            }
            switch (direction)
            {
                case Up:    --location[0].y;      break;
                case Down:  ++location[0].y;      break;
                case Left:  --location[0].x;      break;
                case Right: ++location[0].x;      break;
                default:    break;
            }
            auto find = std::find(std::begin(location) + 1, std::end(location), location[0]);
            return find == std::end(location);
        }
        void grow()
        {
            location.emplace_back(location.back());
        }
        Location const& head() const
        {
            return location[0];
        }
        int size() const
        {
            return location.size();
        }
};

class SnakeGame: public ThorsAnvil::GameEngine::Game
{
    static constexpr int width  = 20;
    static constexpr int height = 20;
    static constexpr double speedInceaseFactor = 0.92;

    int         score;
    char        key;
    Snake       snake;
    Location    cherry;
    char getChar(Location const& pos)
    {
        if (pos.y == 0 || pos.y == height - 1 || pos.x == 0 || pos.x == width -1) {
            return '#';
        }
        if (pos == cherry) {
            return '%';
        }
        char s = snake.check(pos);
        if (s != ' ') {
            return s;
        }
        return ' ';
    }

    virtual int  gameStepTimeMilliSeconds() override
    {
        return 500 * std::pow(speedInceaseFactor, snake.size());
    }
    virtual void drawFrame() override
    {
        std::cout << "Snake V1.0\n";
        for (int y = 0; y < height; ++y) {
            for (int x = 0; x < width; ++x) {
                std::cout << getChar({x, y});
            }
            std::cout << "\n";
        }
        std::cout << "Score: " << score << "  LastKey: " << key << "\n";
        std::cout << "Step:  " << gameStepTimeMilliSeconds() << " \n";
        std::cout << std::flush;
    }
    virtual void handleInput(char k) override
    {
        key = k;
        Game::handleInput(key);
        switch (key)
        {
            case 'q':   snake.changeDirection(Up);      break;
            case 'a':   snake.changeDirection(Down);    break;
            case 'o':   snake.changeDirection(Left);    break;
            case 'p':   snake.changeDirection(Right);   break;
            default:    break;
        }
    }
    bool snakeHitWall()
    {
        Location const& head = snake.head();
        return head.y == 0 || head.y == height -1 || head.x == 0 || head.x == width -1;
    }
    void handleLogic() override
    {
        bool moveOk = snake.move();

        if (!moveOk || snakeHitWall()) {
            std::cout << "Move: " << moveOk << "\n"
                      << "Head: " << snake.head() << " \n"
                      << "Wall: " << snakeHitWall() << "\n";
            setGameOver();
        }
        if (snake.head() == cherry) {
            snake.grow();
            score++;
            cherry  = {(rand() % (width - 2)) +1 , (rand() % (height - 2)) +1};
        }
    }

    public:
        SnakeGame()
            : score(0)
            , key(' ')
            , snake{{width/2, height/2}}
            , cherry{(rand() % (width - 2)) +1 , (rand() % (height - 2)) +1}
        {}
};

int main()
{
    SnakeGame    snake;
    snake.run();
}

Brute force draw of the screen takes 900 micro seconds. Note: that's a grid of 20 * 20 so a small screen. Will look at what an optimized draw looks like this weekend (where I only update head and tail of the snake every drawFrame()`

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Missing #includes

The code you posted doesn't compile without adding additional #include statements, for example cmath for the math functions, <algorithm> for std::find(), and so on.

Make Direction an enum class

Make it a habit to always use enum class instead of enum, unless you really need the latter. An enum class adds extra type-safety.

Naming things

What's a SLocation? Secure location? Static location? Snake location? If you avoid abbreviating names, others won't have to guess. Even the latter is not really appropriate, as it's not a single location. Maybe BodyLocations is better? That it's the body of a snake is clear from the context.

Avoid specifying time resolution unless really necessary

As I also mentioned in the review of part 2 of your game engine, you are dealing with explicit milliseconds way too early. Try to keep durations in unspecified std::chrono::duration variables for as long as possible. Of course, here you need to pass in the initial step time. So do it like this:

class SnakeGame: public ThorsAnvil::GameEngine::Game
{
    using Clock = std::chrono::steady_clock;
    using Duration = Clock::duration;

    Duration stepTime = std::chrono::milliseconds(500);
    …
    virtual Duration gameStepTime() override
    {
        return stepTime;
    }
    …
    void handleLogic() override
    {
        …
        if (snake.head() == cherry) {
            stepTime *= speedInceaseFactor;
            …
        }
    }
    …
};

Drawing is very inefficient

I understand why you wrote the code like you did, but I want to point out that it has some inefficiencies that get larger as the snake gets longer. For every part of the frame that is not a wall, you have to iterate over the whole vector of body locations to find out what character to draw. It's the inverse problem of Space Invaders!

Consider storing the whole frame in a single std::string, and then just updating those characters that need to be updates (the head, the tail and the cherry), and then just print that string to std::cout in one go.

You could also make it an array of strings, one element per line, for a simpler way to address it using x and y coordinates.

\$\endgroup\$
3
  • \$\begingroup\$ I have some obvious misses with the include files. But was thinking I could automate catching these. What system are you using? I want to see if I can set up guthub builds to try and catch my lazyness. PS. I am building on mac with no build errors. \$\endgroup\$ Mar 22 at 3:48
  • \$\begingroup\$ I think clang-tidy can find missing (and also unused) headers. But I can't wait for all compilers to get their act together so we can use C++23's import std; \$\endgroup\$
    – G. Sliepen
    Mar 22 at 13:38
  • \$\begingroup\$ Found clang-tidy gist.github.com/sleepdefic1t/e9bdb1a66b05aa043ab9a2ab6c929509 \$\endgroup\$ Mar 22 at 20:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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