From 74e0c4e81486e9c09a86ab64cbc1b4c9b52c2e06 Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Sat, 10 Feb 2024 21:30:12 +0000 Subject: [PATCH 01/24] Initial commit - configured to build and run from Visual Studio on the local machine and a WSL: Ubuntu environment. --- .gitignore | 2 ++ CMakeLists.txt | 2 +- CMakeSettings.json | 29 +++++++++++++++++++++++++++++ src/main.cpp | 1 + 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 CMakeSettings.json 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..cbaeaae1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,5 +15,5 @@ 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) +string(STRIP "${SDL2_LIBRARIES}" SDL2_LIBRARIES) target_link_libraries(SnakeGame ${SDL2_LIBRARIES}) diff --git a/CMakeSettings.json b/CMakeSettings.json new file mode 100644 index 00000000..eb7b4f43 --- /dev/null +++ b/CMakeSettings.json @@ -0,0 +1,29 @@ +{ + "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": [] + } + ] +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 01aaa03f..c45b46f5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,6 +2,7 @@ #include "controller.h" #include "game.h" #include "renderer.h" +#undef main int main() { constexpr std::size_t kFramesPerSecond{60}; From 9ebb15edd508fe3911d15b613100eae6d97c2009 Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Sun, 11 Feb 2024 21:35:28 +0000 Subject: [PATCH 02/24] Filled out details in the README. --- README.md | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/README.md b/README.md index a3f6ebae..69da2f4a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # CPPND: Capstone Snake Game Example +## Table of Contents +- [Introduction](#introduction) +- [Dependencies for Running Locally](#dependencies-for-running-locally) +- [Basic Build Instructions](#basic-build-instructions) +- [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 +37,91 @@ 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 +11/02/2024 Rendered the snake and food on separate threads. + +11/02/2024 Created a food class. + +## Ideas for future features +- Implement a message queue to decide when to draw a new fruit. +- Render the snake and food on different threads. +- Allow players to enter their names and save their high scores to a text file. + - Add a leaderboard. +- Add fixed and moving obstacles to the game. + - Implement a hard barrier temporarily. +- Add different types of food to the game. + - Have each food manged by it's own thread. + - A consumable that makes the snake go into "ghost" mode temporarily. + - A consumable linked to another consumable that becomes. + - A consumable that renders a sprite image. +- Allow players to select the initial speed of the game. + - Add a starting dialogue. +- Add another snake to the game that is controlled by the computer using the A* search algorithm. + +## 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. | | +| | 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. | | +| | 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. | | + +### 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. | | +| | Class constructors utilize member initialization lists. | All class members that are set to argument values are initialized through member initialization lists. | | +| | 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. | | + +### 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 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. | | +| | 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 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. | | +| | 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 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. | | +| | A condition variable is used in the project. | A std::condition_variable is used in the project code to synchronize thread execution. | | + + +## 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 From d1096f9a8ceab857957f5dac6e05153865bb4d0e Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Mon, 12 Feb 2024 21:29:57 +0000 Subject: [PATCH 03/24] Code not tested yet but there are the bones of an initial implementation for the leaderboard class. --- README.md | 41 +++++++++++++--------- src/leaderboard.cpp | 84 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 16 deletions(-) create mode 100644 src/leaderboard.cpp diff --git a/README.md b/README.md index 69da2f4a..04b51324 100644 --- a/README.md +++ b/README.md @@ -38,25 +38,30 @@ In this project, you can build your own C++ application or extend this Snake gam 4. Run it: `./SnakeGame`. ## New Features Added -11/02/2024 Rendered the snake and food on separate threads. - -11/02/2024 Created a food class. +**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 also displays the date and time of the game.The leaderboard also displays the current users score. ## Ideas for future features -- Implement a message queue to decide when to draw a new fruit. + - Render the snake and food on different threads. +- Use threads to manage the game loop. +- Have a dialog at to start a new game. - Allow players to enter their names and save their high scores to a text file. - - Add a leaderboard. -- Add fixed and moving obstacles to the game. + - Add a leaderboard at the end of the game. +- Add fixed and moving obstacles to the game: - Implement a hard barrier temporarily. -- Add different types of food to the game. - - Have each food manged by it's own thread. +- Add different types of food to the game: + - 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. + - 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 linked to another consumable that becomes. - A consumable that renders a sprite image. -- Allow players to select the initial speed of the game. + - A moving consumable. +- Allow players to select game settings + - Set the intial speed of the snake. - Add a starting dialogue. - Add another snake to the game that is controlled by the computer using the A* search algorithm. +- Add two player mode. ## Project Rubric @@ -78,24 +83,28 @@ In this project, you can build your own C++ application or extend this Snake gam | 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. | | -| | 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. | | +| ☑ | 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. 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 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 project uses data structures and immutable variables. | The project uses arrays or vectors and uses constant variables. | The `Leaderboard` class keeps a vector of `Records`. | ### 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. | | -| | Class constructors utilize member initialization lists. | All class members that are set to argument values are initialized through member initialization lists. | | -| | 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. | | +| ☑ | 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.cpp contains 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. | +| ☑ | 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 our `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. | +| ☑ | 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 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` constructor and `write` method 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. | | diff --git a/src/leaderboard.cpp b/src/leaderboard.cpp new file mode 100644 index 00000000..94c42b1e --- /dev/null +++ b/src/leaderboard.cpp @@ -0,0 +1,84 @@ +'include ' +'include ' +'include ' + +class Record { + + public: + Record(int s, std:string n) : score(s), name(n) {}; + + Record(std::istream& is) { + is >> name >> score; + } + + int getScore() const { return score }; + std::string getName() const { return name }; + + bool operator<(const Record& other) const { + return score > other.getScore(); + } + + void write(std::ostream& os) const { + os << name << " " << score << "\n"; + } + + private: + int score; + std:string name; + + +}; + +class Leaderbord { + +public: + Leaderbord() { + getRecords(); + }; + + void addRecord(Record record) { + records.push_back(record); + } + + void getRecords() { + std::ifstream("leaderboard.txt"); + while (file) { + records.push_back(Record(file)); + } + }; + + void saveRecords() { + std::ofstream("leaderboard.txt"); + for( const auto& record : records) { + record.write(file); + } + } + + ); + + void printRecords(int n) { + sortRecords(); + for (int i =0; i records; + +}; \ No newline at end of file From fcf54a7e288c2365b92924f4397e82881340dbfb Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Tue, 13 Feb 2024 21:30:28 +0000 Subject: [PATCH 04/24] Added rule of file to the Records class because the move constructor will be called with std::move. --- README.md | 7 ++++--- src/leaderboard.cpp | 40 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 04b51324..72f3d55c 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ In this project, you can build your own C++ application or extend this Snake gam 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 also displays the date and time of the game.The leaderboard also displays the current users score. +**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 also displays the current users score. ## Ideas for future features @@ -62,6 +62,7 @@ In this project, you can build your own C++ application or extend this Snake gam - Add a starting dialogue. - 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 @@ -107,8 +108,8 @@ In this project, you can build your own C++ application or extend this Snake gam | ☑ | 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` constructor and `write` method 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. | | -| | 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 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 | +| ☑ | 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` | | | 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 diff --git a/src/leaderboard.cpp b/src/leaderboard.cpp index 94c42b1e..6c51d0c9 100644 --- a/src/leaderboard.cpp +++ b/src/leaderboard.cpp @@ -1,6 +1,6 @@ -'include ' -'include ' -'include ' +#include +#include +#include class Record { @@ -11,6 +11,34 @@ class Record { is >> name >> score; } + ~Record() {}; + + Record(const Record& other) : score(other.getScore()), name(other.getName()) {}; + + Record& operator = (const Record& other) { + if (this != &other) { + score = other.score; + name = other.name; + } + return *this + } + + Record(Record&& other) : score(other.score), name(other.name) { + other.score = 0; + other.name = ""; + } + + Record& operator = (Record&& other) { + if (this != &other) { + score = other.score; + name = other.name; + other.score = 0; + other.name = ""; + } + return *this; + } + + int getScore() const { return score }; std::string getName() const { return name }; @@ -36,10 +64,14 @@ class Leaderbord { getRecords(); }; - void addRecord(Record record) { + void addRecord(Record& record) { records.push_back(record); } + void addRecord(Record&& record) { + records.push_back(std::move(record)); + } + void getRecords() { std::ifstream("leaderboard.txt"); while (file) { From 66356b7bf2db204f5c3daaa922f5a9f09d50850a Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Tue, 13 Feb 2024 21:36:35 +0000 Subject: [PATCH 05/24] Updated README with my idea to meet the concurrency section of the Rubric. --- README.md | 2 ++ src/game.cpp | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/README.md b/README.md index 72f3d55c..f669f8dc 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ In this project, you can build your own C++ application or extend this Snake gam ## 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 also displays the current users score. +**13/02/2024** - Adding a food thread to add additional food to the game. The food is added at a user-specified interval. + ## Ideas for future features - Render the snake and food on different threads. diff --git a/src/game.cpp b/src/game.cpp index cc7d60f9..f651eed8 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -65,6 +65,10 @@ void Game::PlaceFood() { } } +void Game::SupplyFood() { + +} + void Game::Update() { if (!snake.alive) return; From 1f7f378ae7b9a8eecee28fb426caf155455796bd Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Tue, 13 Feb 2024 22:10:39 +0000 Subject: [PATCH 06/24] Progress made with threading... --- README.md | 5 +++-- src/game.cpp | 42 +++++++++++++++++++++++++++--------------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f669f8dc..d6d37633 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ In this project, you can build your own C++ application or extend this Snake gam ## 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 also displays the current users score. -**13/02/2024** - Adding a food thread to add additional food to the game. The food is added at a user-specified interval. +**13/02/2024** - To meet the concurrency requirement, I added a thread to load the leaderboard from a file. ## Ideas for future features @@ -65,7 +65,8 @@ In this project, you can build your own C++ application or extend this Snake gam - 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. - +- Try adding a food and a snake thread to manage the game's state. +- ## Project Rubric ### README (All Rubric Points REQUIRED) diff --git a/src/game.cpp b/src/game.cpp index f651eed8..a27ebb7b 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) @@ -66,25 +68,35 @@ void Game::PlaceFood() { } void Game::SupplyFood() { + while (true) { + std::this_thread::sleep_for(std::chrono::seconds(1)); -} - -void Game::Update() { - if (!snake.alive) return; + std::lock_guard lock(foodMutex); - snake.Update(); + PlaceFood(); + } - 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; - } +void Game::Update() { + if (!snake.alive) return; + + snake.Update(); + + int new_x = static_cast(snake.head_x); + int new_y = static_cast(snake.head_y); + + // Check if there's food over here + { + std::lock_guard lock(foodMutex); + if (food.x == new_x && food.y == new_y) { + score++; + PlaceFood(); + // Grow snake and increase speed. + snake.GrowBody(); + snake.speed += 0.02; + } + } } int Game::GetScore() const { return score; } From 1dd7eea000e6966a18f4395d605fdd082685a945 Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Tue, 13 Feb 2024 22:13:36 +0000 Subject: [PATCH 07/24] Reverted the game state as having a snake and food thread felt like an overcomplication at present. Will load the leaderboard asynchronously as discussed in the updated readme. --- src/game.cpp | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/game.cpp b/src/game.cpp index a27ebb7b..dd80a569 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -67,17 +67,6 @@ void Game::PlaceFood() { } } -void Game::SupplyFood() { - while (true) { - std::this_thread::sleep_for(std::chrono::seconds(1)); - - std::lock_guard lock(foodMutex); - - PlaceFood(); - } - -} - void Game::Update() { if (!snake.alive) return; @@ -87,15 +76,12 @@ void Game::Update() { int new_y = static_cast(snake.head_y); // Check if there's food over here - { - std::lock_guard lock(foodMutex); - if (food.x == new_x && food.y == new_y) { - score++; - PlaceFood(); - // Grow snake and increase speed. - snake.GrowBody(); - snake.speed += 0.02; - } + if (food.x == new_x && food.y == new_y) { + score++; + PlaceFood(); + // Grow snake and increase speed. + snake.GrowBody(); + snake.speed += 0.02; } } From e3626bdf086344bd4b4f4c91194404d5f409d057 Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Fri, 16 Feb 2024 20:46:22 +0000 Subject: [PATCH 08/24] Fixed some typos and and added a header file for leaderboard.h. --- src/leaderboard.cpp | 113 +++++++++++++++++++++++--------------------- src/leaderboard.h | 44 +++++++++++++++++ 2 files changed, 103 insertions(+), 54 deletions(-) create mode 100644 src/leaderboard.h diff --git a/src/leaderboard.cpp b/src/leaderboard.cpp index 6c51d0c9..0527534a 100644 --- a/src/leaderboard.cpp +++ b/src/leaderboard.cpp @@ -1,69 +1,67 @@ -#include -#include -#include +#include "leaderboard.h" class Record { - public: - Record(int s, std:string n) : score(s), name(n) {}; +public: + Record(int s, std::string n) : score(s), name(n) {}; - Record(std::istream& is) { - is >> name >> score; + Record(std::istream& is) { + if (!(is >> name >> score)) { + throw std::runtime_error("Error reading from file"); } + } - ~Record() {}; + ~Record() {}; - Record(const Record& other) : score(other.getScore()), name(other.getName()) {}; + Record(const Record& other) : score(other.getScore()), name(other.getName()) {}; - Record& operator = (const Record& other) { - if (this != &other) { - score = other.score; - name = other.name; - } - return *this + Record& operator = (const Record& other) { + if (this != &other) { + score = other.score; + name = other.name; } + return *this; + } + + Record(Record&& other) : score(other.score), name(other.name) { + other.score = 0; + other.name = ""; + } - Record(Record&& other) : score(other.score), name(other.name) { + Record& operator = (Record&& other) { + if (this != &other) { + score = other.score; + name = other.name; other.score = 0; other.name = ""; } - - Record& operator = (Record&& other) { - if (this != &other) { - score = other.score; - name = other.name; - other.score = 0; - other.name = ""; - } - return *this; - } + return *this; + } - int getScore() const { return score }; - std::string getName() const { return name }; + int getScore() const { return score; } + std::string getName() const { return name; } - bool operator<(const Record& other) const { - return score > other.getScore(); - } - - void write(std::ostream& os) const { - os << name << " " << score << "\n"; - } + bool operator<(const Record& other) const { + return score > other.getScore(); + } - private: - int score; - std:string name; + void write(std::ostream& os) const { + os << name << " " << score << "\n"; + } - +private: + int score; + std::string name; }; -class Leaderbord { +class Leaderboard { public: - Leaderbord() { + Leaderboard() { getRecords(); }; - + void addRecord(Record& record) { records.push_back(record); } @@ -73,25 +71,33 @@ class Leaderbord { } void getRecords() { - std::ifstream("leaderboard.txt"); - while (file) { - records.push_back(Record(file)); + std::ifstream file("leaderboard.txt"); + if (file.is_open()) { + while (file) { + try + records.push_back(Record(file)); + } + catch (std::runtime_error& e) { + std::cerr << "Error reading from file: " << e.what() << "\n"; + break; + } } }; void saveRecords() { - std::ofstream("leaderboard.txt"); - for( const auto& record : records) { + 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 printRecords(int n) { sortRecords(); - for (int i =0; i records; - }; \ No newline at end of file diff --git a/src/leaderboard.h b/src/leaderboard.h new file mode 100644 index 00000000..cdd52e0f --- /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); + Record& operator = (Record&& other); + + 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 From aea031445e6d2c16d540e5dd643559e261160e73 Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Fri, 16 Feb 2024 21:03:38 +0000 Subject: [PATCH 09/24] Added leaderboard code to main. --- src/main.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index c45b46f5..a030b889 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,7 +1,12 @@ #include +#include +#include +#include #include "controller.h" #include "game.h" #include "renderer.h" +#include "leaderboard.h" + #undef main int main() { @@ -12,12 +17,31 @@ int main() { constexpr std::size_t kGridWidth{32}; constexpr std::size_t kGridHeight{32}; + // Get the Leaderboard in the backgrond + std::promise p; + std::future f = p.get_future(); + std::thread t([&p] {p.set_value(Leaderboard()); }); + + // Get the name of the player + 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"; + + // Get the Leaderboard from the future + Leaderboard leaderboard = f.get(); + t.join(); + leaderboard.addRecord(Record(game.GetScore(), name)); + leaderboard.saveRecords(); + leaderboard.printRecords(10); + return 0; } \ No newline at end of file From 2e8d55fbaee087dca002c1614a5db6fb27d1192f Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Fri, 16 Feb 2024 22:18:38 +0000 Subject: [PATCH 10/24] Finished leaderboard.cpp and leaderboard.h. --- src/leaderboard.cpp | 233 ++++++++++++++++++++++---------------------- src/leaderboard.h | 4 +- 2 files changed, 116 insertions(+), 121 deletions(-) diff --git a/src/leaderboard.cpp b/src/leaderboard.cpp index 0527534a..f511c49d 100644 --- a/src/leaderboard.cpp +++ b/src/leaderboard.cpp @@ -1,121 +1,116 @@ #include "leaderboard.h" -class Record { - -public: - Record(int s, std::string n) : score(s), name(n) {}; - - Record(std::istream& is) { - if (!(is >> name >> score)) { - throw std::runtime_error("Error reading from file"); - } - } - - ~Record() {}; - - Record(const Record& other) : score(other.getScore()), name(other.getName()) {}; - - Record& operator = (const Record& other) { - if (this != &other) { - score = other.score; - name = other.name; - } - return *this; - } - - Record(Record&& other) : score(other.score), name(other.name) { - other.score = 0; - other.name = ""; - } - - Record& operator = (Record&& other) { - if (this != &other) { - score = other.score; - name = other.name; - other.score = 0; - other.name = ""; - } - return *this; - } - - - int getScore() const { return score; } - std::string getName() const { return name; } - - bool operator<(const Record& other) const { - return score > other.getScore(); - } - - void write(std::ostream& os) const { - os << name << " " << score << "\n"; - } - -private: - int score; - std::string name; -}; - -class Leaderboard { - -public: - Leaderboard() { - getRecords(); - }; - - void addRecord(Record& record) { - records.push_back(record); - } - - void addRecord(Record&& record) { - records.push_back(std::move(record)); - } - - void getRecords() { - std::ifstream file("leaderboard.txt"); - if (file.is_open()) { - while (file) { - try - records.push_back(Record(file)); - } - catch (std::runtime_error& e) { - std::cerr << "Error reading from file: " << e.what() << "\n"; - break; - } - } - }; - - void 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 printRecords(int n) { - sortRecords(); - for (int i = 0; i < n; i++) { - 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"; - } - } - - void sortRecords() { - std::sort(records.begin(), records.end()); - } - -private: - std::vector records; -}; \ No newline at end of file +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 index cdd52e0f..21aba5e0 100644 --- a/src/leaderboard.h +++ b/src/leaderboard.h @@ -14,8 +14,8 @@ class Record { ~Record(); Record(const Record& other); Record& operator = (const Record& other); - Record(Record&& other); - Record& operator = (Record&& other); + Record(Record&& other) noexcept; + Record& operator = (Record&& other) noexcept; int getScore() const; std::string getName() const; From 6f4216350350507c3bdcb04ae6c1039c21d4a475 Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Fri, 16 Feb 2024 22:19:07 +0000 Subject: [PATCH 11/24] Changed formatting in main.cpp a little. --- src/main.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index a030b889..ece3e298 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,7 +10,7 @@ #undef main int main() { - constexpr std::size_t kFramesPerSecond{60}; + constexpr std::size_t kFramesPerSecond{60}; constexpr std::size_t kMsPerFrame{1000 / kFramesPerSecond}; constexpr std::size_t kScreenWidth{640}; constexpr std::size_t kScreenHeight{640}; @@ -22,10 +22,10 @@ int main() { std::future f = p.get_future(); std::thread t([&p] {p.set_value(Leaderboard()); }); - // Get the name of the player - std::string name; - std::cout << "Enter your name: "; - std::cin >> name; + + std::string name; + std::cout << "Enter your name: "; + std::cin >> name; Renderer renderer(kScreenWidth, kScreenHeight, kGridWidth, kGridHeight); Controller controller; @@ -33,8 +33,9 @@ int main() { 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(); From 12a6b92593c4be0557070a9a4c3a7b3b6f23bdbd Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Fri, 16 Feb 2024 22:19:36 +0000 Subject: [PATCH 12/24] Add settings to compile on windows and linux. --- CMakeLists.txt | 4 ++-- CMakeSettings.json | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cbaeaae1..2907be42 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,6 @@ 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) +add_executable(SnakeGame src/main.cpp src/game.cpp src/controller.cpp src/renderer.cpp src/snake.cpp src/leaderboard.cpp) string(STRIP "${SDL2_LIBRARIES}" SDL2_LIBRARIES) -target_link_libraries(SnakeGame ${SDL2_LIBRARIES}) +target_link_libraries(SnakeGame ${SDL2_LIBRARIES}) \ No newline at end of file diff --git a/CMakeSettings.json b/CMakeSettings.json index eb7b4f43..0375ae59 100644 --- a/CMakeSettings.json +++ b/CMakeSettings.json @@ -23,7 +23,13 @@ "ctestCommandArgs": "", "inheritEnvironments": [ "linux_x64" ], "wslPath": "${defaultWSLPath}", - "variables": [] + "variables": [ + { + "name": "CMAKE_CXX_FLAGS", + "value": "-pthread", + "type": "STRING" + } + ] } ] } \ No newline at end of file From 81bb6423d0db20f5133c74a715c3f12dd43964e9 Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Fri, 16 Feb 2024 22:29:08 +0000 Subject: [PATCH 13/24] Updated CMakeLists.txt for UNIX. --- CMakeLists.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2907be42..e5beb0f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,4 +16,10 @@ include_directories(${SDL2_INCLUDE_DIRS} src) add_executable(SnakeGame src/main.cpp src/game.cpp src/controller.cpp src/renderer.cpp src/snake.cpp src/leaderboard.cpp) string(STRIP "${SDL2_LIBRARIES}" SDL2_LIBRARIES) -target_link_libraries(SnakeGame ${SDL2_LIBRARIES}) \ No newline at end of file +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 From 67a628b90e11b2ef2bc377b98e0b7b13750317ca Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Fri, 16 Feb 2024 22:31:53 +0000 Subject: [PATCH 14/24] Update leaderborad printing. --- src/leaderboard.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/leaderboard.cpp b/src/leaderboard.cpp index f511c49d..4f2898c8 100644 --- a/src/leaderboard.cpp +++ b/src/leaderboard.cpp @@ -102,7 +102,7 @@ void Leaderboard::printRecords(int n) { default: std::cout << i + 1 << "th: "; break; } - std::cout << records[i].getName() << " " << records[i].getScore() << "\n"; + std::cout << records[i].getName() << " - " << records[i].getScore() << "\n"; } else { break; From 9254826e7b9e52a993614a36114c1a42c6b93d93 Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Fri, 16 Feb 2024 23:01:23 +0000 Subject: [PATCH 15/24] Updated the readme to satisfy the project rubric. --- README.md | 44 +++++++++++++++++++++++++++----------------- src/main.cpp | 3 +-- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d6d37633..4b9437db 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,16 @@ - [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 @@ -38,17 +48,17 @@ In this project, you can build your own C++ application or extend this Snake gam 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 also displays the current users score. +**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. -**13/02/2024** - To meet the concurrency requirement, I added a thread to load the leaderboard from a file. +**13/02/2024** - To meet the concurrency requirement, I used a thread to load the leaderboard from a file in the background. -## Ideas for future features +## Feature Ideas - Render the snake and food on different threads. - Use threads to manage the game loop. - Have a dialog at to start a new game. - Allow players to enter their names and save their high scores to a text file. - - Add a leaderboard at the end of the game. + - Add a graphical leaderboard at the end of the game. - Add fixed and moving obstacles to the game: - Implement a hard barrier temporarily. - Add different types of food to the game: @@ -87,20 +97,20 @@ In this project, you can build your own C++ application or extend this Snake gam | 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. 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 which read and write data to a file. | +| ☑ | 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 keeps a vector of `Records`. | +| ☑ | 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.cpp contains 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. | -| ☑ | 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 our `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. | -| ☑ | Classes follow an appropriate inheritance hierarchy. | Inheritance hierarchies are logical. On member functions in an inherited class override virtual base class functions. | | +| ☑ | 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. | | @@ -108,19 +118,19 @@ In this project, you can build your own C++ application or extend this Snake gam | 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` constructor and `write` method use pass by reference. | +| ☑ | 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 | -| ☑ | 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` | +| ☑ | 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. | | -| | 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. | | +| ☑ | The project uses multithreading. | The project uses multiple threads or async tasks in the execution. | A thread is used to load the current leaderboard on line 23 of *main.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 promise and future are created on lines 21 and 22 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. | | | | A condition variable is used in the project. | A std::condition_variable is used in the project code to synchronize thread execution. | | diff --git a/src/main.cpp b/src/main.cpp index ece3e298..51f858da 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,7 +10,7 @@ #undef main int main() { - constexpr std::size_t kFramesPerSecond{60}; + constexpr std::size_t kFramesPerSecond{60}; constexpr std::size_t kMsPerFrame{1000 / kFramesPerSecond}; constexpr std::size_t kScreenWidth{640}; constexpr std::size_t kScreenHeight{640}; @@ -22,7 +22,6 @@ int main() { std::future f = p.get_future(); std::thread t([&p] {p.set_value(Leaderboard()); }); - std::string name; std::cout << "Enter your name: "; std::cin >> name; From 55fb6f804f2daff2858e13ee94fb5a5ddd0b02c1 Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Sat, 17 Feb 2024 17:05:41 +0000 Subject: [PATCH 16/24] Tried to create a threaded implementaion of bad food... but code is getting confusing. --- src/game.cpp | 65 +++++++++++++++++++++++++++++++++++++++++++++++- src/renderer.cpp | 18 +++++++++++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/game.cpp b/src/game.cpp index dd80a569..357be454 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -19,6 +19,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) { @@ -27,7 +28,11 @@ 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) + + lock.lock(); + renderer.RenderBadFood(bad_food); + lock.unlock(); frame_end = SDL_GetTicks(); @@ -67,6 +72,45 @@ 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)) { + bad_food.x = x; + bad_food.y = y; + is_bad_food_active = true; + return; + } + } +} + +void Game::BadFoodTimer() +{ + const int badSeconds = 10; + auto startTime = std::chrono::high_resolution_clock::now(); + std::unique_lock lock(mutex); + while (is_bad_food_active) + { + auto current_Time = std::chrono::high_resolution_clock::now(); + auto elapsed_Seconds = std::chrono::duration_cast(current_Time - startTime).count(); + if (elapsed_Seconds >= badSeconds ) + { + // Bonus food time is up + is_bad_food_active = false; + bad_food.x = 1; + bad_food.y = 1; + break; + } + lock.unlock(); + // Wait for a short interval or until the condition_variable is notified + cond.wait_for(lock, std::chrono::milliseconds(800)); + } +} + void Game::Update() { if (!snake.alive) return; @@ -75,10 +119,29 @@ void Game::Update() { int new_x = static_cast(snake.head_x); int new_y = static_cast(snake.head_y); + // Check if there's bad food over here + lock.lock(); + if (is_bad_food_active && bad_food.x == new_x && bad_food.y == new_y) { + lock.unlock(); + return; + } + lock.unlock(); + // Check if there's food over here if (food.x == new_x && food.y == new_y) { score++; PlaceFood(); + lock.lock(); + if (!is_bad_food_active) + { + if (score>0 && score % 3 == 0) + { + PlaceBadFood(); + is_bad_food_active = true; + badFoodTimer = std::thread(&Game::badFoodTimer, this); + BadFoodTimer.detach(); + } + } // Grow snake and increase speed. snake.GrowBody(); snake.speed += 0.02; diff --git a/src/renderer.cpp b/src/renderer.cpp index 1ca1c9f7..fb410910 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -38,7 +38,20 @@ Renderer::~Renderer() { SDL_Quit(); } -void Renderer::Render(Snake const snake, SDL_Point const &food) { + void Renderer::RenderBadFood(SDL_Point const& bad_food) { + SDL_Rect block; + block.w = screen_width / grid_width; + block.h = screen_height / grid_height; + + // Render bad food + SDL_SetRenderDrawColor(sdl_renderer, 0xFF, 0xFF, 0xFF, 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, bool const is_bad_food_active, SDL_Point const &bad_food) { + SDL_Rect block; block.w = screen_width / grid_width; block.h = screen_height / grid_height; @@ -53,6 +66,7 @@ void Renderer::Render(Snake const snake, SDL_Point const &food) { 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 +89,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()); From 292ed16911832876338064cf7cd3a2669a26ca35 Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Sat, 17 Feb 2024 17:40:33 +0000 Subject: [PATCH 17/24] BadFood class is almost complete. Just need to ensure the timing works correctly. --- src/bad_food.cpp | 54 ++++++++++++++++++++++++++++++++++++++++++++ src/game.cpp | 58 ++++++++++++++++++++++-------------------------- 2 files changed, 81 insertions(+), 31 deletions(-) create mode 100644 src/bad_food.cpp diff --git a/src/bad_food.cpp b/src/bad_food.cpp new file mode 100644 index 00000000..981e3c5f --- /dev/null +++ b/src/bad_food.cpp @@ -0,0 +1,54 @@ +class BadFood { + +public: + + BadFood() : is_active(false), x(1), y(1) {} + + void Place(int new_w, int new_y) { + std::lock_guard lock(mutex); + x = new_x; + y = new_y; + is_active = true; + } + + void Remove() { + std::lock_guard lock(mutex); + x = 1; + y = 1; + is_active = false; + } + + bool IsActive() { + std::lock_guard lock(mutex); + return is_active; + } + + bool IsEaten(int head_x, int head_y) { + std::lock_guard lock(mutex); + return head_x == x && head_y == y; + } + + std::pair GetPosition() { + std::lock_guard lock(mutex); + return {x, y}; + } + + // Need to figure out how to do this safely... Remove method is also calling the mutex. + void BadFoodTimer() { + const int duration = 10; + auto start_time = std::chrono::high_resolution_clock::now(); + while (is_active) { + 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; + } + } + +private: + std::mutex mutex; + bool is_active; + int x, y; + +}; \ No newline at end of file diff --git a/src/game.cpp b/src/game.cpp index 357be454..9b25a808 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -28,11 +28,11 @@ void Game::Run(Controller const &controller, Renderer &renderer, // Input, Update, Render - the main game loop. controller.HandleInput(running, snake); Update(); - renderer.Render(snake, food) - - lock.lock(); - renderer.RenderBadFood(bad_food); - lock.unlock(); + renderer.Render(snake, food); + if (badFood.IsActive()) { + auto [bad_food_x, bad_food_y] = badFood.GetPosition(); + renderer.RenderBadFood(bad_food_x, bad_food_y); + } frame_end = SDL_GetTicks(); @@ -73,19 +73,16 @@ void Game::PlaceFood() { } void Game::PlaceBadFood() { - int x, y; + 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. + 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)) { - bad_food.x = x; - bad_food.y = y; - is_bad_food_active = true; - return; - } - } + badFood.Place(x, y); + } + } } void Game::BadFoodTimer() @@ -120,31 +117,30 @@ void Game::Update() { int new_y = static_cast(snake.head_y); // Check if there's bad food over here - lock.lock(); - if (is_bad_food_active && bad_food.x == new_x && bad_food.y == new_y) { - lock.unlock(); - return; + if (badFood.IsEaten(new_x, new_y)) + { + score--; + badFood.Remove(); + snake.GrowBody(); + snake.speed += 0.02; } - lock.unlock(); // Check if there's food over here if (food.x == new_x && food.y == new_y) { score++; - PlaceFood(); - lock.lock(); - if (!is_bad_food_active) + + PlaceFood(); + if (score > 0 && score % 3 == 0) { - if (score>0 && score % 3 == 0) - { - PlaceBadFood(); - is_bad_food_active = true; - badFoodTimer = std::thread(&Game::badFoodTimer, this); - BadFoodTimer.detach(); - } + PlaceBadFood(); + badFoodTimer = std::thread(&BadFood::BadFoodTimer, &badFood); + } + // Grow snake and increase speed. snake.GrowBody(); snake.speed += 0.02; + } } From 5b9d45186f85b5e760663911c5fd187f183529e1 Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Sat, 17 Feb 2024 17:44:39 +0000 Subject: [PATCH 18/24] Think BadFoodTiImer is ow thread safe. --- src/bad_food.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/bad_food.cpp b/src/bad_food.cpp index 981e3c5f..f0f41fbd 100644 --- a/src/bad_food.cpp +++ b/src/bad_food.cpp @@ -37,13 +37,18 @@ class BadFood { void BadFoodTimer() { const int duration = 10; auto start_time = std::chrono::high_resolution_clock::now(); - while (is_active) { + while (true) { + if !(IsActive()) { + break; + } 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; } + std::this_thread::sleep_for(std::chrono::milliseconds(800)); + } } private: From 1e2ecea680cab6a61848957c6afd375c4727431c Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Sun, 18 Feb 2024 10:33:56 +0000 Subject: [PATCH 19/24] Happy with my bad food code structure. Need to create header files and test,. --- src/bad_food.cpp | 33 ++++++++++++++++++------------ src/game.cpp | 53 ++++++++++++++++++++++-------------------------- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/src/bad_food.cpp b/src/bad_food.cpp index f0f41fbd..59f4f811 100644 --- a/src/bad_food.cpp +++ b/src/bad_food.cpp @@ -4,32 +4,38 @@ class BadFood { BadFood() : is_active(false), x(1), y(1) {} - void Place(int new_w, int new_y) { - std::lock_guard lock(mutex); + void Place(int new_x, int new_y) { + std::lock_guard lock(data_mutex); x = new_x; y = new_y; is_active = true; } void Remove() { - std::lock_guard lock(mutex); + std::lock_guard lock(data_mutex); x = 1; y = 1; is_active = false; } bool IsActive() { - std::lock_guard lock(mutex); + std::lock_guard lock(data_mutex); return is_active; } + void Cancel() { + std::unique_lock lock(cancel_mutex); + cancel = true; + cond.notify_one(); + } + bool IsEaten(int head_x, int head_y) { - std::lock_guard lock(mutex); + std::lock_guard lock(data_mutex); return head_x == x && head_y == y; } std::pair GetPosition() { - std::lock_guard lock(mutex); + std::lock_guard lock(data_mutex); return {x, y}; } @@ -37,23 +43,24 @@ class BadFood { void BadFoodTimer() { const int duration = 10; auto start_time = std::chrono::high_resolution_clock::now(); - while (true) { - if !(IsActive()) { - break; - } + std::unique_lock lock(cancel_mutex); + 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; - } - std::this_thread::sleep_for(std::chrono::milliseconds(800)); + } + cond.wait_for(lock, std::chrono::milliseconds(500)); } } private: - std::mutex mutex; + std::mutex data_mutex; + std::mutex cancel_mutex; + std::condition_variable cond; bool is_active; + bool cancel = false; int x, y; }; \ No newline at end of file diff --git a/src/game.cpp b/src/game.cpp index 9b25a808..44786bbd 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -12,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(); @@ -81,33 +88,17 @@ void Game::PlaceBadFood() { // food. if (!snake.SnakeCell(x, y) && (x != food.x && y != food.y)) { badFood.Place(x, y); + if (badFoodTimer.joinable()) + { + badFood.Cancel(); + badFoodTimer.join(); + } + badFoodTimer = std::thread(&BadFood::BadFoodTimer, &badFood); + return; } } } -void Game::BadFoodTimer() -{ - const int badSeconds = 10; - auto startTime = std::chrono::high_resolution_clock::now(); - std::unique_lock lock(mutex); - while (is_bad_food_active) - { - auto current_Time = std::chrono::high_resolution_clock::now(); - auto elapsed_Seconds = std::chrono::duration_cast(current_Time - startTime).count(); - if (elapsed_Seconds >= badSeconds ) - { - // Bonus food time is up - is_bad_food_active = false; - bad_food.x = 1; - bad_food.y = 1; - break; - } - lock.unlock(); - // Wait for a short interval or until the condition_variable is notified - cond.wait_for(lock, std::chrono::milliseconds(800)); - } -} - void Game::Update() { if (!snake.alive) return; @@ -120,27 +111,31 @@ void Game::Update() { if (badFood.IsEaten(new_x, new_y)) { score--; + badFood.Remove(); + badFood.Cancel(); + if (badFoodTimer.joinable()) badFoodTimer.join(); + snake.GrowBody(); snake.speed += 0.02; } // Check if there's food over here if (food.x == new_x && food.y == new_y) { - score++; + // Update the score + score++; + + // Place any more food PlaceFood(); - if (score > 0 && score % 3 == 0) + if (score > 0 && score % 3 == 0 && !badFood.IsActive()) { PlaceBadFood(); - badFoodTimer = std::thread(&BadFood::BadFoodTimer, &badFood); - } // Grow snake and increase speed. snake.GrowBody(); - snake.speed += 0.02; - + snake.speed += 0.02; } } From 56530690e241a852ebf35858bf05d6bf00d40a08 Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Sun, 18 Feb 2024 11:02:33 +0000 Subject: [PATCH 20/24] Partially fixed the rendering of the bad food. --- src/bad_food.cpp | 120 +++++++++++++++++++++-------------------------- src/bad_food.h | 30 ++++++++++++ src/game.cpp | 5 +- src/game.h | 5 ++ src/renderer.cpp | 6 +-- src/renderer.h | 1 + 6 files changed, 95 insertions(+), 72 deletions(-) create mode 100644 src/bad_food.h diff --git a/src/bad_food.cpp b/src/bad_food.cpp index 59f4f811..5003dff7 100644 --- a/src/bad_food.cpp +++ b/src/bad_food.cpp @@ -1,66 +1,54 @@ -class BadFood { - -public: - - BadFood() : is_active(false), x(1), y(1) {} - - void Place(int new_x, int new_y) { - std::lock_guard lock(data_mutex); - x = new_x; - y = new_y; - is_active = true; - } - - void Remove() { - std::lock_guard lock(data_mutex); - x = 1; - y = 1; - is_active = false; - } - - bool IsActive() { - std::lock_guard lock(data_mutex); - return is_active; - } - - void Cancel() { - std::unique_lock lock(cancel_mutex); - cancel = true; - cond.notify_one(); - } - - bool IsEaten(int head_x, int head_y) { - std::lock_guard lock(data_mutex); - return head_x == x && head_y == y; - } - - std::pair GetPosition() { - std::lock_guard lock(data_mutex); - return {x, y}; - } - - // Need to figure out how to do this safely... Remove method is also calling the mutex. - void BadFoodTimer() { - const int duration = 10; - auto start_time = std::chrono::high_resolution_clock::now(); - std::unique_lock lock(cancel_mutex); - 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)); - } - } - -private: - std::mutex data_mutex; - std::mutex cancel_mutex; - std::condition_variable cond; - bool is_active; - bool cancel = false; - int x, y; - -}; \ No newline at end of file +#include "bad_food.h" +#include + +BadFood::BadFood() : is_active(false), x(1), y(1) {} + +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() { + 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) { + std::lock_guard lock(data_mutex); + return head_x == position.x && head_y == position.y; +} + +SDL_Point BadFood::GetPosition() { + std::lock_guard lock(data_mutex); + return location +} + +void BadFood::BadFoodTimer() { + const int duration = 10; + auto start_time = std::chrono::high_resolution_clock::now(); + std::unique_lock lock(cancel_mutex); + 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)); + } +} diff --git a/src/bad_food.h b/src/bad_food.h new file mode 100644 index 00000000..58a95f02 --- /dev/null +++ b/src/bad_food.h @@ -0,0 +1,30 @@ +#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(); + void Cancel(); + bool IsEaten(int head_x, int head_y); + SDL_Point GetPosition(); + void BadFoodTimer(); + +private: + std::mutex data_mutex; + std::mutex cancel_mutex; + bool is_active; + bool cancel = false; + SDL_Point position; + +}; + +#endif // !BAD_FOOD_H diff --git a/src/game.cpp b/src/game.cpp index 44786bbd..5bcb86b9 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -14,7 +14,7 @@ Game::Game(std::size_t grid_width, std::size_t grid_height) Game::~Game() { badFood.Cancel(); - if (badFoodTimer.joinable) { + if (badFoodTimer.joinable()) { badFoodTimer.join(); } } @@ -37,8 +37,7 @@ void Game::Run(Controller const &controller, Renderer &renderer, Update(); renderer.Render(snake, food); if (badFood.IsActive()) { - auto [bad_food_x, bad_food_y] = badFood.GetPosition(); - renderer.RenderBadFood(bad_food_x, bad_food_y); + renderer.RenderBadFood(badFood.GetPosition()); } frame_end = SDL_GetTicks(); diff --git a/src/game.h b/src/game.h index 75554afe..c01ffe76 100644 --- a/src/game.h +++ b/src/game.h @@ -2,10 +2,12 @@ #define GAME_H #include +#include #include "SDL.h" #include "controller.h" #include "renderer.h" #include "snake.h" +#include "bad_food.h" class Game { public: @@ -18,6 +20,8 @@ class Game { private: Snake snake; SDL_Point food; + BadFood badFood; + std::thread badFoodTimer; std::random_device dev; std::mt19937 engine; @@ -27,6 +31,7 @@ class Game { int score{0}; void PlaceFood(); + void PlaceBadFood(); void Update(); }; diff --git a/src/renderer.cpp b/src/renderer.cpp index fb410910..8f9c9809 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -38,19 +38,19 @@ Renderer::~Renderer() { SDL_Quit(); } - void Renderer::RenderBadFood(SDL_Point const& bad_food) { + void Renderer::RenderBadFood(SDL_Point const &&bad_food) { SDL_Rect block; block.w = screen_width / grid_width; block.h = screen_height / grid_height; // Render bad food - SDL_SetRenderDrawColor(sdl_renderer, 0xFF, 0xFF, 0xFF, 0xFF)); + SDL_SetRenderDrawColor(sdl_renderer, 0xFF, 0xFF, 0xFF, 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, bool const is_bad_food_active, SDL_Point const &bad_food) { +void Renderer::Render(Snake const snake, SDL_Point const &food) { SDL_Rect block; block.w = screen_width / grid_width; diff --git a/src/renderer.h b/src/renderer.h index fd28db2e..955874d5 100644 --- a/src/renderer.h +++ b/src/renderer.h @@ -11,6 +11,7 @@ class Renderer { const std::size_t grid_width, const std::size_t grid_height); ~Renderer(); + void RenderBadFood(SDL_Point const &&bad_food); void Render(Snake const snake, SDL_Point const &food); void UpdateWindowTitle(int score, int fps); From a79b1d72400bc81c005a12f0eb9f2050021f430f Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Sun, 18 Feb 2024 20:22:07 +0000 Subject: [PATCH 21/24] Debugged multi-threaded behaviour and the games now runs as I want. --- src/bad_food.cpp | 13 ++++++++----- src/bad_food.h | 9 +++++---- src/game.cpp | 13 +++++-------- src/game.h | 2 ++ src/renderer.cpp | 10 ++++++++-- src/renderer.h | 5 +++-- 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/bad_food.cpp b/src/bad_food.cpp index 5003dff7..d0a6bef7 100644 --- a/src/bad_food.cpp +++ b/src/bad_food.cpp @@ -1,7 +1,7 @@ #include "bad_food.h" #include -BadFood::BadFood() : is_active(false), x(1), y(1) {} +BadFood::BadFood() : is_active(false) {} void BadFood::Place(int new_x, int new_y) { std::lock_guard lock(data_mutex); @@ -17,7 +17,8 @@ void BadFood::Remove() { is_active = false; } -bool BadFood::IsActive() { + +bool BadFood::IsActive() const { std::lock_guard lock(data_mutex); return is_active; } @@ -28,20 +29,21 @@ void BadFood::Cancel() { cond.notify_one(); } -bool BadFood::IsEaten(int head_x, int head_y) { +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() { +SDL_Point BadFood::GetPosition() const { std::lock_guard lock(data_mutex); - return location + 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(); @@ -51,4 +53,5 @@ void BadFood::BadFoodTimer() { } cond.wait_for(lock, std::chrono::milliseconds(500)); } + cancel = false; } diff --git a/src/bad_food.h b/src/bad_food.h index 58a95f02..9f25f0a1 100644 --- a/src/bad_food.h +++ b/src/bad_food.h @@ -12,15 +12,16 @@ class BadFood { void Place(int new_x, int new_y); void Remove(); - bool IsActive(); + bool IsActive() const; void Cancel(); - bool IsEaten(int head_x, int head_y); - SDL_Point GetPosition(); + bool IsEaten(int head_x, int head_y) const; + SDL_Point GetPosition() const; void BadFoodTimer(); private: - std::mutex data_mutex; + mutable std::mutex data_mutex; std::mutex cancel_mutex; + std::condition_variable cond; bool is_active; bool cancel = false; SDL_Point position; diff --git a/src/game.cpp b/src/game.cpp index 5bcb86b9..c9ec8afd 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -35,10 +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); - if (badFood.IsActive()) { - renderer.RenderBadFood(badFood.GetPosition()); - } + renderer.Render(snake, food, badFood); frame_end = SDL_GetTicks(); @@ -86,12 +83,12 @@ void Game::PlaceBadFood() { // 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)) { - badFood.Place(x, y); if (badFoodTimer.joinable()) { badFood.Cancel(); badFoodTimer.join(); } + badFood.Place(x, y); badFoodTimer = std::thread(&BadFood::BadFoodTimer, &badFood); return; } @@ -116,7 +113,7 @@ void Game::Update() { if (badFoodTimer.joinable()) badFoodTimer.join(); snake.GrowBody(); - snake.speed += 0.02; + snake.speed += speed_increment; } // Check if there's food over here @@ -127,14 +124,14 @@ void Game::Update() { // Place any more food PlaceFood(); - if (score > 0 && score % 3 == 0 && !badFood.IsActive()) + if (score > 0 && !badFood.IsActive()) { PlaceBadFood(); } // Grow snake and increase speed. snake.GrowBody(); - snake.speed += 0.02; + snake.speed += speed_increment; } } diff --git a/src/game.h b/src/game.h index c01ffe76..0fc65378 100644 --- a/src/game.h +++ b/src/game.h @@ -12,6 +12,7 @@ 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; @@ -29,6 +30,7 @@ class Game { std::uniform_int_distribution random_h; int score{0}; + float speed_increment = 0.01; void PlaceFood(); void PlaceBadFood(); diff --git a/src/renderer.cpp b/src/renderer.cpp index 8f9c9809..4c1ca564 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -38,7 +38,7 @@ Renderer::~Renderer() { SDL_Quit(); } - void Renderer::RenderBadFood(SDL_Point const &&bad_food) { + void Renderer::RenderBadFood(SDL_Point const &bad_food) { SDL_Rect block; block.w = screen_width / grid_width; block.h = screen_height / grid_height; @@ -48,9 +48,11 @@ Renderer::~Renderer() { block.x = bad_food.x * block.w; block.y = bad_food.y * block.h; SDL_RenderFillRect(sdl_renderer, &block); + + SDL_RenderPresent(sdl_renderer); } -void Renderer::Render(Snake const snake, SDL_Point const &food) { +void Renderer::Render(Snake const snake, SDL_Point const &food, BadFood const &bad_food) { SDL_Rect block; block.w = screen_width / grid_width; @@ -60,6 +62,10 @@ 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; diff --git a/src/renderer.h b/src/renderer.h index 955874d5..edad6f7f 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,8 +12,8 @@ class Renderer { const std::size_t grid_width, const std::size_t grid_height); ~Renderer(); - void RenderBadFood(SDL_Point const &&bad_food); - 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: From 6db43c9b0b23854542031ea87f977f06e9fbe77f Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Sun, 18 Feb 2024 20:22:46 +0000 Subject: [PATCH 22/24] Updated CMakeLists.txt with the new source. --- CMakeLists.txt | 2 +- CMakeSettings.json | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e5beb0f2..e2a26184 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,7 +14,7 @@ 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 src/leaderboard.cpp) +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}) diff --git a/CMakeSettings.json b/CMakeSettings.json index 0375ae59..8cfe419a 100644 --- a/CMakeSettings.json +++ b/CMakeSettings.json @@ -30,6 +30,18 @@ "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 From 2b912f00203af8e8c2f0ab5377bbe3003a79246b Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Sun, 18 Feb 2024 20:33:20 +0000 Subject: [PATCH 23/24] Switched to using aysnc to load the leaderboard and cleared a few errors. --- src/bad_food.cpp | 2 +- src/main.cpp | 7 +++---- src/renderer.cpp | 6 ++---- src/renderer.h | 2 +- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/bad_food.cpp b/src/bad_food.cpp index d0a6bef7..45db30e4 100644 --- a/src/bad_food.cpp +++ b/src/bad_food.cpp @@ -1,7 +1,7 @@ #include "bad_food.h" #include -BadFood::BadFood() : is_active(false) {} +BadFood::BadFood() : is_active(false), position{0, 0} {} void BadFood::Place(int new_x, int new_y) { std::lock_guard lock(data_mutex); diff --git a/src/main.cpp b/src/main.cpp index 51f858da..6d9af3e8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,9 +18,9 @@ int main() { constexpr std::size_t kGridHeight{32}; // Get the Leaderboard in the backgrond - std::promise p; - std::future f = p.get_future(); - std::thread t([&p] {p.set_value(Leaderboard()); }); + std::future f = std::async(std::launch::async, []() { + return Leaderboard(); + }); std::string name; std::cout << "Enter your name: "; @@ -38,7 +38,6 @@ int main() { // Get the Leaderboard from the future Leaderboard leaderboard = f.get(); - t.join(); leaderboard.addRecord(Record(game.GetScore(), name)); leaderboard.saveRecords(); leaderboard.printRecords(10); diff --git a/src/renderer.cpp b/src/renderer.cpp index 4c1ca564..d3b4e521 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -39,7 +39,7 @@ Renderer::~Renderer() { } void Renderer::RenderBadFood(SDL_Point const &bad_food) { - SDL_Rect block; + 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; @@ -48,13 +48,11 @@ Renderer::~Renderer() { block.x = bad_food.x * block.w; block.y = bad_food.y * block.h; SDL_RenderFillRect(sdl_renderer, &block); - - SDL_RenderPresent(sdl_renderer); } void Renderer::Render(Snake const snake, SDL_Point const &food, BadFood const &bad_food) { - SDL_Rect block; + 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; diff --git a/src/renderer.h b/src/renderer.h index edad6f7f..e75c7497 100644 --- a/src/renderer.h +++ b/src/renderer.h @@ -4,7 +4,7 @@ #include #include "SDL.h" #include "snake.h" -#include "bad_food.h"; +#include "bad_food.h" class Renderer { public: From 4f881529e5bdd7f673c2ba71220ec617ad2f1530 Mon Sep 17 00:00:00 2001 From: Ryan Potter Date: Sun, 18 Feb 2024 21:02:59 +0000 Subject: [PATCH 24/24] Switched bad food to red and updated the readme. --- README.md | 56 ++++++++++++++++++++++++++++-------------------- src/renderer.cpp | 2 +- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 4b9437db..0e77efcf 100644 --- a/README.md +++ b/README.md @@ -48,35 +48,45 @@ In this project, you can build your own C++ application or extend this Snake gam 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. +**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. -**13/02/2024** - To meet the concurrency requirement, I used a thread to load the leaderboard from a file in the background. +**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 -- Render the snake and food on different threads. -- Use threads to manage the game loop. -- Have a dialog at to start a new game. -- Allow players to enter their names and save their high scores to a text file. +- **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. -- Add fixed and moving obstacles to the game: - - Implement a hard barrier temporarily. -- Add different types of food to 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 linked to another consumable that becomes. - - A consumable that renders a sprite image. + - A consumable that goes bad/mouldy and reduces a player's score. - A moving consumable. -- Allow players to select game settings - - Set the intial speed of the snake. - - Add a starting dialogue. -- 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. -- Try adding a food and a snake thread to manage the game's state. -- + - 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) @@ -129,10 +139,10 @@ In this project, you can build your own C++ application or extend this Snake gam | Done | Success Criteria | Specifications | Evidence | |------|------------------|----------------|----------| -| ☑ | The project uses multithreading. | The project uses multiple threads or async tasks in the execution. | A thread is used to load the current leaderboard on line 23 of *main.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 promise and future are created on lines 21 and 22 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. | | -| | A condition variable is used in the project. | A std::condition_variable is used in the project code to synchronize thread execution. | | +| ☑ | 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 diff --git a/src/renderer.cpp b/src/renderer.cpp index d3b4e521..f57def33 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -44,7 +44,7 @@ Renderer::~Renderer() { block.h = screen_height / grid_height; // Render bad food - SDL_SetRenderDrawColor(sdl_renderer, 0xFF, 0xFF, 0xFF, 0xFF); + 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);