4
\$\begingroup\$

Description

I've written the snake game using C++ and FLTK. For simplifying the use of FLTK, a built-up library written by Bjarne Stroustrup was used. Bellow located main parts of the code written by me, a whole project can be found on GitHub: https://github.com/WumpusHunter/Snake-game.

Source.cpp

/*
    Snake game

    Revision history:
        Written by Oleg Kovalevskiy in August 2020
*/

//------------------------------------------------------------------------------------

#include "Game_window.h"
using namespace Graph_lib;

//------------------------------------------------------------------------------------

int main()
try {
    // Window with top-left angle at (100, 100), of size 600 * 400, labeled "Snake game"
    Snake_window win{ Point{ 100, 100 }, 600, 400, "Snake game" };
    
    return gui_main();
}
catch (const exception& e) {
    cerr << "Error message: " << e.what() << '\n';
    return 1;
}
catch (...) {
    cerr << "Unknown error\n";
    return 1;
}

//------------------------------------------------------------------------------------

Game_window.h

// Snake game's window

//------------------------------------------------------------------------------

#pragma once
#include "GraphicsLib/Window.h"
#include "GraphicsLib/GUI.h"
#include "GraphicsLib/Graph.h"
#include "Game_graph.h"

//------------------------------------------------------------------------------

namespace Graph_lib {

    //------------------------------------------------------------------------------

    // Invariant: w > 0, h > 0
    class Snake_window : public Window {        // Game window
    public:
        // Construction
        Snake_window(Point xy, int w, int h, const string& lab);

    private:
        // Callback functions
        int handle(int event) override;
        static void cb_game_loop(Address pw);
        static void cb_pause(Address, Address pw);
        static void cb_new_game(Address, Address pw);
        static void cb_quit(Address, Address pw);
        static void cb_game(Address, Address pw);
        static void cb_help(Address, Address pw);

        // Action functions
        void start();
        void game_loop();
        bool is_pause();
        void pause();
        void new_game();
        void quit();
        void game();
        void help();
        int current_score();
        void put_score();
        void show_graphics();
        void hide_graphics();

    private:
        // Graphics
        Grid field;
        Snake snake;
        Rectangle fruit;
        // GUI
        Menu game_menu;
        Button game_button;
        Button help_button;
        Text_box help_box;
        Out_box score_box;
        Out_box max_score_box;
    };

    //------------------------------------------------------------------------------

}   // End of Graph_lib namespace

//------------------------------------------------------------------------------

Game_window.cpp

// Snake game's window

//------------------------------------------------------------------------------

#include "Game_window.h"

//------------------------------------------------------------------------------

namespace Graph_lib {

    //------------------------------------------------------------------------------

    // Min possible size of window
    constexpr int min_w = 400;      // Window's min width
    constexpr int min_h = 300;      // Window's min height
    // Size of cells
    constexpr int cell_w = 50;      // Cell's width
    constexpr int cell_h = 50;      // Cell's height
    // Default parameters of snake
    constexpr int snake_sz = 3;     // Snake's length
    // Default location of graphics
    Point snake_xy = { 0, 0 };      // Snake's location
    Point fruit_xy = { 0, 0 };      // Fruit's location
    // Size of widgets
    constexpr int widget_h = 25;    // Widgets' height
    constexpr int out_box_w = 30;   // Output boxes' width
    constexpr int button_w = 100;   // Buttons' width
    // Indexes of game menu's buttons
    constexpr int new_game_ind = 0; // New game button's index
    constexpr int pause_ind = 1;    // Pause button's index
    constexpr int quit_ind = 2;     // Quit button's index

    // Constructs window with top-left angle at xy, of size w * h (if
    // it's not less than min, which is 400 * 300), labeled with lab
    Snake_window::Snake_window(Point xy, int w, int h, const string& lab)
        : Window{ xy, w >= min_w ? w - w % cell_w : min_w, h >= min_h ? h - h % cell_h : min_h, lab },
        field{ Point{ 0, cell_h }, cell_w, cell_h, x_max() / cell_w, (y_max() - cell_h) / cell_h },
        snake{ Point{ snake_sz * cell_w, y_max() / 2 }, cell_w, cell_h, snake_sz },
        fruit{ Point{ x_max() - cell_w * 2, y_max() / 2 }, cell_w, cell_h },
        game_menu{ Point{ 0, 0 }, button_w, widget_h, Menu::Kind::horizontal, "Game" },
        game_button{ Point{ 0, 0 }, button_w, widget_h, "&Game", cb_game },
        help_button{ Point{ button_w, 0 }, button_w, widget_h, "&Help", cb_help },
        help_box{ Point{ 0, cell_h }, x_max(), y_max() - cell_h, "" },
        score_box{ Point{ cell_w * 2, widget_h }, out_box_w, widget_h, "Current score: " },
        max_score_box{ Point{ cell_w * 4 + out_box_w, widget_h }, out_box_w, widget_h, "Max score: " }
    {
        if (w <= 0 || h <= 0)       // Error handling
            throw invalid_argument("Bad Snake_window: non-positive size");
        // Keep default location of graphics
        snake_xy = snake.point(0);
        fruit_xy = fruit.point(0);
        // Attach graphics to window
        attach(field);
        attach(snake);
        attach(fruit);
        // Attach widgets to window
        game_menu.attach(new Button{ Point{ 0, 0 }, 0, 0, "&New game", cb_new_game });
        game_menu.attach(new Button{ Point{ 0, 0 }, 0, 0, "&Pause", cb_pause });
        game_menu.attach(new Button{ Point{ 0, 0 }, 0, 0, "&Quit", cb_quit });
        attach(game_menu);
        attach(game_button);
        attach(help_button);
        attach(help_box);
        attach(score_box);
        attach(max_score_box);
        // Default value for graphics
        show_graphics();
        put_on_top(snake);
        // Default value for widgets
        game_menu.hide();
        help_box.put(" SNAKE GAME\n"
            " Snake is a video game concept where the player maneuvers a line\n"
            "that grows in length, with the line itself being a primary obstacle.\n"
            "The concept originated in the 1976 arcade game Blockade.\n"
            " GAMEPLAY\n"
            " The player controls an object on a bordered plane. As it moves for-\n"
            "ward, it leaves a trail behind, resembling a moving snake. The snake\n"
            "has a specific length, so there is a moving tail a fixed number of units\n"
            "away from the head. The player loses when the snake runs into the\n"
            "screen border or itself.\n"
            " A sole player attempts to eat items by running into them with the he-\n"
            "ad of the snake. Each item eaten makes the snake longer, so con-\n"
            "trolling is progressively more difficult.\n"
            " CONTROL\n"
            " The snake moves forward automatically, everything you need to do\n"
            "is to choose the direction of moving. To choose the direction of mov-\n"
            "ing use arrow-buttons, that is,\n"
            "1) Left-arrow - to move in the left direction;\n"
            "2) Up-arrow - to move in the up direction;\n"
            "3) Right-arrow - to move in the right direction;\n"
            "4) Down-arrow - to move in the down direction.\n"
            "Remember: you can't rotate the snake's head to the opposite direc-\n"
            "tion, for instance, from the left to the right, or from the up to the\n"
            "down.\n"
            " ADDITIONAL NOTES\n"
            " Good luck on the game, try to eat as much as you can!\n");
        help_box.hide();
        score_box.put(0);
        max_score_box.put(0);
    }

    // Handles passed to window event, for instance, pressed key
    int Snake_window::handle(int event)
    {
        switch (event) {
        case FL_FOCUS: case FL_UNFOCUS:     // Focuses are skipped (required by system)
            return 1;
        case FL_KEYBOARD: {                 // Keys, pressed using keyboard
            switch (Fl::event_key()) {
                // Arrow-keys used to change snake's direction
            case FL_Left:                   // Left-arrow
                snake.set_direction(Snake::Direction::left);
                cout << "Changed direction to the left (" << static_cast<int>(snake.direction()) << ")\n";
                return 1;
            case FL_Up:                     // Up-arrow
                snake.set_direction(Snake::Direction::up);
                cout << "Changed direction to the up (" << static_cast<int>(snake.direction()) << ")\n";
                return 1;
            case FL_Right:                  // Right-arrow
                snake.set_direction(Snake::Direction::right);
                cout << "Changed direction to the right (" << static_cast<int>(snake.direction()) << ")\n";
                return 1;
            case FL_Down:                   // Down-arrow
                snake.set_direction(Snake::Direction::down);
                cout << "Changed direction to the down (" << static_cast<int>(snake.direction()) << ")\n";
                return 1;
            }
        }
        }
        return Window::handle(event);       // Everything else is handled by base window
    }

    // Callback function for game_loop
    void Snake_window::cb_game_loop(Address pw)
    {
        constexpr double delay = 0.25;                  // Delay of game's loop
        reference_to<Snake_window>(pw).game_loop();     // Call of action function
        Fl::repeat_timeout(delay, cb_game_loop, pw);    // Execute delay of game's loop
    }

    // Callback function for pause
    void Snake_window::cb_pause(Address, Address pw)
    {
        reference_to<Snake_window>(pw).pause();
        reference_to<Snake_window>(pw).game();
    }

    // Callback function for new game
    void Snake_window::cb_new_game(Address, Address pw)
    {
        reference_to<Snake_window>(pw).new_game();
        reference_to<Snake_window>(pw).game();
    }

    // Callback function for quit
    void Snake_window::cb_quit(Address, Address pw)
    {
        reference_to<Snake_window>(pw).quit();
        reference_to<Snake_window>(pw).game();
    }

    // Callback function for game
    void Snake_window::cb_game(Address, Address pw)
    {
        reference_to<Snake_window>(pw).game();
    }

    // Callback function for help
    void Snake_window::cb_help(Address, Address pw)
    {
        reference_to<Snake_window>(pw).help();
    }

    // Starts game's loop
    void Snake_window::start()
    {
        constexpr double delay = 1.0;                   // Delay before first timeout
        Fl::add_timeout(delay, cb_game_loop, this);     // Start game's loop and delay proccess
        cout << "Started the game\n";
    }

    // Starts all proccesses of game's loop
    void Snake_window::game_loop()
    {
        // Snake's bumping (obstacle is snake's body or field's borders)
        if (snake.is_body_except_head(snake.body_head())) {     // Snake's body as obstacle
            cout << "Bumped into the snake's body\n";
            // Pause after losed game
            return Fl::add_timeout(0.0, [](Address pw) { cb_pause(nullptr, pw); }, this);;
        }
        if (!is_grid(field, snake.body_head())) {               // Grid's border as obstacle
            cout << "Bumped into the grid's border\n";
            // Pause after losed game
            return Fl::add_timeout(0.0, [](Address pw) { cb_pause(nullptr, pw); }, this);
        }
        // Snake's eating
        if (snake.point(0) == fruit.point(0)) {
            snake.grow_length();
            put_score();                // Update score after eating
            cout << "Ate the fruit; the length becomes equal to " << snake.length() << '\n';
            // Randomly change location of fruit to everywhere, except snake's body
            while (snake.is_body(fruit))
                random_move(fruit, field.point(0), field.width() - fruit.width(), field.height() - fruit.height());
        }
        else snake.move_forward();      // Snake's moving
        cout << "Moved to (" << snake.point(0).x << ", " << snake.point(0).y << ")\n";
        redraw();                       // Redraw window after made changes
    }

    // Determines either game is paused or not
    bool Snake_window::is_pause()
    {
        return Fl::has_timeout(cb_game_loop, this) ? false : true;
    }

    // Pauses game if it's playing, or starts if it's already
    // paused, that is, pause prevents snake's moves
    void Snake_window::pause()
    {
        if (!is_pause()) {
            Fl::remove_timeout(cb_game_loop, this);     // Stop timeout
            cout << "Paused the game\n";
        }
        else start();                                   // Start timeout
    }

    // Starts new game, that is, returns everything to initial state
    void Snake_window::new_game()
    {
        if (!is_pause()) pause();                   // Pause game
        snake.shrink_length(current_score());       // Shrink length to default length
        // Return graphics to default location
        snake.set_direction(Snake::Direction::up);
        snake.set_direction(Snake::Direction::right);
        for (int i = 0; i < snake_sz; ++i)
            snake.move_forward();
        snake.move(-snake.point(0).x, -snake.point(0).y);       // Top-left angle of window
        snake.move(snake_xy.x, snake_xy.y);
        fruit.move(-fruit.point(0).x, -fruit.point(0).y);       // Top-left angle of window
        fruit.move(fruit_xy.x, fruit_xy.y);
        cout << "Started the new game; shrank the length to " << snake.length() << '\n';
        put_score();                                // Update score after shrinking
        redraw();                                   // Redraw window after made changes
    }

    // Quits game, that is, closes window
    void Snake_window::quit()
    {
        Window::hide();         // Hide window to close it
        cout << "Quited the game\n";
    }

    // Hides game button and shows game menu, if game button is pressed,
    // or shows game button and hides game menu, if game menu is pressed
    void Snake_window::game()
    {
        // Hide game button and show game menu
        if (game_button.visible()) {        // Game button is pressed
            game_button.hide();
            game_menu.show();
            help_button.move(game_menu.selection.size() * game_menu.width - help_button.width, 0);
            cout << "Hid the game button and showed the game menu\n";
        }
        // Hide game menu and show game button
        else {                              // Game menu is pressed
            game_menu.hide();
            game_button.show();
            help_button.move(help_button.width - game_menu.selection.size() * game_menu.width, 0);
            cout << "Hid the game menu and showed the game button\n";
        }
    }

    // Shows help box if it's invisible, or hides it if it's visible
    void Snake_window::help()
    {
        // Show help box
        if (!help_box.visible()) {      // Help box is invisible
            if (!is_pause()) pause();   // Pause game
            game_menu.selection[pause_ind].deactivate();
            hide_graphics();
            help_box.show();
            cout << "Showed the help box\n";
        }
        // Hide help box
        else {                          // Help box is visible
            game_menu.selection[pause_ind].activate();
            help_box.hide();
            show_graphics();
            cout << "Hid the help box\n";
        }
    }

    // Determines current score
    int Snake_window::current_score()
    {
        return snake.length() - snake_sz;
    }

    // Writes current score and max score into score boxes, if required
    void Snake_window::put_score()
    {
        int score = current_score();
        score_box.put(score);                   // Write current score
        if (score > max_score_box.get_int()) {  // New record
            max_score_box.put(score);           // Write max score
            cout << "Updated the max score to " << score << '\n';
        }
        cout << "Updated the current score to " << score << '\n';
    }

    // Shows game's graphics, that is, makes field, snake, and fruit visible
    void Snake_window::show_graphics()
    {
        // Modify color parameters of graphics
        field.set_color(Color::black);
        field.set_fill_color(Color::dark_green);
        snake.set_color(Color::black);
        snake.set_fill_color(Color::dark_yellow);
        snake.head_set_fill_color(Color::yellow);
        fruit.set_color(Color::black);
        fruit.set_fill_color(Color::red);
        cout << "Showed the graphics\n";
    }

    // Hides game's graphics, that is, makes field, snake, and fruit invisible
    void Snake_window::hide_graphics()
    {
        // Modify color parameters of graphics
        field.set_color(Color::invisible);
        field.set_fill_color(Color::invisible);
        snake.set_color(Color::invisible);
        snake.set_fill_color(Color::invisible);
        snake.head_set_fill_color(Color::invisible);
        fruit.set_color(Color::invisible);
        fruit.set_fill_color(Color::invisible);
        cout << "Hid the graphics\n";
    }

    //------------------------------------------------------------------------------

}   // End of Graph_lib namespace

//------------------------------------------------------------------------------

Game_graph.h

// Snake game's graphics

//------------------------------------------------------------------------------

#pragma once
#include "GraphicsLib/Graph.h"

//------------------------------------------------------------------------------

namespace Graph_lib {

    //------------------------------------------------------------------------------

    // Invariant: cell_w > 0, cell_h > 0, sz > 0
    class Snake : public Shape {
    public:
        enum class Direction {      // Possible directions of head
            left, up, right, down
        };

        // Construction
        Snake(Point xy, int cell_w, int cell_h, int sz);

        // Drawing
        void draw_lines() const override;
        void move(int dx, int dy) override;
        void move_forward();
        void grow_length();
        void shrink_length(int num);

        // Modification of parameters
        void set_color(Color c);
        void set_fill_color(Color c);
        void set_style(Line_style ls);
        void set_direction(Direction d);
        void head_set_fill_color(Color c);

        // Access to parameters
        const Rectangle& body_head() const;
        Direction direction() const { return head; }
        int length() const { return body.size(); }
        bool is_body(const Rectangle& cell) const;
        bool is_body_except_head(const Rectangle& cell) const;

    private:
        Vector_ref<Rectangle> body;
        Direction head;                 // Direction of head
    };

    //------------------------------------------------------------------------------

    // Helper function
    void random_move(Rectangle& rect, Point xy, int w, int h);

    //------------------------------------------------------------------------------

}   // End of Graph_lib namespace

//------------------------------------------------------------------------------

Game_graph.cpp

// Snake game's graphics

//------------------------------------------------------------------------------

#include "Game_graph.h"
#include "RandomNumber/Generator.h"

//------------------------------------------------------------------------------

namespace Graph_lib {

    //------------------------------------------------------------------------------

    // Indexes of snake's body
    constexpr int head_ind = 0;

    // Constructs snake with top left-angle of its head at xy, of sz
    // cells, and with size of each cell equal to cell_w * cell_h
    Snake::Snake(Point xy, int cell_w, int cell_h, int sz)
        : body{}, head{ Direction::right }
    {
        if (sz <= 0)        // Error handling
            throw invalid_argument("Bad Snake: non-positive length");
        // Fill of body
        for (int i = 0; i < sz; ++i)        // Horizontal line
            body.push_back(new Rectangle{ Point{ xy.x - i * cell_w, xy.y }, cell_w, cell_h });
        add(xy);            // Top-left angle of snake's head
    }

    // Draws snake and fills it with color if required
    void Snake::draw_lines() const
    {
        // Draw each cell of body
        for (int i = 0; i < body.size(); ++i)
            body[i].draw();
    }

    // Moves snake by dx at x-coordinate and dy at y-coordinate 
    void Snake::move(int dx, int dy)
    {
        Shape::move(dx, dy);
        // Move each cell of body
        for (int i = 0; i < body.size(); ++i)
            body[i].move(dx, dy);
    }

    // Moves snake forward, that is, moves each cell from tail to head
    // to its next neighbor, and moves head one cell in its direction
    void Snake::move_forward()
    {
        // Move each cell from tail to head to its next neighbour
        for (int i = body.size() - 1; i > 0; --i) {
            body[i].move(-body[i].point(0).x, -body[i].point(0).y);         // Move to initial point
            body[i].move(body[i - 1].point(0).x, body[i - 1].point(0).y);   // Move to neigbhour's point
        }
        // Move head one cell in its direction
        switch (head) {
        case Direction::left:       // Left-side
            body[head_ind].move(-body[head_ind].width(), 0);
            break;
        case Direction::up:         // Up-side
            body[head_ind].move(0, -body[head_ind].height());
            break;
        case Direction::right:      // Right-side
            body[head_ind].move(body[head_ind].width(), 0);
            break;
        case Direction::down:       // Down-side
            body[head_ind].move(0, body[head_ind].height());
            break;
        }
        set_point(0, body[head_ind].point(0));         // Update location of snake's head
    }

    // Grows snake in length, that is, adds one cell to its tail
    void Snake::grow_length()
    {
        const Point tail = body[body.size() - 1].point(0);      // Tail's coordinate
        move_forward();
        // Add new cell into body at previous tail's location
        body.push_back(new Rectangle{ tail, body[head_ind].width(), body[head_ind].height() });
        // Set same parameters for new tail as for all body
        body[body.size() - 1].set_color(color());
        body[body.size() - 1].set_fill_color(fill_color());
        body[body.size() - 1].set_style(style());
    }

    // Shrinks snake in length, that is, removes num cells from its body, starting with tail
    void Snake::shrink_length(int num)
    {
        if (num >= body.size())     // Error handling
            throw invalid_argument("Bad Snake: can't shrink to non-positive length");
        constexpr bool own = true;  // Cells are owned by body
        // Remove num cells from snake's body
        for (int i = 0; i < num; ++i)
            body.pop_back(own);
    }

    // Sets c as color of snake's lines
    void Snake::set_color(Color c)
    {
        Shape::set_color(c);
        // Set c as color of lines to each cell of body
        for (int i = 0; i < body.size(); ++i)
            body[i].set_color(c);
    }

    // Sets c as fill color of snake's body
    void Snake::set_fill_color(Color c)
    {
        Shape::set_fill_color(c);
        // Set c as fill color to each cell of body
        for (int i = 0; i < body.size(); ++i)
            body[i].set_fill_color(c);
    }

    // Sets c as fill color of snake's head
    void Snake::head_set_fill_color(Color c)
    {
        if (body.begin() == body.end())     // Error handling
            throw out_of_range("Bad Snake: can't set fill color to head of empty snake");
        body[head_ind].set_fill_color(c);
    }

    // Sets ls as line style of snake's body
    void Snake::set_style(Line_style ls)
    {
        Shape::set_style(ls);
        // Set ls as line style to each cell of body
        for (int i = 0; i < body.size(); ++i)
            body[i].set_style(ls);
    }

    // Sets d as direction of snake's head
    void Snake::set_direction(Direction d)
    {
        constexpr int opposite_diff = 2;     // Module of opposite direction's difference
        // Difference of directions
        const int diff = abs(static_cast<int>(head) - static_cast<int>(d));
        if (diff != opposite_diff)           // Set direction if it's not opposite
            head = d;
    }

    // Gets snake's head
    const Rectangle& Snake::body_head() const
    {
        if (body.cbegin() == body.cend())       // Error handling
            throw out_of_range("Bad Snake: can't get head of empty snake");
        return body[head_ind];
    }

    // Determines either cell is one of snake's body's cells
    bool Snake::is_body(const Rectangle& cell) const
    {
        // Search for cell in snake's body, located same as cell, and compare parameters
        return find_if(body.cbegin(), body.cend(), [&cell](const Rectangle* rect)
            { return rect->point(0) == cell.point(0); }) != body.cend()
            && body[0].width() == cell.width() && body[0].height() == cell.height();
    }

    // Determines either cell is one of snake's body's cells, except its head
    bool Snake::is_body_except_head(const Rectangle& cell) const
    {
        // Search for cell in snake's body, located same as cell, except snake's head, and compare parameters
        return body.cbegin() != body.cend() ? find_if(next(body.cbegin()), body.cend(),
            [&cell](const Rectangle* rect) { return rect->point(0) == cell.point(0); }) != body.cend()
            && body[0].width() == cell.width() && body[0].height() == cell.height() : false;
    }

    //------------------------------------------------------------------------------

    // Moves rect randomly in range [xy.x; xy.x + w] for x-coordinate and [xy.y; xy.y + h] for
    // y-coordinate, with xy as original point, w as width of range and h as height of range
    void random_move(Rectangle& rect, Point xy, int w, int h)
    {
        if (w < 0 || h < 0)     // Error handling
            throw invalid_argument("Bad random_move: invalid range for coordinates");
        // Move to original location, that is, xy
        rect.move(-(rect.point(0).x - xy.x), -(rect.point(0).y - xy.y));
        rect.move(rect.width() * randint(0, w / rect.width()),      // Random x-coordinate
            rect.height() * randint(0, h / rect.height()));         // Random y-coordinate
    }

    //------------------------------------------------------------------------------

}   // End of Graph_lib namespace

//------------------------------------------------------------------------------

Question

How can I improve my code in the future? Any tips are appreciated, but especially hope to see your thoughts on the structuring of the code, its flexibility, and readability.

Credits

Thanks for your time and efforts.

\$\endgroup\$
1
  • \$\begingroup\$ Great game! i was struggling with fltk callbacks without a button and this helped me out so thank you! \$\endgroup\$ Commented Nov 12, 2020 at 12:51

1 Answer 1

4
\$\begingroup\$

Don't catch errors you can't handle

You should catch exceptions if you can do something useful with them. However, just printing an error message and then quitting immediately is not useful. If you don't catch an exception, that is what will happen by default anyway.

Don't specify the window position

You should let the window manager decide the initial position of your window. It knows better where the user would like the window, and can use heuristics like where the mouse cursor is currently, where there is still unused space on the screen, and so on.

Make game menu buttons member variables

Why are the three buttons added to game_menu created with new, when other buttons are just member variables of Snake_window? Looking at your code it seems Window::attach() also has an overload that takes a reference to a Button, so that should just work, and will be more consistent.

Move the help text out of the constructor, use a raw string literal

The constructor of Snake_window() contains mostly logic for adding widgets to the window, but there's a huge blob of help text in the middle of it. It might make sense to move the text itself out of this function, and put it in a static variable. You can also use a raw string literal so you don't have to write quote characters and escape newlines anymore:

static const char *help_text =
R"( SNAKE GAME
 Snake is a video game concept where the player maneuvers a line
that grows in length, with the line itself being a primary obstacle.
The concept originated in the 1976 arcade game Blockade.
...
 ADDITIONAL NOTES
 Good luck on the game, try to eat as much as you can!
)";

...

Snake_window::Snake_window(...)
    : ...
{
    ...
    help_box.put(help_text);
    ...
}

Remove debug statements

In Snake_window::handle() you are printing something everytime the snake changes direction. It looks like you used this for debugging? You should remove this in production code. There are other examples throughout your code that prints to cout that should be removed.

Give a name to reference_to<Snake_window>(pw)

It's a bit unfortunate that FLTK doesn't support callbacks to non-static member functions. So now you have to write reference_to<Snake_window>(pw) to get the class instance. But it's a bit long and cryptic. Consider giving it a name, like self, which should be reasonably self-explanatory:

void Snake_window::cb_pause(Address, Address pw)
{
    auto self = reference_to<Snake_window>(pw);
    self.pause();
    self.game();
}

The body of the snake

This is where it went horribly wrong. Let's look at how the body is declared:

Vector_ref<Rectangle> body;

I see that Vector_ref is kind of a wrapper around std::vector<T *>. But why do you need to store the Rectangles by pointer or reference? Looking at your GitHub repository, it seems Rectangle derives from Shape, but you deleted the copy constructor and copy assignment operator. I don't see a reason for that. If you want to prevent someone from copying a bare Shape, it is better to make the copy operations protected, like so:

class Shape {
    ...
protected:
    Shape(const Shape &other) = default;
    Shape &operator=(const Shape &other) = default;
    ...
};

Once you have that, you should be able to create a vector of Rectangles like so:

std::vector<Rectangle> body;

But there are other issues, which I'll discuss below:

Use a std::deque<> to store the body positions

You are using a vector, and whenever you remove the tail piece and add a new head piece, you have to shift all the positions in the body. That's quite an expensive operation. Your own for-loop is very inefficient, because you move each point twice. If you use a std::vector, you could use pop_back() and emplace() like so:

void Snake::move_forward() {
    body.pop_back();
    body.emplace(body.begin(), { /* new head constructor arguments */ });
}

But then std::vector will just shift all element for you. What you ideally want is to keep all the body positions as they are, and then remove the tail and add a new head in O(1) time. That can be done by using either a std::list, but if you want something that works more like a std::vector, a std::deque is ideal. Your code would then look like:

void Snake::move_forward() {
    body.pop_back();
    body.emplace_front({ /* new head constructor arguments */ });
}

And again:

Avoid moving points unnecessarily

I see this pattern being used in several places:

fruit.move(-fruit.point(0).x, -fruit.point(0).y); // Top-left angle of window
fruit.move(fruit_xy.x, fruit_xy.y);

Basically what you want is setting the fruit position to fruit_xy. Why not create a member function of Rectangle that allows direct setting of the desired position, so you can write the following:

fruit.set_xy(fruit_xy);

Simplifying growing the body

Instead of having a separate function to grow the body, which first moves the snake (which removes its old tail), and then add the old tail back, consider changing Snake::move_forward() to optionally not remove the tail. I would do this by adding a member variable to Snake that indicates how many elements the body needs to grow with:

class Snake {
    ...
public:
    void grow(size_t length) { to_grow += length; }
private:
    size_t to_grow;
};

And then in Snake::move_forward(), do something like this:

void Snake::move_forward() {
    if (to_grow)
        to_grow--;
    else
        body.pop_back();

    body.emplace_front({ /* new head constructor arguments */ });
}

Use assert() to check for things that shouldn't be possible

I see several member functions of Snake that check whether body.begin() == body.end(). That's only true if the length of the body is zero. But the constructor of Snake already throws an error if you specify a length that is less than 1. So this check if in principle unnecessary. But, it's good practice to encode your assumptions using assert() statements, so these assumptions can be checked in debug builds, but won't slow down release builds, like so:

#include <cassert>
...
const Rectangle &Snake::body_head() const {
    assert(head_ind >= 0 && head_ind < body.size());
    return body[head_ind];
}

Although it would be simpler to either use body.front() to get the head element, and write:

const Rectangle &Snake::body_head() const {
    assert(!body.empty());
    return body.front();
}

Although personally, in this particular case, if it is clear that the Snake always has a non-zero body length, I wouldn't write those assert() statements at all; they just clutter the code, and tools like Valgrind can catch out-of-bounds errors as well.

Regardless, I would use an assert in the constructor of Snake to check the length parameter instead of throwing an exception.

Asserts should generally be used to check for assumptions about your own code. But use if (...) plus some kind of error reporting (like throwing an exception) when the condition is something that depends on user input.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thank you a lot for your deep analysis! I'll take your notes into account, and will make changes in the code to make it better. \$\endgroup\$ Commented Aug 22, 2020 at 4:22

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.