diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..f912847 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,18 @@ +{ + "configurations": [ + { + "name": "windows-gcc-x86", + "includePath": [ + "${workspaceFolder}/**" + ], + "compilerPath": "C:/MinGW/bin/gcc.exe", + "cStandard": "${default}", + "cppStandard": "${default}", + "intelliSenseMode": "windows-gcc-x86", + "compilerArgs": [ + "" + ] + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..ce76bb9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "C/C++ Runner: Debug Session", + "type": "cppdbg", + "request": "launch", + "args": [], + "stopAtEntry": false, + "externalConsole": true, + "cwd": "d:/snake-cli/snake-cli-final", + "program": "d:/snake-cli/snake-cli-final/build/Debug/outDebug", + "MIMode": "gdb", + "miDebuggerPath": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bb879da --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,59 @@ +{ + "C_Cpp_Runner.cCompilerPath": "gcc", + "C_Cpp_Runner.cppCompilerPath": "g++", + "C_Cpp_Runner.debuggerPath": "gdb", + "C_Cpp_Runner.cStandard": "", + "C_Cpp_Runner.cppStandard": "", + "C_Cpp_Runner.msvcBatchPath": "C:/Program Files/Microsoft Visual Studio/VR_NR/Community/VC/Auxiliary/Build/vcvarsall.bat", + "C_Cpp_Runner.useMsvc": false, + "C_Cpp_Runner.warnings": [ + "-Wall", + "-Wextra", + "-Wpedantic", + "-Wshadow", + "-Wformat=2", + "-Wcast-align", + "-Wconversion", + "-Wsign-conversion", + "-Wnull-dereference" + ], + "C_Cpp_Runner.msvcWarnings": [ + "/W4", + "/permissive-", + "/w14242", + "/w14287", + "/w14296", + "/w14311", + "/w14826", + "/w44062", + "/w44242", + "/w14905", + "/w14906", + "/w14263", + "/w44265", + "/w14928" + ], + "C_Cpp_Runner.enableWarnings": true, + "C_Cpp_Runner.warningsAsError": false, + "C_Cpp_Runner.compilerArgs": [], + "C_Cpp_Runner.linkerArgs": [], + "C_Cpp_Runner.includePaths": [], + "C_Cpp_Runner.includeSearch": [ + "*", + "**/*" + ], + "C_Cpp_Runner.excludeSearch": [ + "**/build", + "**/build/**", + "**/.*", + "**/.*/**", + "**/.vscode", + "**/.vscode/**" + ], + "C_Cpp_Runner.useAddressSanitizer": false, + "C_Cpp_Runner.useUndefinedSanitizer": false, + "C_Cpp_Runner.useLeakSanitizer": false, + "C_Cpp_Runner.showCompilationTime": false, + "C_Cpp_Runner.useLinkTimeOptimization": false, + "C_Cpp_Runner.msvcSecureNoWarnings": false +} \ No newline at end of file diff --git a/main.cpp b/main.cpp index ef65093..ce66eab 100644 --- a/main.cpp +++ b/main.cpp @@ -1,9 +1,18 @@ #include "snake.h" +#include +using namespace std; int main(int argc, char *argv[]) { - thread input_thread(input_handler); - thread game_thread(game_play); + system("clear"); + + HighScores highScores; + Game game(10, highScores); + + // start input handler thread and game thread + thread input_thread(inputHandler, ref(game)); + thread game_thread(&Game::loop, &game); + input_thread.join(); game_thread.join(); -return 0; -} \ No newline at end of file + return 0; +} diff --git a/snake.h b/snake.h index ebe1192..7c06154 100644 --- a/snake.h +++ b/snake.h @@ -4,98 +4,172 @@ #include #include #include -#include // for system clear +#include #include #include #include using namespace std; -using std::chrono::system_clock; using namespace std::this_thread; -char direction='r'; +class HighScores { + vector scores; +public: + void update(int score) { + scores.push_back(score); + sort(scores.begin(), scores.end(), greater()); + if (scores.size() > 10) scores.resize(10); + } + void show() const { + cout << "\n=== TOP 10 HIGH SCORES ===\n"; + if (scores.empty()) { + cout << "No scores yet.\n"; + return; + } + for (size_t i = 0; i < scores.size(); i++) { + cout << i + 1 << ". " << scores[i] << endl; + } + } +}; + +class Game { + int size; + deque> snake; + pair food; + pair poison; + char direction; + bool paused; + int score; + int steps; + int foodsEaten; + int speedMs; + HighScores &highScores; // reference to shared highscore tracker + + pair randomCellExcluding(const pair &other = {-1, -1}) { + pair p; + do { + p = make_pair(rand() % size, rand() % size); + } while (find(snake.begin(), snake.end(), p) != snake.end() || p == other); + return p; + } + + pair getNextHead(pair current) { + if (direction == 'r') { + return make_pair(current.first, (current.second + 1) % size); + } else if (direction == 'l') { + return make_pair(current.first, current.second == 0 ? size - 1 : current.second - 1); + } else if (direction == 'd') { + return make_pair((current.first + 1) % size, current.second); + } else if (direction == 'u') { + return make_pair(current.first == 0 ? size - 1 : current.first - 1, current.second); + } + return current; + } + + void render() { + for (size_t i = 0; i < size; i++) { + for (size_t j = 0; j < size; j++) { + if (i == food.first && j == food.second) { + cout << "🍎"; + } else if (i == poison.first && j == poison.second) { + cout << "💀"; + } else if (find(snake.begin(), snake.end(), make_pair(int(i), int(j))) != snake.end()) { + cout << "🐍"; + } else { + cout << "⬜"; + } + } + cout << endl; + } + cout << "Length: " << snake.size() + << " | Score: " << score + << (paused ? " | PAUSED (press 'p' to resume)" : "") << endl; + } + + void gameOver(const string &reason) { + system("clear"); + cout << "Game Over - " << reason << "\nFinal Score: " << score << endl; + highScores.update(score); + highScores.show(); + exit(0); + } + +public: + Game(int gridSize, HighScores &hs) + : size(gridSize), direction('r'), paused(false), score(0), steps(0), + foodsEaten(0), speedMs(500), highScores(hs) { + snake.push_back(make_pair(0, 0)); + food = randomCellExcluding(); + poison = randomCellExcluding(food); + } + + void togglePause() { paused = !paused; } + + void changeDirection(char newDir) { direction = newDir; } + + //this is the loop wherein snake goes about doing its job + void loop() { + for (pair head = make_pair(0, 1);; head = getNextHead(head, direction)) { + cout << "\033[H"; + if (paused) { + render(); + sleep_for(200ms); + continue; + } + + // self collision - feature + if (find(snake.begin(), snake.end(), head) != snake.end()) { + gameOver("You hit yourself!"); + } + + // poison collision - feature + if (head == poison) { + gameOver("You ate poison!"); + } + + // normal food - feature + if (head == food) { + snake.push_back(head); + score += 10; + foodsEaten++; + if (foodsEaten % 10 == 0 && speedMs > 100) speedMs -= 50; + food = randomCellExcluding(poison); + } else { + snake.push_back(head); + snake.pop_front(); + } + + // move poison - feature + steps++; + if (steps % 10 == 0) poison = randomCellExcluding(food); + + render(); + sleep_for(std::chrono::milliseconds(speedMs)); + } + } +}; -void input_handler(){ - // change terminal settings +// Separate input handler function but pass Game to it -> this is to pass the game to it and separate input handler, to make it easy +void inputHandler(Game &game) { struct termios oldt, newt; tcgetattr(STDIN_FILENO, &oldt); newt = oldt; - // turn off canonical mode and echo newt.c_lflag &= ~(ICANON | ECHO); tcsetattr(STDIN_FILENO, TCSANOW, &newt); - map keymap = {{'d', 'r'}, {'a', 'l'}, {'w', 'u'}, {'s', 'd'}, {'q', 'q'}}; + + map keymap = { + {'d', 'r'}, {'a', 'l'}, {'w', 'u'}, {'s', 'd'} + }; while (true) { char input = getchar(); if (keymap.find(input) != keymap.end()) { - // This now correctly modifies the single, shared 'direction' variable - direction = keymap[input]; - }else if (input == 'q'){ + game.changeDirection(keymap[input]); + } else if (input == 'q') { exit(0); + } else if (input == 'p') { + game.togglePause(); } - // You could add an exit condition here, e.g., if (input == 'q') break; } tcsetattr(STDIN_FILENO, TCSANOW, &oldt); } -void render_game(int size, deque> &snake, pair food){ - for(size_t i=0;i get_next_head(pair current, char direction){ - pair next; - if(direction =='r'){ - next = make_pair(current.first,(current.second+1) % 10); - }else if (direction=='l') - { - next = make_pair(current.first, current.second==0?9:current.second-1); - }else if(direction =='d'){ - next = make_pair((current.first+1)%10,current.second); - }else if (direction=='u'){ - next = make_pair(current.first==0?9:current.first-1, current.second); - } - return next; - -} - - - -void game_play(){ - system("clear"); - deque> snake; - snake.push_back(make_pair(0,0)); - - pair food = make_pair(rand() % 10, rand() % 10); - for(pair head=make_pair(0,1);; head = get_next_head(head, direction)){ - // send the cursor to the top - cout << "\033[H"; - // check self collision - if (find(snake.begin(), snake.end(), head) != snake.end()) { - system("clear"); - cout << "Game Over" << endl; - exit(0); - }else if (head.first == food.first && head.second == food.second) { - // grow snake - food = make_pair(rand() % 10, rand() % 10); - snake.push_back(head); - }else{ - // move snake - snake.push_back(head); - snake.pop_front(); - } - render_game(10, snake, food); - cout << "length of snake: " << snake.size() << endl; - - sleep_for(500ms); - } -} diff --git a/snake_test.cpp b/snake_test.cpp index 42f8561..c72a3f6 100644 --- a/snake_test.cpp +++ b/snake_test.cpp @@ -1,49 +1,122 @@ #include #include "snake.h" +// Subclass Game to expose private methods and override gameOver +class TestGame : public Game { +public: + bool gameOverCalled = false; + string gameOverReason; -TEST(SnakeBehaviour, NextHeadRight) { - pair current = make_pair(rand() % 10, rand() % 10); - EXPECT_EQ(get_next_head(current, 'r'),make_pair(current.first,current.second+1)); + using Game::Game; // inherit constructor + using Game::changeDirection; -} + // Expose getNextHead + pair testNextHead(pair current) { + return Game::getNextHead(current); + } + + // Override gameOver to avoid exit() + void gameOver(const string &reason) override { + gameOverCalled = true; + gameOverReason = reason; + } + + // Helper to place food and poison at specific positions + void setFood(pair pos) { this->food = pos; } + void setPoison(pair pos) { this->poison = pos; } +}; +class SnakeEnhancementsTest : public ::testing::Test { +protected: + HighScores hs; + TestGame game{10, hs}; // 10x10 grid +}; + +// ---------------- Movement Tests ---------------- +TEST_F(SnakeEnhancementsTest, NextHeadRight) { + pair current = {5,5}; + game.changeDirection('r'); + EXPECT_EQ(game.testNextHead(current), make_pair(5,6)); +} -TEST(SnakeBehaviour, NextHeadLeft) { - pair current = make_pair(rand() % 10, rand() % 10); - EXPECT_EQ(get_next_head(current, 'l'),make_pair(current.first,current.second-1)); - +TEST_F(SnakeEnhancementsTest, NextHeadLeft) { + pair current = {5,5}; + game.changeDirection('l'); + EXPECT_EQ(game.testNextHead(current), make_pair(5,4)); } -TEST(SnakeBehaviour, NextHeadUp) { - pair current = make_pair(rand() % 10, rand() % 10); - EXPECT_EQ(get_next_head(current, 'u'),make_pair(current.first-1,current.second)); +// ---------------- Food Tests ---------------- +TEST_F(SnakeEnhancementsTest, EatingFoodIncreasesScoreAndLength) { + pair foodPos = {0,1}; + game.setFood(foodPos); + + // Initial snake length + size_t initialLength = 1; + + // Move head to food + pair head = game.testNextHead({0,0}); + game.changeDirection('r'); + + // Simulate eating food + if (head == foodPos) { + game.snake.push_back(head); + game.score += 10; + } + + EXPECT_EQ(game.score, 10); + EXPECT_EQ(game.snake.size(), initialLength + 1); } -TEST(SnakeBehaviour, NextHeadDown) { - pair current = make_pair(rand() % 10, rand() % 10); - EXPECT_EQ(get_next_head(current, 'd'),make_pair(current.first+1,current.second)); - +// ---------------- Poison Tests ---------------- +TEST_F(SnakeEnhancementsTest, HittingPoisonCallsGameOver) { + pair poisonPos = {0,1}; + game.setPoison(poisonPos); + + pair head = game.testNextHead({0,0}); + game.changeDirection('r'); + + // Simulate poison collision + if (head == poisonPos) { + game.gameOver("You ate poison!"); + } + + EXPECT_TRUE(game.gameOverCalled); + EXPECT_EQ(game.gameOverReason, "You ate poison!"); } +// ---------------- HighScores Tests ---------------- +TEST_F(SnakeEnhancementsTest, HighScoresUpdatesCorrectly) { + hs.update(50); + hs.update(30); + hs.update(70); -/** - * g++ -o my_tests snake_test.cpp -lgtest -lgtest_main -pthread; - * This command is a two-part shell command. Let's break it down. + vector expected = {70,50,30}; + for (size_t i=0; i current = {5,9}; + game.changeDirection('r'); + EXPECT_EQ(game.testNextHead(current), make_pair(5,0)); +} +TEST_F(SnakeEnhancementsTest, WrapAroundLeft) { + pair current = {5,0}; + game.changeDirection('l'); + EXPECT_EQ(game.testNextHead(current), make_pair(5,9)); +} + +TEST_F(SnakeEnhancementsTest, WrapAroundUp) { + pair current = {0,5}; + game.changeDirection('u'); + EXPECT_EQ(game.testNextHead(current), make_pair(9,5)); +} - * g++: This invokes the GNU C++ compiler. - * -o my_tests: This tells the compiler to create an executable file named - my_tests. - * hello_gtest.cpp: This is the C++ source file containing your tests. - * -lgtest: This links the Google Test library, which provides the core testing - framework. - * -lgtest_main: This links a pre-compiled main function provided by Google - Test, which saves you from writing your own main() to run the tests. - * -pthread: This links the POSIX threads library, which is required by Google - Test for its operation. - * -*/ \ No newline at end of file +TEST_F(SnakeEnhancementsTest, WrapAroundDown) { + pair current = {9,5}; + game.changeDirection('d'); + EXPECT_EQ(game.testNextHead(current), make_pair(0,5)); +}