10
\$\begingroup\$

I just completed a snake game using object-oriented programming. The focus is on the class implementation.

OVERVIEW

Snake game works by moving the snake in order to eat fruits whilst escaping obstacles such as walls and itself.

Aim

  1. To conform to SOLID design pattern.

food.h

#ifndef SNAKEXENXIA_FOOD_H_
#define SNAKEXENXIA_FOOD_H_

class Food
{
    public:
        Food()
            : food_char( 'O' ), x_location( 0 ), y_location( 0 ), food_size( 3 ) {}
        Food( const char food_chr )
            : food_char( food_chr ), x_location( 0 ), y_location( 0 ), food_size( 3 ) {}
        Food( const char food_chr, const unsigned x_loc, const unsigned y_loc, const unsigned sz )
            : food_char( food_chr ), x_location( x_loc ), y_location( y_loc ), food_size( sz ) {}

        char get_food_char() const { return food_char; }
        Food& set_food_char( const char val )
        {
            food_char = val;
            return *this;
        }

        unsigned get_x_location() const { return x_location; }
        Food& set_x_location( const unsigned val )
        {
            x_location = val;
            return *this;
        }

        unsigned get_y_location() const { return y_location; }
        Food& set_y_location( const unsigned val )
        {
            y_location = val;
            return *this;
        }

        unsigned get_food_size() const { return food_size; }
        Food& set_food_size( const unsigned val )
        {
            food_size = val;
            return *this;
        }
    private:
        char food_char;
        unsigned x_location;
        unsigned y_location;
        unsigned food_size;
};

#endif // SNAKEXENXIA_FOOD_H_

snakebody.h

#ifndef SNAKEXENXIA_SNAKEBODY_H
#define SNAKEXENXIA_SNAKEBODY_H

#include <iostream>

enum class COORD {
    /* This specifies where the snake individual body is
        relative to North, South, East, West */
    N = 1,
    S = 2,
    E = 3,
    W = 4,
};

class SnakeBody
{
    public:
        SnakeBody() = default;
        SnakeBody( const char body_chr, const COORD &coord, unsigned number )
            : snake_co_ordinate( coord ),
            x_location( 0 ), y_location( 0 ), snake_body_char( body_chr ), body_number( number ){}
        SnakeBody( const unsigned x_loc, const unsigned y_loc, const char body_chr, const COORD &coord,\
                    const unsigned number )
                    : snake_co_ordinate( coord ),
            x_location( x_loc ), y_location( y_loc ), snake_body_char( body_chr ), body_number( number ){}

        unsigned get_x_location() const { return x_location; }
        SnakeBody& set_x_location( const unsigned val )
        {   x_location = val;
            return *this;
        }

        unsigned get_y_location() const { return y_location; }
        SnakeBody& set_y_location( const unsigned val )
        {   y_location = val;
            return *this;
        }

        char get_snake_body_char() const { return snake_body_char; }
        SnakeBody& set_snake_body_char( const char val )
        {   snake_body_char = val;
            return *this;
        }

        unsigned get_number() const { return body_number; }
        bool operator==( const SnakeBody &sb ) const;

        COORD snake_co_ordinate;
    private:
        unsigned x_location;
        unsigned y_location;
        char snake_body_char;
        unsigned body_number; // unique number to diffrentiate each snakebody
};

#endif // SNAKEXENXIA_SNAKEBODY_H

snakebody.cpp

#include "snakebody.h"

bool SnakeBody::operator==( const SnakeBody &sb ) const
{
    if( x_location != sb.x_location ) return false;
    if( y_location != sb.y_location ) return false;
    if( snake_body_char != sb.snake_body_char ) return false;
    if( snake_co_ordinate != sb.snake_co_ordinate ) return false;
    if( body_number != sb.body_number ) return false;

    return true;
}

snake.h

#ifndef SNAKEXENXIA_SNAKE_H
#define SNAKEXENXIA_SNAKE_H

#include "snake.h"
#include <vector>
#include "snakebody.h"

class Snake
{
    public:
        Snake( const char body_chr )
            : snake_char( body_chr ), can_move_north( true ), can_move_south( true ),
                can_move_west( true ), can_move_east( false ) {
                body.push_back( SnakeBody( 20, 20, 'H', COORD::W, 1 ) );
                body.push_back( SnakeBody( 20, 21, snake_char, COORD::W, 2 ) );
            }

        char get_snake_char() const { return snake_char; }
        Snake& set_snake_char( const char val )
        {   snake_char = val;
            return *this;
        }
        void eat() { grow(); }
        void move_north();
        void move_south();
        void move_west();
        void move_east();
        const std::vector<SnakeBody>& get_snake() const { return body; }
        unsigned get_snake_head_x() const { return body.front().get_x_location(); }
        unsigned get_snake_head_y() const { return body.front().get_y_location(); }

    private:
        std::vector<SnakeBody> body;
        char snake_char;
        bool can_move_north;
        bool can_move_south;
        bool can_move_east;
        bool can_move_west;

         /* This holds the current location of the snake
            Before moving N, S, E, W */
        unsigned move_x;
        unsigned move_y;

        void grow();
        void move_snake( const COORD &coord );
        void set_snake_valid_moves( const COORD &coord );
};

#endif // SNAKEXENXIA_SNAKE_H

snake.cpp

#include "snake.h"

void Snake::grow()
{
    unsigned tail_x = body.back().get_x_location();
    unsigned tail_y = body.back().get_y_location();
    unsigned number = body.back().get_number();

    if( body.back().snake_co_ordinate == COORD::N ) {
        SnakeBody sb( ++tail_x, tail_y, snake_char, COORD::N, ++number );
        body.push_back(sb);
    }
    else if( body.back().snake_co_ordinate == COORD::S ) {
        SnakeBody sb( --tail_x, tail_y, snake_char, COORD::S, ++number);
        body.push_back(sb);
    }
    else if( body.back().snake_co_ordinate == COORD::E ) {
        SnakeBody sb( tail_x, ++tail_y, snake_char, COORD::E, ++number );
        body.push_back(sb);
    }
    else if( body.back().snake_co_ordinate == COORD::W ) {
       SnakeBody sb( tail_x, --tail_y, snake_char, COORD::W, ++number );
       body.push_back(sb);
    }
    else
        return;
}

void Snake::move_north()
{
    if( can_move_north )
        move_snake( COORD::N );
    return;
}

void Snake::move_south()
{
    if( can_move_south )
        move_snake( COORD::S );
    return;
}

void Snake::move_east()
{
    if( can_move_east )
        move_snake( COORD::E );
    return;
}

void Snake::move_west()
{
    if( can_move_west )
        move_snake( COORD::W );
    return;
}

void Snake::move_snake( const COORD &coord )
{
    move_x = body.front().get_x_location();
    move_y = body.front().get_y_location();

    unsigned temp_x = 0;
    unsigned temp_y = 0;

    switch( coord )
    {
        case COORD::N:
            body.front().set_x_location( move_x - 1 );
            break;
         case COORD::S:
            body.front().set_x_location( move_x + 1 );
            break;
         case COORD::E:
            body.front().set_y_location( move_y + 1 );
            break;
         case COORD::W:
            body.front().set_y_location( move_y - 1 );
            break;
        default:
            break;
    }
    for( auto &item : body )
    {
        item.snake_co_ordinate = coord;
        if( item == body.front() )
            continue;

        /* get x and y location of snakebody before it moves */
        temp_x = item.get_x_location();
        temp_y = item.get_y_location();

        item.set_x_location( move_x );
        item.set_y_location( move_y );

        /* store the x and y for next snakebody move */
        move_x = temp_x;
        move_y = temp_y;

    }
    set_snake_valid_moves( coord );
}

void Snake::set_snake_valid_moves( const COORD &coord )
{
    switch( coord )
    {
        case COORD::N:
            can_move_east = true;
            can_move_south = false;
            can_move_west = true;
            can_move_north = true;
            break;
         case COORD::S:
            can_move_east = true;
            can_move_north = false;
            can_move_west = true;
            can_move_south = true;
            break;
         case COORD::E:
            can_move_west = false;
            can_move_north = true;
            can_move_south = true;
            can_move_east = true;
            break;
         case COORD::W:
            can_move_east = false;
            can_move_north = true;
            can_move_south = true;
            can_move_west = true;
            break;
        default:
            break;
    }
}

I also included main. ncurses.h is required to run main

main.cpp

#include "food.h"
#include "snake.h"

#include <iostream>
#include <random>
#include <vector>

#include <ncurses.h>

constexpr int HEIGHT = 30;
constexpr int WIDTH = 80;

std::default_random_engine engine ( static_cast<unsigned int> (time( nullptr )) );
std::uniform_int_distribution<unsigned int> random_WIDTH( 1, HEIGHT );
std::uniform_int_distribution<unsigned int> random_HEIGHT( 1, WIDTH );

void set_food( WINDOW *win, Food &food, unsigned HEIGHT, unsigned WIDTH );
void display_snake( WINDOW *win, const std::vector<SnakeBody>& snake );
void display_food( WINDOW *win, const Food &food );
bool game_over( unsigned HEIGHT, unsigned WIDTH,  const std::vector<SnakeBody>& snake );

int main()
{
    srand( static_cast<unsigned int> (time( nullptr )) );
    initscr();
    cbreak();
    noecho();
    curs_set( 0 );

    int start_y = ( LINES - HEIGHT ) / 2;
    int start_x = ( COLS - WIDTH ) / 2;
    refresh();

    WINDOW *win = newwin( HEIGHT, WIDTH, start_y, start_x );
    keypad( win, true );

    box( win, 0, 0 );
    wrefresh( win );

    /* Initialize the game */
    Food game_food('*', 4, 5, 4 );
    Snake game_snake( 'O' );
    std::vector<SnakeBody> snake_vector = game_snake.get_snake();
    set_food( win, game_food, HEIGHT, WIDTH );
    display_food( win, game_food );
    display_snake( win, snake_vector );
    unsigned head_x, head_y;
    bool game_is_over = false;

    while( !game_is_over )
    {
        int c = wgetch( win );
        switch( c )
        {
            case KEY_UP:
                game_snake.move_north();
                break;
            case KEY_DOWN:
                game_snake.move_south();
                break;
            case KEY_LEFT:
                game_snake.move_west();
                break;
            case KEY_RIGHT:
                game_snake.move_east();
                break;
            default:
                break;
        }
        snake_vector = game_snake.get_snake();
        game_is_over = game_over( HEIGHT, WIDTH, snake_vector );

        /* clear and reinitialize the screen */
        wclear( win );
        display_food( win, game_food );
        box( win, 0, 0 );
        display_snake( win, snake_vector );
        wrefresh( win );

        head_x = game_snake.get_snake_head_x();
        head_y = game_snake.get_snake_head_y();
        if( head_x == game_food.get_x_location() && head_y == game_food.get_y_location() ) {
            game_snake.eat();
            mvwaddch( win, game_food.get_y_location(), game_food.get_x_location(), ' ' );
            set_food( win, game_food, HEIGHT, WIDTH );
            wrefresh( win );
        }
    }
    endwin();
}

void set_food( WINDOW *win, Food &food, unsigned height, unsigned width ) {
    unsigned x = random_WIDTH( engine );
    unsigned y = random_HEIGHT( engine );

    while( x > height - 2 || y > width - 2 ) {
        x = random_WIDTH( engine );
        y = random_HEIGHT( engine );
    }
    food.set_x_location( x ).set_y_location( y );
}

void display_snake( WINDOW *win, const std::vector<SnakeBody>& snake )
{
    for( const auto &item : snake )
    {
        mvwaddch( win, item.get_x_location(), item.get_y_location(), item.get_snake_body_char() );
    }
}

void display_food( WINDOW *win, const Food &food )
{
    mvwaddch( win, food.get_x_location(), food.get_y_location(), food.get_food_char() );
}

bool game_over( unsigned height, unsigned width,  const std::vector<SnakeBody>& snake )
{
    unsigned snake_head_x = snake.front().get_x_location();
    unsigned snake_head_y = snake.front().get_y_location();

    if( snake_head_x > height - 2 || snake_head_x <= 0 )
        return true;
     if( snake_head_y > width - 2 || snake_head_y <= 0 )
        return true;

    for( const auto &item : snake ) {
        if( item == snake.front() )
            continue;
        if( item.get_x_location() == snake_head_x && item.get_y_location() == snake_head_y )
            return true;
    }
    return false;
}

```
\$\endgroup\$
0
9
\$\begingroup\$

Avoid setters and getters

        unsigned get_x_location() const { return x_location; }
        Food& set_x_location( const unsigned val );
        unsigned get_y_location() const { return y_location; }
        Food& set_y_location( const unsigned val );

For such a simple variable like x_location, having a setter/getter pair just means writing more lines for no good reason, why not make x_location public in the first place?
Now there is no point in the variable being private. If you keep following this pattern, you'll be forced to create a new getter/setter pair for every new variable that should be public

Your new Food class would be

struct Food
{
    char food_char;
    unsigned x_location;
    unsigned y_location;

    // ctors..
};

This applies to all of your other classes


Representing a position

You have a lot of these pairs

unsigned x_location;
unsigned y_location;

I highly recommend you use std::pair for this and just keep position. Even something like

struct Position
{
    int x, y;

    Position(const int x, const int y)
        : x(x), y(y)
    {}
};

will be much better.

Position position;

Unnecessary return;

void Snake::move_north()
{
    if( can_move_north )
        move_snake( COORD::N );
    return;
}

void Snake::move_south()
{
    if( can_move_south )
        move_snake( COORD::S );
    return;
}

void Snake::move_east()
{
    if( can_move_east )
        move_snake( COORD::E );
    return;
}

void Snake::move_west()
{
    if( can_move_west )
        move_snake( COORD::W );
    return;
}

What purpose do these return; statements serve here? Nothing, they are quite unnecessary here.


set_snake_valid_moves()

Let's have a look at this function

void Snake::set_snake_valid_moves( const COORD &coord )
{
    switch( coord )
    {
        case COORD::N:
            can_move_east = true;
            can_move_south = false;
            can_move_west = true;
            can_move_north = true;
            break;
         case COORD::S:
            can_move_east = true;
            can_move_north = false;
            can_move_west = true;
            can_move_south = true;
            break;
         case COORD::E:
            can_move_west = false;
            can_move_north = true;
            can_move_south = true;
            can_move_east = true;
            break;
         case COORD::W:
            can_move_east = false;
            can_move_north = true;
            can_move_south = true;
            can_move_west = true;
            break;
        default:
            break;
    }
}

I have a problem here. Out of the 4 directions, there will always be only one direction that the snake cannot move to. So instead of having 4 directions = can_move_west, can_move_east..., why not just have one cannot_move direction?

void Snake::set_snake_valid_moves( const COORD &coord )
{
    switch( coord )
    {
        case COORD::N:
            cannot_move == COORD::S;
            break;
         case COORD::S:
            cannot_move = COORD::N;
            break;
         case COORD::E:
            cannot_move = COORD::W;
            break;
         case COORD::W:
            cannot_move = COORD::E;
            break;

        default:
            break;
    }
}

Or,

if      (coord == COORD::S) cannot_move = COORD::N;
else if (coord == COORD::E) cannot_move = COORD::W;
else if (coord == COORD::W) cannot_move = COORD::E;
else if (coord == COORD::N) cannot_move = COORD::S;

That way, when you have to check if a certain direction is valid i.e doesn't break this rule, you can simply do

if ( direction != cannot_move) //...

as simple as that


When should you pass by &

I see a lot of

const COORD &coord

When you pass by reference, you are implicitly passing a pointer to the object. In this case, COORD has an underlying type of int.

The size of int differs a lot, mostly it is 4-bytes. You can check for yourself by doing std::cout << sizeof(int);. For me, it's 4.

As I said, passing by reference implicitly passes a pointer. The size of a pointer is 8 bytes. which is double the size of int. It's doing you no good. As a rule of thumb, you don't need to pas the primitive types i.e int, char, float as a reference. However, if you have something bigger like a std::vector, passing by value will be much more expensive.


Moving the snake

After I read your method, I understand that you are updating the whole body of the snake by assigning the position of snake[n] to snake[n+1. While this is okay, I propose another method.
What you can do here is pop back the last part of the snake, or its tail and add that to the front.

enter image description here

Procedure

  • Pop the last element
  • Create a new body part
  • Set its new position to be positon_of_head + offset where offset is the change ( the distance to be moved ). Basically, the future position of the head
  • Repeat

For this, you need to just a container like std::deque which allowest fast insertion/deletion at both ends
std::vector doesn't work here since it has a very slow insertion at front


Smaller suggestions

  • void eat() { grow; }?? Why create this bridge, just make grow() public
  • Keep your main() clean, create another file / class to handle the GUI

final thoughts

I like the design of your program, here are some things that I don't like

  • Overcomplication of many classes. Keep it simple, there only a few attributes Food can have, but at first glance, it looks huge
  • Noise in main.cpp. I don't like the GUI handling in main, I highly suggest that you create a class of its own, which would handle all of that
\$\endgroup\$
8
  • 3
    \$\begingroup\$ Great. Thanks for the really optimized snake movement algorithm. smashed an O(n) operation to O(1). \$\endgroup\$ Nov 7 '20 at 22:35
  • 3
    \$\begingroup\$ I had a thought about the grow(). I came to this conclusion, the user should not have a say about whether the snake grows or not, all the user can do is simply make the snake eat. Though the snakes grows whenever it eats :) \$\endgroup\$ Nov 7 '20 at 22:38
  • 1
    \$\begingroup\$ If I may ask, calling snakebody constructor every single time a movement is made, won't that be quite expensive. \$\endgroup\$ Nov 7 '20 at 22:40
  • 1
    \$\begingroup\$ @theProgrammer You've actually over complicated the snake body class a lot, it should just be containing a position and it's character. With that, no. \$\endgroup\$
    – Parekh
    Nov 7 '20 at 22:44
  • 1
    \$\begingroup\$ If you have more than one value to return from a function then passing basic variables by reference does make sense. If speed is a concern then passing basic variables by reference may also make sense so that the copy doesn't happen. \$\endgroup\$
    – pacmaninbw
    Nov 8 '20 at 1:34

Your Answer

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

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