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.

asked Aug 21, 2020 at 13:01
\$\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.

answered Aug 21, 2020 at 21:16
\$\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

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.