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()`