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\$

0

Your Answer

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

Browse other questions tagged or ask your own question.