diff --git a/.gitignore b/.gitignore
index c129b08a..493a2f2a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
.github/**
+/.vs
+/out/build
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2be36482..e2a26184 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -14,6 +14,12 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/")
find_package(SDL2 REQUIRED)
include_directories(${SDL2_INCLUDE_DIRS} src)
-add_executable(SnakeGame src/main.cpp src/game.cpp src/controller.cpp src/renderer.cpp src/snake.cpp)
-string(STRIP ${SDL2_LIBRARIES} SDL2_LIBRARIES)
-target_link_libraries(SnakeGame ${SDL2_LIBRARIES})
+add_executable(SnakeGame src/main.cpp src/game.cpp src/controller.cpp src/renderer.cpp src/snake.cpp src/leaderboard.cpp src/bad_food.cpp)
+string(STRIP "${SDL2_LIBRARIES}" SDL2_LIBRARIES)
+target_link_libraries(SnakeGame PRIVATE ${SDL2_LIBRARIES})
+
+if(UNIX)
+ set(THREADS_PREFER_PTHREAD_FLAG ON)
+ find_package(Threads REQUIRED)
+ target_link_libraries(SnakeGame PRIVATE Threads::Threads)
+endif()
\ No newline at end of file
diff --git a/CMakeSettings.json b/CMakeSettings.json
new file mode 100644
index 00000000..8cfe419a
--- /dev/null
+++ b/CMakeSettings.json
@@ -0,0 +1,47 @@
+{
+ "configurations": [
+ {
+ "name": "x64-Debug",
+ "generator": "Ninja",
+ "configurationType": "Debug",
+ "inheritEnvironments": [ "msvc_x64_x64" ],
+ "buildRoot": "${projectDir}\\out\\build\\${name}",
+ "installRoot": "${projectDir}\\out\\install\\${name}",
+ "cmakeCommandArgs": "",
+ "buildCommandArgs": "",
+ "ctestCommandArgs": ""
+ },
+ {
+ "name": "WSL-GCC-Debug",
+ "generator": "Ninja",
+ "configurationType": "Debug",
+ "buildRoot": "${projectDir}\\out\\build\\${name}",
+ "installRoot": "${projectDir}\\out\\install\\${name}",
+ "cmakeExecutable": "cmake",
+ "cmakeCommandArgs": "",
+ "buildCommandArgs": "",
+ "ctestCommandArgs": "",
+ "inheritEnvironments": [ "linux_x64" ],
+ "wslPath": "${defaultWSLPath}",
+ "variables": [
+ {
+ "name": "CMAKE_CXX_FLAGS",
+ "value": "-pthread",
+ "type": "STRING"
+ }
+ ]
+ },
+ {
+ "name": "x64-Release",
+ "generator": "Ninja",
+ "configurationType": "RelWithDebInfo",
+ "buildRoot": "${projectDir}\\out\\build\\${name}",
+ "installRoot": "${projectDir}\\out\\install\\${name}",
+ "cmakeCommandArgs": "",
+ "buildCommandArgs": "",
+ "ctestCommandArgs": "",
+ "inheritEnvironments": [ "msvc_x64_x64" ],
+ "variables": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index a3f6ebae..0e77efcf 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,22 @@
# CPPND: Capstone Snake Game Example
+## Table of Contents
+- [Introduction](#introduction)
+- [Dependencies for Running Locally](#dependencies-for-running-locally)
+- [Basic Build Instructions](#basic-build-instructions)
+- [New Features Added](#new-features-added)
+- [Feature Ideas](#feature-ideas)
+- [Project Rubric](#project-rubric)
+ - [README (All Rubric Points REQUIRED)](#readme-all-rubric-points-required)
+ - [Compiling and Testing (All Rubric Points REQUIRED)](#compiling-and-testing-all-rubric-points-required)
+ - [Loops, Functions, I/O - meet at least 2 criteria](#loops-functions-io---meet-at-least-2-criteria)
+ - [Object Oriented Programming - meet at least 3 criteria](#object-oriented-programming---meet-at-least-3-criteria)
+ - [Memory Management - meet at least 3 criteria](#memory-management---meet-at-least-3-criteria)
+ - [Concurrency - meet at least 2 criteria](#concurrency---meet-at-least-2-criteria)
+- [References](#references)
+- [CC Attribution-ShareAlike 4.0 International](#cc-attribution-sharealike-40-international)
+
+## Introduction
This is a starter repo for the Capstone project in the [Udacity C++ Nanodegree Program](https://www.udacity.com/course/c-plus-plus-nanodegree--nd213). The code for this repo was inspired by [this](https://codereview.stackexchange.com/questions/212296/snake-game-in-c-with-sdl) excellent StackOverflow post and set of responses.
@@ -30,6 +47,114 @@ In this project, you can build your own C++ application or extend this Snake gam
3. Compile: `cmake .. && make`
4. Run it: `./SnakeGame`.
+## New Features Added
+**12/02/2024** - Added a leaderboard. The leaderboard is saved to a text file. The leaderboard is displayed at the end of the game. The leaderboard is sorted by the highest score. The leaderboard is limited to the top 10 scores. The leaderboard is initialised using `std::async` in case the file to load is very large.
+
+**18/02/2024** - To meet the concurrency requirement, I've implemented a second food that exists for a specified time. The food is "bad", rendered as a red square and disappears after 10s. A thread is required so that a timing loop can run without blocking the main thread of execution.
+
+## Feature Ideas
+
+- **Make the game look better**:
+ - Add a background image.
+ - Add a game over screen.
+ - Add a start screen.
+ - Add a pause screen.
+ - Add a settings screen.
+ - Add a game over sound.
+ - Add a sound when the snake eats food.
+ - Add a sound when the snake dies.
+ - Add a sound when the snake moves.
+ - Add a sound when the snake changes direction.
+ - Add a sound when the snake speeds up.
+ - Add a sound when the snake slows down
+ - Add a graphical leaderboard at the end of the game.
+ - Render items with sprites instead of squares.
+- **Explore software design ideas**:
+ - Manage the snake and food on different threads.
+ - Have a parent class to track all consummables.
+ - Have each consumable managed by it's own thread. Aim to understand the benefits of just using a loop.
+- **Add new mechanics to the game**:
+ - A consumable with a message queue to decide when to draw a new consumable/change the state of a consumable for the renderer e.g. food that becomes a barrier.
+ - A consumable that makes the snake go into "ghost" mode temporarily.
+ - A consumable that goes bad/mouldy and reduces a player's score.
+ - A moving consumable.
+ - A consumable that behaves as a barrier
+ - A consumable that implement a hard barrier around the games's border temporarily.
+- **Functionality Improvements**:
+ - Allow players to select game settings e.g. the intial speed of the snake.
+ - Add another snake to the game that is controlled by the computer using the A* search algorithm.
+ - Add two player mode.
+ - Add replay functionality to the game, storing the game state at each frame (or ever n frames) and then replaying the game from the start. Admittedly, you could just record the snakes's position and the food's position and then replay the game from the start.
+
+ -
+## Project Rubric
+
+### README (All Rubric Points REQUIRED)
+
+| Done | Success Criteria | Specifications | Evidence |
+|------|------------------|----------------|----------|
+| ☑ | A README with instructions is included with the project | The README is included with the project and has instructions for building/running the project. If any additional libraries are needed to run the project, these are indicated with cross-platform installation instructions. You can submit your writeup as markdown or pdf. | A README has been included with instructions to build the project. |
+| ☑ | The README indicates the new features you added to the game | The README indicates the new features you added to the game, along with the expected behavior or output of the program. | See the *New Features Added* Section. |
+| ☑ | The README includes information about each rubric point addressed | The README indicates which rubric points are addressed. The README also indicates where in the code (i.e. files and line numbers) that the rubric points are addressed. | See the current section. |
+
+### Compiling and Testing (All Rubric Points REQUIRED)
+
+| Done | Success Criteria | Specifications | Evidence |
+|------|------------------|----------------|----------|
+| ☑ | The submission must compile without any errors on the Udacity project workspace. | We strongly recommend using cmake and make, as provided in the starter repos. If you choose another build system, the code must be compiled on the Udacity project workspace. | The code has been compiled on the Udacity workspace. |
+
+### Loops, Functions, I/O - meet at least 2 criteria
+
+| Done | Success Criteria | Specifications | Evidence |
+|------|------------------|----------------|----------|
+| ☑ | The project demonstrates an understanding of C++ functions and control structures. | A variety of control structures are added to the project. The project code is clearly organized into functions. | *leaderboard.cpp* uses for, while and if loops, switch-case blocks and try-catch blcoks. Each class has functions with clearly defined scope. |
+| ☑ | The project reads data from a file and process the data, or the program writes data to a file. | The project reads data from an external file or writes data to a file as part of the necessary operation of the program. | *learderboard.cpp* has `getRecords` and `saveRecords` methods, starting on lines 62 and 77 respectively, which read and write data to a file. |
+| | The project accepts user input and processes the input. | In addition to controlling the snake, the game can also receive new types of input from the player. | |
+| ☑ | The project uses data structures and immutable variables. | The project uses arrays or vectors and uses constant variables. | The `Leaderboard` class stores each game result in a vector of `Records` as shown on line 41 of *leaderboard.h.* From line 15 of *leaderboard.h* the copy constructor ad copy-assignment operator for the `Record` class are defined, which take inputs defined as `const`. |
+
+### Object Oriented Programming - meet at least 3 criteria
+
+| Done | Success Criteria | Specifications | Evidence |
+|------|------------------|----------------|----------|
+| ☑ | One or more classes are added to the project with appropriate access specifiers for class members. | Classes are organized with attributes to hold data and methods to perform tasks. All class data members are explicitly specified as public, protected, or private. Member data that is subject to an invariant is hidden from the user and accessed via member methods. | *leaderboard.h* declares both the `Leaderboard` and `Record` classes. |
+| ☑ | Class constructors utilize member initialization lists. | All class members that are set to argument values are initialized through member initialization lists. | The `Record` class uses an initialiser list for the constructor defined on line 3 of *leaderboard.cpp*. |
+| ☑ | Classes abstract implementation details from their interfaces. | All class member functions document their effects, either through function names, comments, or formal documentation. Member functions do not change the program state in undocumented ways. | The `Record` class's implementation is abstracted from it's interface. We can change our records without changing how the `Record` class is used by the `Leaderboard` class. |
+| ☑ | Overloaded functions allow the same function to operate on different parameters. | One function is overloaded with different signatures for the same function name. | The `Record` class has two constructors with different function signatures defined on lines 3 and 5 of *leaderboard.cpp* |
+| | Classes follow an appropriate inheritance hierarchy. | Inheritance hierarchies are logical. On member functions in an inherited class override virtual base class functions. | |
+| | Template generalise functions in the project. | One function or class is declared with a template that allows it to accept a generic parameter. | |
+
+
+### Memory Management - meet at least 3 criteria
+
+| Done | Success Criteria | Specifications | Evidence |
+|------|------------------|----------------|----------|
+| ☑ | The project makes use of references in function declarations. | At least two variables are defined as references, or two functions use pass-by-reference in the project code. | The `Record` class' constructor defined on line 5 of *leaderboard.cpp* and `write` method defined on line 46 of *leaderboard.cpp* use pass by reference.|
+| | The project uses destructors appropriately. | At least one class that uses unmanaged dynamically allocated memory, along with any class that otherwise needs to modify state upon the termination of an object, uses a destructor. | |
+| | The project uses scope / Resource Acquisition Is Initialization (RAII) where appropriate. | The project follows the Resource Acquisition Is Initialization pattern where appropriate, by allocating objects at compile-time, initializing objects when they are declared, and utilizing scope to ensure their automatic destruction. | |
+| ☑ | The project follows the Rule of 5. | For all classes, if any one of the copy constructor, copy assignment operator, move constructor, move assignment operator, and destructor are defined, then all of these functions are defined. | Added the rule of 5 to the `Record` class as the move constructor will be called from the `addRecord` method of the `Leaderboard` class defined on line 58 of *leaderboard.cpp*. |
+| ☑ | The project uses move semantics to move data instead of copying it, where possible. | The project relies on the move semantics, instead of copying the object. | The `Leaderboard` class uses move semantics to add r-value `Records` on line 58 of *leaderboard.cpp* |
+| | The project uses smart pointers instead of raw pointers. | The project uses at least one smart pointer: unique_ptr, shared_ptr, or weak_ptr. | |
+
+### Concurrency - meet at least 2 criteria
+
+| Done | Success Criteria | Specifications | Evidence |
+|------|------------------|----------------|----------|
+| ☑ | The project uses multithreading. | The project uses multiple threads or async tasks in the execution. | `std::async` is used to load the current leaderboard on line 21 of *main.cpp*. A thread is used for the `bad_food` timer on line 93 of *game.cpp* |
+| | A promise and future is used in the project. | A promise and future is used to pass data from a worker thread to a parent thread in the project code. | A `Leaderboard` class future is created on lines 21 of *main.cpp*, respectively. |
+| ☑ | A mutex or lock is used in the project. | A mutex or lock (e.g. `std::lock_guard` or `std::unique_lock`) is used to protect data that is shared across multiple threads in the project code. | The `BadFood` class, implemented in *bad_food.cpp*, demonstrates extensive use of locks and mutexes to ensure that `bad-food` in *game.cpp* is accessed in a thread-safe way. |
+| ☑ | A condition variable is used in the project. | A std::condition_variable is used in the project code to synchronize thread execution. | A condition variable is used to cancel our timing loop immediately, without waiting for the 0.5s rest interval in the `BadFoodTimer` method to elapse. See the `BadFoodTimer` and `Cancel` methods starting on lines 26 and 42 of *bad_food.cpp*, respectively. |
+
+
+## References
+
+- [The StackExchange post that inspired this project](https://codereview.stackexchange.com/questions/212296/snake-game-in-c-with-sdl)
+- [An example of a great submission](https://github.com/nihguy/cpp-snake-game/tree/master) - maybe I'll do this one day.
+- [An explanation of the game loop](https://www.informit.com/articles/article.aspx?p=2928180&seqNum=4)
+- [Another explanation of the game loop](https://gameprogrammingpatterns.com/game-loop.html)
+- [Lazy Foo SDL Tutorials](https://lazyfoo.net/tutorials/SDL/01_hello_SDL/linux/index.php)
+- [Parallel Realities SDL Tutorials](https://www.parallelrealities.co.uk/tutorials/)
+- [TwinklebearDev SDL Tutorials](https://www.willusher.io/pages/sdl2/)
+- [SDL Wiki](https://wiki.libsdl.org/SDL2/FrontPage)
## CC Attribution-ShareAlike 4.0 International
diff --git a/src/bad_food.cpp b/src/bad_food.cpp
new file mode 100644
index 00000000..45db30e4
--- /dev/null
+++ b/src/bad_food.cpp
@@ -0,0 +1,57 @@
+#include "bad_food.h"
+#include
+
+BadFood::BadFood() : is_active(false), position{0, 0} {}
+
+void BadFood::Place(int new_x, int new_y) {
+ std::lock_guard lock(data_mutex);
+ position.x = new_x;
+ position.y = new_y;
+ is_active = true;
+}
+
+void BadFood::Remove() {
+ std::lock_guard lock(data_mutex);
+ position.x = 1;
+ position.y = 1;
+ is_active = false;
+}
+
+
+bool BadFood::IsActive() const {
+ std::lock_guard lock(data_mutex);
+ return is_active;
+}
+
+void BadFood::Cancel() {
+ std::unique_lock lock(cancel_mutex);
+ cancel = true;
+ cond.notify_one();
+}
+
+bool BadFood::IsEaten(int head_x, int head_y) const {
+ std::lock_guard lock(data_mutex);
+ return head_x == position.x && head_y == position.y;
+}
+
+SDL_Point BadFood::GetPosition() const {
+ std::lock_guard lock(data_mutex);
+ return position;
+}
+
+void BadFood::BadFoodTimer() {
+ const int duration = 10;
+ auto start_time = std::chrono::high_resolution_clock::now();
+ std::unique_lock lock(cancel_mutex);
+ cancel = false;
+ while (!cancel && IsActive()) {
+ auto current_time = std::chrono::high_resolution_clock::now();
+ auto elapsed_seconds = std::chrono::duration_cast(current_time - start_time).count();
+ if (elapsed_seconds >= duration) {
+ Remove();
+ break;
+ }
+ cond.wait_for(lock, std::chrono::milliseconds(500));
+ }
+ cancel = false;
+}
diff --git a/src/bad_food.h b/src/bad_food.h
new file mode 100644
index 00000000..9f25f0a1
--- /dev/null
+++ b/src/bad_food.h
@@ -0,0 +1,31 @@
+#ifndef BAD_FOOD_H
+#define BAD_FOOD_H
+
+#include
+#include
+#include "SDL.h"
+
+class BadFood {
+
+public:
+ BadFood();
+
+ void Place(int new_x, int new_y);
+ void Remove();
+ bool IsActive() const;
+ void Cancel();
+ bool IsEaten(int head_x, int head_y) const;
+ SDL_Point GetPosition() const;
+ void BadFoodTimer();
+
+private:
+ mutable std::mutex data_mutex;
+ std::mutex cancel_mutex;
+ std::condition_variable cond;
+ bool is_active;
+ bool cancel = false;
+ SDL_Point position;
+
+};
+
+#endif // !BAD_FOOD_H
diff --git a/src/game.cpp b/src/game.cpp
index cc7d60f9..c9ec8afd 100644
--- a/src/game.cpp
+++ b/src/game.cpp
@@ -1,5 +1,7 @@
#include "game.h"
#include
+#include
+#include
#include "SDL.h"
Game::Game(std::size_t grid_width, std::size_t grid_height)
@@ -10,6 +12,13 @@ Game::Game(std::size_t grid_width, std::size_t grid_height)
PlaceFood();
}
+Game::~Game() {
+ badFood.Cancel();
+ if (badFoodTimer.joinable()) {
+ badFoodTimer.join();
+ }
+}
+
void Game::Run(Controller const &controller, Renderer &renderer,
std::size_t target_frame_duration) {
Uint32 title_timestamp = SDL_GetTicks();
@@ -17,6 +26,7 @@ void Game::Run(Controller const &controller, Renderer &renderer,
Uint32 frame_end;
Uint32 frame_duration;
int frame_count = 0;
+ bool is_bad_food_active = false;
bool running = true;
while (running) {
@@ -25,7 +35,7 @@ void Game::Run(Controller const &controller, Renderer &renderer,
// Input, Update, Render - the main game loop.
controller.HandleInput(running, snake);
Update();
- renderer.Render(snake, food);
+ renderer.Render(snake, food, badFood);
frame_end = SDL_GetTicks();
@@ -65,22 +75,64 @@ void Game::PlaceFood() {
}
}
+void Game::PlaceBadFood() {
+ int x, y;
+ while (true) {
+ x = random_w(engine);
+ y = random_h(engine);
+ // Check that the location is not occupied by a snake item before placing
+ // food.
+ if (!snake.SnakeCell(x, y) && (x != food.x && y != food.y)) {
+ if (badFoodTimer.joinable())
+ {
+ badFood.Cancel();
+ badFoodTimer.join();
+ }
+ badFood.Place(x, y);
+ badFoodTimer = std::thread(&BadFood::BadFoodTimer, &badFood);
+ return;
+ }
+ }
+}
+
void Game::Update() {
- if (!snake.alive) return;
+ if (!snake.alive) return;
- snake.Update();
+ snake.Update();
- int new_x = static_cast(snake.head_x);
- int new_y = static_cast(snake.head_y);
+ int new_x = static_cast(snake.head_x);
+ int new_y = static_cast(snake.head_y);
- // Check if there's food over here
- if (food.x == new_x && food.y == new_y) {
- score++;
- PlaceFood();
- // Grow snake and increase speed.
- snake.GrowBody();
- snake.speed += 0.02;
- }
+ // Check if there's bad food over here
+ if (badFood.IsEaten(new_x, new_y))
+ {
+ score--;
+
+ badFood.Remove();
+ badFood.Cancel();
+ if (badFoodTimer.joinable()) badFoodTimer.join();
+
+ snake.GrowBody();
+ snake.speed += speed_increment;
+ }
+
+ // Check if there's food over here
+ if (food.x == new_x && food.y == new_y) {
+
+ // Update the score
+ score++;
+
+ // Place any more food
+ PlaceFood();
+ if (score > 0 && !badFood.IsActive())
+ {
+ PlaceBadFood();
+ }
+
+ // Grow snake and increase speed.
+ snake.GrowBody();
+ snake.speed += speed_increment;
+ }
}
int Game::GetScore() const { return score; }
diff --git a/src/game.h b/src/game.h
index 75554afe..0fc65378 100644
--- a/src/game.h
+++ b/src/game.h
@@ -2,14 +2,17 @@
#define GAME_H
#include
+#include
#include "SDL.h"
#include "controller.h"
#include "renderer.h"
#include "snake.h"
+#include "bad_food.h"
class Game {
public:
Game(std::size_t grid_width, std::size_t grid_height);
+ ~Game();
void Run(Controller const &controller, Renderer &renderer,
std::size_t target_frame_duration);
int GetScore() const;
@@ -18,6 +21,8 @@ class Game {
private:
Snake snake;
SDL_Point food;
+ BadFood badFood;
+ std::thread badFoodTimer;
std::random_device dev;
std::mt19937 engine;
@@ -25,8 +30,10 @@ class Game {
std::uniform_int_distribution random_h;
int score{0};
+ float speed_increment = 0.01;
void PlaceFood();
+ void PlaceBadFood();
void Update();
};
diff --git a/src/leaderboard.cpp b/src/leaderboard.cpp
new file mode 100644
index 00000000..4f2898c8
--- /dev/null
+++ b/src/leaderboard.cpp
@@ -0,0 +1,116 @@
+#include "leaderboard.h"
+
+Record::Record(int s, std::string n) : score(s), name(n) {}
+
+Record::Record(std::istream& is) {
+ if (!(is >> name >> score)) {
+ throw std::runtime_error("Error reading from file");
+ }
+}
+
+Record::~Record() {}
+
+Record::Record(const Record& other) : score(other.getScore()), name(other.getName()) {}
+
+Record& Record::operator = (const Record& other) {
+ if (this != &other) {
+ score = other.score;
+ name = other.name;
+ }
+ return *this;
+}
+
+Record::Record(Record&& other) noexcept : score(other.score), name(other.name) {
+ other.score = 0;
+ other.name = "";
+}
+
+Record& Record::operator = (Record&& other) noexcept {
+ if (this != &other) {
+ score = other.score;
+ name = other.name;
+ other.score = 0;
+ other.name = "";
+ }
+ return *this;
+}
+
+int Record::getScore() const { return score; }
+
+std::string Record::getName() const { return name; }
+
+bool Record::operator<(const Record& other) const {
+ return score > other.getScore();
+}
+
+void Record::write(std::ostream& os) const {
+ os << name << " " << score << "\n";
+}
+
+Leaderboard::Leaderboard() {
+ getRecords();
+}
+
+void Leaderboard::addRecord(Record& record) {
+ records.push_back(record);
+}
+
+void Leaderboard::addRecord(Record&& record) {
+ records.push_back(std::move(record));
+}
+
+void Leaderboard::getRecords() {
+ std::ifstream file("leaderboard.txt");
+ if (file.is_open()) {
+ while (file >> std::ws && !file.eof()) {
+ try {
+ records.push_back(Record(file));
+ }
+ catch (std::runtime_error& e) {
+ std::cerr << "Error reading from file: " << e.what() << "\n";
+ break;
+ }
+ }
+ }
+}
+
+void Leaderboard::saveRecords() {
+ std::ofstream file("leaderboard.txt");
+ if (!file.is_open()) {
+ throw std::runtime_error("Failed to open leaderboard.txt for writing.");
+ }
+ for (const auto& record : records) {
+ record.write(file);
+ }
+}
+
+void Leaderboard::printRecords(int n) {
+ sortRecords();
+ int i = 0;
+ std::cout << "\n";
+ std::cout << "Leaderboard\n";
+ std::cout << "===========\n";
+ for (const auto& record : records) {
+ if (i < n) {
+ switch (i) {
+ case 0:
+ std::cout << "1st: "; break;
+ case 1:
+ std::cout << "2nd: "; break;
+ case 2:
+ std::cout << "3rd: "; break;
+ default:
+ std::cout << i + 1 << "th: "; break;
+ }
+ std::cout << records[i].getName() << " - " << records[i].getScore() << "\n";
+ }
+ else {
+ break;
+ }
+ i++;
+ }
+}
+
+void Leaderboard::sortRecords() {
+ std::sort(records.begin(), records.end());
+}
diff --git a/src/leaderboard.h b/src/leaderboard.h
new file mode 100644
index 00000000..21aba5e0
--- /dev/null
+++ b/src/leaderboard.h
@@ -0,0 +1,44 @@
+#ifndef LEADERBOARD_H
+#define LEADERBOARD_H
+
+#include
+#include
+#include
+#include
+#include
+
+class Record {
+public:
+ Record(int s, std::string n);
+ Record(std::istream& is);
+ ~Record();
+ Record(const Record& other);
+ Record& operator = (const Record& other);
+ Record(Record&& other) noexcept;
+ Record& operator = (Record&& other) noexcept;
+
+ int getScore() const;
+ std::string getName() const;
+ bool operator<(const Record& other) const;
+ void write(std::ostream& os) const;
+
+private:
+ int score;
+ std::string name;
+};
+
+class Leaderboard {
+public:
+ Leaderboard();
+ void addRecord(Record& record);
+ void addRecord(Record&& record);
+ void getRecords();
+ void saveRecords();
+ void printRecords(int n);
+ void sortRecords();
+
+private:
+ std::vector records;
+};
+
+#endif // LEADERBOARD_H
diff --git a/src/main.cpp b/src/main.cpp
index 01aaa03f..6d9af3e8 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,7 +1,13 @@
#include
+#include
+#include
+#include
#include "controller.h"
#include "game.h"
#include "renderer.h"
+#include "leaderboard.h"
+
+#undef main
int main() {
constexpr std::size_t kFramesPerSecond{60};
@@ -11,12 +17,30 @@ int main() {
constexpr std::size_t kGridWidth{32};
constexpr std::size_t kGridHeight{32};
+ // Get the Leaderboard in the backgrond
+ std::future f = std::async(std::launch::async, []() {
+ return Leaderboard();
+ });
+
+ std::string name;
+ std::cout << "Enter your name: ";
+ std::cin >> name;
+
Renderer renderer(kScreenWidth, kScreenHeight, kGridWidth, kGridHeight);
Controller controller;
Game game(kGridWidth, kGridHeight);
game.Run(controller, renderer, kMsPerFrame);
+
std::cout << "Game has terminated successfully!\n";
- std::cout << "Score: " << game.GetScore() << "\n";
- std::cout << "Size: " << game.GetSize() << "\n";
+
+ std::cout << "You scored " << game.GetScore() << "\n";
+
+
+ // Get the Leaderboard from the future
+ Leaderboard leaderboard = f.get();
+ leaderboard.addRecord(Record(game.GetScore(), name));
+ leaderboard.saveRecords();
+ leaderboard.printRecords(10);
+
return 0;
}
\ No newline at end of file
diff --git a/src/renderer.cpp b/src/renderer.cpp
index 1ca1c9f7..f57def33 100644
--- a/src/renderer.cpp
+++ b/src/renderer.cpp
@@ -38,8 +38,21 @@ Renderer::~Renderer() {
SDL_Quit();
}
-void Renderer::Render(Snake const snake, SDL_Point const &food) {
- SDL_Rect block;
+ void Renderer::RenderBadFood(SDL_Point const &bad_food) {
+ SDL_Rect block{ 0, 0, screen_width / grid_width, screen_height / grid_height };
+ block.w = screen_width / grid_width;
+ block.h = screen_height / grid_height;
+
+ // Render bad food
+ SDL_SetRenderDrawColor(sdl_renderer, 0xFF, 0x00, 0x00, 0xFF);
+ block.x = bad_food.x * block.w;
+ block.y = bad_food.y * block.h;
+ SDL_RenderFillRect(sdl_renderer, &block);
+}
+
+void Renderer::Render(Snake const snake, SDL_Point const &food, BadFood const &bad_food) {
+
+ SDL_Rect block{ 0, 0, screen_width / grid_width, screen_height / grid_height };
block.w = screen_width / grid_width;
block.h = screen_height / grid_height;
@@ -47,12 +60,17 @@ void Renderer::Render(Snake const snake, SDL_Point const &food) {
SDL_SetRenderDrawColor(sdl_renderer, 0x1E, 0x1E, 0x1E, 0xFF);
SDL_RenderClear(sdl_renderer);
+ if (bad_food.IsActive()) {
+ RenderBadFood(bad_food.GetPosition());
+ }
+
// Render food
SDL_SetRenderDrawColor(sdl_renderer, 0xFF, 0xCC, 0x00, 0xFF);
block.x = food.x * block.w;
block.y = food.y * block.h;
SDL_RenderFillRect(sdl_renderer, &block);
+
// Render snake's body
SDL_SetRenderDrawColor(sdl_renderer, 0xFF, 0xFF, 0xFF, 0xFF);
for (SDL_Point const &point : snake.body) {
@@ -75,6 +93,8 @@ void Renderer::Render(Snake const snake, SDL_Point const &food) {
SDL_RenderPresent(sdl_renderer);
}
+
+
void Renderer::UpdateWindowTitle(int score, int fps) {
std::string title{"Snake Score: " + std::to_string(score) + " FPS: " + std::to_string(fps)};
SDL_SetWindowTitle(sdl_window, title.c_str());
diff --git a/src/renderer.h b/src/renderer.h
index fd28db2e..e75c7497 100644
--- a/src/renderer.h
+++ b/src/renderer.h
@@ -4,6 +4,7 @@
#include
#include "SDL.h"
#include "snake.h"
+#include "bad_food.h"
class Renderer {
public:
@@ -11,7 +12,8 @@ class Renderer {
const std::size_t grid_width, const std::size_t grid_height);
~Renderer();
- void Render(Snake const snake, SDL_Point const &food);
+ void RenderBadFood(SDL_Point const &bad_food);
+ void Render(Snake const snake, SDL_Point const &food, BadFood const &bad_food);
void UpdateWindowTitle(int score, int fps);
private: