4
\$\begingroup\$

I implemented a console version of the classic snake game. I wanted it to be portable but some of the functions like setting console position and _kbhit weren't available on Non-Windows operating systems, thus as is, this code is Windows only but I tried to isolate the non-portable parts in controls.h. I would especially like comments about the object-oriented design.
Here's the code:

main.cpp

#include "GamePlay.h"

int main()
{
    GamePlay gamePlay;
    gamePlay.play();
    return 0;
}

Cell.h

#ifndef SNAKE_CELL_H
#define SNAKE_CELL_H

#include <iostream>


// Base class for snake cell, field cell and other cell types
class Cell 
{
public:

    // the type each cell holds and prints as its content
    using cell_content_type = char;
    explicit Cell(const cell_content_type& = ' ', bool = false, bool prize_ = false); // an empty cell by default

    friend std::ostream& operator<<(std::ostream&os, const Cell& cell);

    // if the cell helds something
    bool isOccupied() const { return occupied; }
    void setOccupid(bool o) { occupied = o; }
    bool get_prize() { return prize;  }

    //pure virtual destructor to make this class abstract
    virtual ~Cell() = 0;
protected:

    // whether or not the cell is holds something (part of
    // the snake/border)
    bool occupied;

    // if this cell is edible
    bool prize;


    // each cell prints this when draw method is called
    cell_content_type content;
};

#endif // !SNAKE_CELL_H

Cell.cpp

#include "Cell.h"

Cell::Cell(const cell_content_type& content_, bool occupied_, bool prize_)
    : content(content_)
    , occupied(occupied_)
    , prize(prize_)
{ }

Cell::~Cell()
{ }

std::ostream & operator<<(std::ostream & os, const Cell & cell)
{
    os << cell.content;
    return os;
}

Field.h

#ifndef SNAKE_FIELD_H
#define SNAKE_FIELD_H

#include <vector>
#include <iostream>
#include <memory>
#include "Cell.h"
#include "Field_border_cell.h"



class Field
{
public:
    // friends
    friend std::ostream& operator<<(std::ostream&, const Field&);

    // havind Cell-based objects in this field
    using stored_type = Cell;

    // "matrix" for container
    using field_vector_type = std::vector<std::shared_ptr<stored_type>>;
    using field_container_type = std::vector<field_vector_type>;

    using size_type = field_container_type::size_type;
    // side-length of the square field
    static const size_type field_height = 20;
    static const size_type field_width = 20;
    Field();
    Field(Field&) = delete;

    // being / end
    auto begin() { return container.begin(); }
    auto end() { return container.end(); }

    auto begin() const { return container.begin(); }
    auto end() const { return container.end(); }

    auto cbegin() const { return begin(); }
    auto cend() const { return end(); }

    // swipe the cells within the container
    // intended to be used to move the snake
    void swipe_cells(size_type col1, size_type row1,
        size_type col2, size_type row2);

    void add_to_field(std::shared_ptr<Cell>, size_type, size_type);

    bool cellIsOccupied(size_type, size_type) const;

    field_vector_type& operator[](size_type index)
    { return container[index]; }

    void reset_cell(size_type, size_type);
private:

    field_container_type container;

    bool isBorder(size_type col, size_type row)
    {
        return (col == 0 || row == 0 
            || col == field_height - 1 || row == field_width - 1);
    }

    std::shared_ptr<Field_cell> default_field_cell;
    std::shared_ptr<Field_border_cell> default_field_border_cell;
};

#endif // !SNAKE_FIELD_H

Field.cpp

#include <utility>
#include <iostream>

#include "Field.h"
#include "Field_cell.h"
#include "Field_border_cell.h"


using std::size_t;
using std::make_shared;
using std::cout;
using std::endl;

Field::Field()
    : default_field_cell(make_shared<Field_cell>())
    , default_field_border_cell(make_shared<Field_border_cell>())
{
    // fill in the container with empty `Field_cell`s
    for (size_t icol = 0; icol != field_height; ++icol) {
        container.push_back(field_vector_type());
        for (size_t irow = 0; irow != field_width; ++irow) {
            // if on borders, add a border cell
            // else add a normal field_cell
            container.back().push_back(
                (isBorder(icol, irow) ? default_field_border_cell : default_field_cell)
            );
        }
    }
}



// swipe the cells within the container
// intended to be used to move the snake

void Field::swipe_cells(size_type col1, size_type row1, size_type col2, size_type row2)
{
    std::swap(container[col1][row1], container[col2][row2]);
}

void Field::add_to_field(std::shared_ptr<Cell> cell, size_type col , size_type row)
{
    container[col][row] = cell;
}

bool Field::cellIsOccupied(size_type col, size_type row) const
{
    return container[col][row]->isOccupied();
}

void Field::reset_cell(size_type col, size_type row)
{
    add_to_field(default_field_cell, col, row);
}


std::ostream & operator<<(std::ostream &os, const Field &field)
{
    // for each row
    for (const auto& row : field) {

        // for each element in that row
        for (const auto& elem : row) {
            cout << *elem;
        }

        // break the line after each row
        cout << '\n';
    }

    // finish off drawing the board with 
    // flushing the buffer
    cout << endl;

    return os;
}

Field_border_cell.h

#ifndef SNAKE_FIELD_BORDER_CELL_H
#define SNAKE_FIELD_BORDER_CELL_H

#include "Field_cell.h"

// Same as a Field_cell 
// except that this is occupied
// and prints an 'x' instead of ' '
class Field_border_cell :
    public Field_cell
{
public:
    Field_border_cell();
    ~Field_border_cell() = default;
};

#endif // !SNAKE_FIELD_BORDER_CELL_H

Field_border_cell.cpp

#include "Field_border_cell.h"



Field_border_cell::Field_border_cell()
    : Field_cell('x', true)
{ }

Field_cell.h

#ifndef SNAKE_FIELD_CELL_H
#define SNAKE_FIELD_CELL_H


#include "Cell.h"
class Field_cell :
    public Cell
{
public:
    // an explicitly empty cell
    explicit Field_cell(const cell_content_type& = ' ', bool = false);
    ~Field_cell() override = default;
protected:
    // nothing yet
};

#endif // !SNAKE_FIELD_CELL_H

Field_cell.cpp

#include "Field_cell.h"

Field_cell::Field_cell(const cell_content_type& content_, bool occupied_)
    :Cell(content_, occupied_)
{ }

GamePlay.h

#ifndef SNAKE_GAME_PLAY_H
#define SNAKE_GAME_PLAY_H

#include <iostream>
#include "Field.h"
#include "Snake.h"
#include "Prize_cell.h"
#include "controls.h"

class GamePlay
{
public:
    GamePlay();
    void play();

private:
    using size_type = Field::size_type;

    // How many prize cells have been eaten:
    size_type score;

    // how fast the snake moves: smaller number = faster
    size_type game_wait_time;

    Field field;
    Snake snake;

    // converts a char to direction
    Snake::Direction get_direction(int ch);

    // update the display with new version of the field
    void update_display();


    // create a prize randomly in the field
    void add_prize_to_field(size_type = 1);

    // the opening screen
    void initial_display();
};


#endif // !SNAKE_GAME_PLAY_H

GamePlay.cpp

#include "GamePlay.h"
#include <random>
#include <ctime>
#include <memory>
#include "Prize_cell.h"
#include "Field_cell.h"

using std::cout;
using std::endl;
using std::cin;
using std::default_random_engine;
using std::uniform_int_distribution;
using std::shared_ptr;
using std::make_shared;

GamePlay::GamePlay()
    : field()
    , snake(field)
    , score(0)
    , game_wait_time(0)
{ }

void GamePlay::play()
{
    // opening screen
    initial_display();

    bool move_succeded = true;

    // When prize_counter reaches prize_cnt, a new prize appears
    size_type prize_counter = 0;

    // main game loop
    // the loop goes on until snake.slither returns false
    while (move_succeded) {

        // if a key is pressed, the snake should move in the 
        // corresponding direction
        if (a_key_pressed()) {
            move_succeded = snake.slither(get_direction(get_pressed_key()));
        }
        // else the snake should continue moving in the previous direction
        else {
            move_succeded = snake.slither(Snake::Direction::CONTINUE);
        }

        // draw the new version of the board
        update_display();

        if (snake.ate_this_move) {
            add_prize_to_field();
            ++score;
            snake.ate_this_move = false;
        }

        // wait a little before the next move
        sleep(game_wait_time);
    }

    cout << "Game Over" << endl;
    keep_window_open();
}


Snake::Direction GamePlay::get_direction(int ch)
{
    switch (ch) {
    case 'w':
        return Snake::Direction::UP;
    case 's':
        return Snake::Direction::DOWN;
    case 'a':
        return Snake::Direction::LEFT;
    case 'd':
        return Snake::Direction::RIGHT;
    default :
        return Snake::Direction::CONTINUE;
    }
}

inline void GamePlay::update_display()
{
    gotoxy(0, 0);
    std::cout << field;
    std::cout << "Score: " << score << '\n';
}

void GamePlay::add_prize_to_field(size_type prize_amount)
{
    static default_random_engine e(std::time(0));
    static uniform_int_distribution<size_type> dis_height(0, Field::field_height - 1);
    static uniform_int_distribution<size_type> dis_width(0, Field::field_width - 1);
    static std::shared_ptr<Prize_cell> default_prize_cell = make_shared<Prize_cell>();
    static size_type col = 0, row = 0;

    // if we don't find an empty spot in this many tries
    // we just give up
    size_type prize_rand_try_count = Field::field_height * Field::field_width;
    do {
        col = dis_height(e);
        row = dis_width(e);

        if (--prize_rand_try_count == 0) {
            return;
        }
    } while (field.cellIsOccupied(col, row));

    field.add_to_field(default_prize_cell, col, row);
}

void GamePlay::initial_display()
{
    cout << "Welcome to Snake\n";

    cout << "Controls:\n"
        << "       W\n"
        << "    A  S  D\n\n\n";

    cout << "Pick enter a speed number between 1 and 10 (larger==faster)" << endl;

    size_type speed_numb = 11;
    cin >> speed_numb;
    while (speed_numb > 10) {
        cout << "This was out of range, plase try again." << endl;
        cin.clear();
        cin >> speed_numb;
    }

    // add 1 to avoid dividing by 0
    ++speed_numb;
    // the wait time is the difference in msec
    game_wait_time = 1050 - speed_numb * 100;

    clear_scrn();
}

Prize_cell.h

#ifndef SNAKE_PRIZE_CELL_H
#define SNAKE_PRIZE_CELL_H

#include "Cell.h"
class Prize_cell :
    public Cell
{
public:
    Prize_cell();
    ~Prize_cell() override = default;
};

#endif // !SNAKE_PRIZE_CELL_H

Prize_cell.cpp

#include "Prize_cell.h"

using std::size_t;

Prize_cell::Prize_cell()
    : Cell('*', false, true)
    // the cell is not occupied although it is not empty
    // because hitting an occupied cell would kill the snake
{ }

Snake_cell.h

#ifndef SNAKE_SNAKE_CELL_H
#define SNAKE_SNAKE_CELL_H


#include "Cell.h"
class Snake_cell :
    public Cell
{
public:
    Snake_cell();
    ~Snake_cell() override = default;
};

#endif // !SNAKE_SNAKE_CELL_H

Snake_cell.cpp

#include "Snake_cell.h"



Snake_cell::Snake_cell()
    : Cell('o', true)
{ }

Snake.h

#ifndef SNAKE_SNAKE_H
#define SNAKE_SNAKE_H


#include <list>
#include <tuple>
#include "Field.h"
#include "Snake_cell.h"

class Snake
{
public:
    using size_type = Field::size_type;
    Snake(Field&);

    // directions in which snake may move.
    // CONTINUE means keep on going in the 
    // previous direction. Opposite sides are
    // are multiplied by -1
    enum class Direction {
        UP = 1, DOWN = -1,
        LEFT = 2, RIGHT = -2,
        CONTINUE = 3
    };

    Snake(Snake&) = delete;

    // bool slither(Direction) : move snake in the field.
    // Returns false if snake hits something
    // and game should be over. true otherwise
    bool slither(Direction);

    // size of the snake in cells (how many cells
    // it has)
    size_type size() { return container.size(); }

    // a signal to game to create a new prize
    bool ate_this_move = true;

private:
    using cell_ptr = std::shared_ptr<Snake_cell>;
    const size_type starting_snake_size;


    // details about a cell: a (shared) pointer to the cell,
    // column number and row number
    using cell_detail_type = std::tuple<cell_ptr, size_type, size_type>;

    using snake_container_type = std::list<cell_detail_type>;


    Field& field;
    snake_container_type container;

    // called when eaten something causes the snake
    // to grow by 1
    void grow();

    // pushes tail to back and pops from front
    void move_tail_back();

    // returns true if the position within 
    // the field and it is not occupied. false other wise
    bool pos_is_valid(size_type col, size_type row);

    // used by the slither function to get
    // the new col and row pos
    bool newPos(size_type&, size_type&, Direction);

    // update the cell with new position details in cell_detail_type object
    void update_cell_pos_info(cell_detail_type& cdt, size_type col, size_type row);

    std::shared_ptr<Snake_cell> default_snake_cell;

    // if and when a prize is eaten, this becomes true
    // until the snake has moved and is grown accordingly
    bool waiting_eaten = false;
};

#endif // !SNAKE_SNAKE_H

Snake.cpp

#include "Snake.h"
#include <memory>

using std::make_shared;
using std::make_tuple;
using std::get;

Snake::Snake(Field& field_)
    : field(field_)
    , default_snake_cell(make_shared<Snake_cell>())
    , starting_snake_size(3)
{
    size_type height_half = Field::field_height / 2;
    size_type width_half = Field::field_width / 2;

    for (size_type icell = 0; icell != starting_snake_size; ++icell) {
        container.push_back(
            make_tuple(
                default_snake_cell,
                height_half - icell,
                width_half
            )
        );
        // put those `Snake_cell`s into the field
        field.add_to_field(get<0>(container.back()),
            get<1>(container.back()),
            get<2>(container.back())
        );
    }
}

bool Snake::slither(Direction direction)
{
    // the new position we the snake is moving to
    auto newCol = get<1>(container.back());
    auto newRow = get<2>(container.back());


    // update newCol and newRow
    if (!newPos(newCol, newRow, direction)) {
        return true;
    }


    // if the cell we just moved is occupied, the game is over
    // thus, we return false
    if (!pos_is_valid(newCol, newRow)) {
        return false;
    }

    if (field[newCol][newRow]->get_prize())
        grow();


    // if a prize is eaten, move accordingly
    if (waiting_eaten) {
        field.add_to_field(default_snake_cell, newCol, newRow);
        waiting_eaten = false;
    }
    else {
        // swipe the tail and the (empty) cell it is moving to
        field.swipe_cells(newCol, newRow, get<1>(container.front()), get<2>(container.front()));
    }

    // update the position of the cell in snake's own container
    update_cell_pos_info(container.front(), newCol, newRow);

    // move the tail to the back making it the new head
    move_tail_back();

    return true;
}


void Snake::grow()
{
    container.push_front(
        make_tuple(
            default_snake_cell,
            get<1>(container.back()),
            get<2>(container.back())
        )
    );
    waiting_eaten = true;
    ate_this_move = true;
}

// pushes tail to back and pops from front
inline void Snake::move_tail_back()
{
    container.push_back(container.front());
    container.pop_front();
}

// returns true if the position within 
// the field and it is not occupied. false other wise
inline bool Snake::pos_is_valid(size_type col, size_type row)
{
    return col < Field::field_height
        && row < Field::field_width
        && !field.cellIsOccupied(col, row);
}

bool Snake::newPos(size_type &newCol, size_type &newRow, Direction direction)
{
    static auto previous = Direction::UP;

    // if the directions are opposite, snake cannot do this move
    if (direction == Direction::CONTINUE || direction == Direction(int(previous) * -1))
        direction = previous;

    // this switch case does the actual movement calculation.
    // There are some moves (like UP when previous was DOWN)
    // that snake cannot do and those have no effect

    switch (direction) {
    case Direction::UP:
        --newCol;
        break;
    case Direction::DOWN:
        ++newCol;
        break;
    case Direction::RIGHT:
        ++newRow;
        break;
    case Direction::LEFT:
        --newRow;
        break;
    }


    previous = direction;
    return true;
}

// update the cell with new position details in cell_detail_type object
inline void Snake::update_cell_pos_info(cell_detail_type & cdt, size_type col, size_type row)
{
    std::get<1>(cdt) = col;
    std::get<2>(cdt) = row;
}

controls.h

#ifndef SNAKE_CONTROLS_H
#define SNAKE_CONTROLS_H

#include <thread>         // std::this_thread::sleep_for
#include <chrono>         // std::chrono::seconds
#include <iostream>

#if defined _WIN32

#include <conio.h>
#define a_key_pressed() _kbhit()
#define get_pressed_key() _getch()

#define clear_scrn() system("cls")

#include <Windows.h>
inline void gotoxy(short x, short y)
{
    COORD pos = { x, y };
    HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE);
    SetConsoleCursorPosition(output, pos);
}

#elif defined (__LINUX__) || defined(__gnu_linux__) || defined(__linux__) || defined(__APPLE__)



// Unix-like operating systems are not supported yet
static_assert(false);
#define clear_scrn() system("clear")

int get_pressed_key();
bool a_key_pressed();
inline void gotoxy(short x, short y);

void clear();


#endif // OS check


// Universal tools
inline void sleep(long long msec)
{
    std::this_thread::sleep_for(std::chrono::milliseconds(msec));
}

inline void keep_window_open()
{
    std::cout << "Press Enter to close the window" << std::endl;
    std::cin.get(); 
    std::cin.get();
}

#endif //SNAKE_CONTROLS_H

Here is a screenshot:
A screenshot from the game

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.