From 28c38e8a1d52f86c39fbf03c388c7bb16f6a8c16 Mon Sep 17 00:00:00 2001 From: Shenal Parmar <169513116+shenal-parmar@users.noreply.github.com> Date: Fri, 3 Oct 2025 20:03:59 +0530 Subject: [PATCH] snake-game all changes --- .vscode/c_cpp_properties.json | 18 +++ .vscode/launch.json | 24 +++ .vscode/settings.json | 59 ++++++++ highscores.txt | 1 + main.cpp | 48 +++++- snake.h | 274 ++++++++++++++++++++++++---------- snake_test.cpp | 57 ++----- 7 files changed, 350 insertions(+), 131 deletions(-) create mode 100644 .vscode/c_cpp_properties.json create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 highscores.txt diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..a3d4300 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,18 @@ +{ + "configurations": [ + { + "name": "windows-gcc-x64", + "includePath": [ + "${workspaceFolder}/**" + ], + "compilerPath": "C:/msys64/ucrt64/bin/gcc.exe", + "cStandard": "${default}", + "cppStandard": "${default}", + "intelliSenseMode": "windows-gcc-x64", + "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..e0e33e4 --- /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": "f:/snake_game/snake-cli", + "program": "f:/snake_game/snake-cli/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/highscores.txt b/highscores.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/highscores.txt @@ -0,0 +1 @@ +0 diff --git a/main.cpp b/main.cpp index ef65093..f57434d 100644 --- a/main.cpp +++ b/main.cpp @@ -1,9 +1,43 @@ #include "snake.h" +#include +#include +#include -int main(int argc, char *argv[]) { - thread input_thread(input_handler); - thread game_thread(game_play); - input_thread.join(); - game_thread.join(); -return 0; -} \ No newline at end of file +void enableANSI() +{ + HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); + DWORD inMode = 0; + GetConsoleMode(hIn, &inMode); + inMode |= ENABLE_PROCESSED_INPUT; + SetConsoleMode(hIn, inMode); + + // Get console output handle + HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); + if (hOut == INVALID_HANDLE_VALUE) + return; + + DWORD dwMode = 0; + if (!GetConsoleMode(hOut, &dwMode)) + return; + + // Enable ANSI escape codes + dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + SetConsoleMode(hOut, dwMode); +} + +int main() +{ + enableANSI(); + + std::atomic running(true); + + Game game; + game.init(); + + std::thread inputThread(input_handler, std::ref(running)); + std::thread gameThread(&Game::run, &game, std::ref(running)); + + inputThread.join(); + gameThread.join(); + return 0; +} diff --git a/snake.h b/snake.h index ebe1192..50685c4 100644 --- a/snake.h +++ b/snake.h @@ -1,101 +1,209 @@ +#pragma once #include #include -#include -#include -#include -#include -#include // for system clear -#include #include #include -using namespace std; -using std::chrono::system_clock; -using namespace std::this_thread; -char direction='r'; - - -void input_handler(){ - // change terminal settings - 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'}}; - 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'){ - exit(0); - } - // You could add an exit condition here, e.g., if (input == 'q') break; +#include +#include +#include +#include +#include +#include + +// === Console Colors === +const std::string GREEN_BG = "\033[42m"; // food +const std::string RED_BG = "\033[41m"; // poison +const std::string YELLOW_BG = "\033[43m"; // snake +const std::string BLUE_BG = "\033[44m"; // background +const std::string RESET = "\033[0m"; + +const int BOARD_SIZE = 15; + +std::atomic direction('r'); +std::atomic paused(false); + +// ================= SCORE MANAGER ================= +class ScoreManager { + std::vector scores; + +public: + void load(const std::string& file = "highscores.txt") { + scores.clear(); + std::ifstream in(file); + int s; + while (in >> s) scores.push_back(s); + sort(scores.rbegin(), scores.rend()); + if (scores.size() > 10) scores.resize(10); + } + + void save(const std::string& file = "highscores.txt") { + sort(scores.rbegin(), scores.rend()); + if (scores.size() > 10) scores.resize(10); + std::ofstream out(file); + for (int s : scores) out << s << "\n"; + } + + void add(int score) { + scores.push_back(score); + save(); + } + + void display() { + std::cout << "Top 10 Highest Scores:\n"; + for (size_t i = 0; i < scores.size(); i++) + std::cout << (i + 1) << ". " << scores[i] << "\n"; + } + + const std::vector& getScores() const { return scores; } +}; + +// ================= SNAKE ================= +class Snake { + std::deque> body; + +public: + Snake() { body.push_back({0,0}); } + + const std::deque>& getBody() const { return body; } + std::pair head() const { return body.back(); } + + void grow(std::pair h) { body.push_back(h); } + void move(std::pair h) { + body.push_back(h); + body.pop_front(); + } + + bool collides(std::pair pos) const { + return std::find(body.begin(), body.end(), pos) != body.end(); + } +}; + +// ================= GAME ================= +class Game { + Snake snake; + ScoreManager scoreManager; + std::pair food, poison; + int score = 0; + +public: + Game() { srand((unsigned)time(NULL)); } + + void init() { + scoreManager.load(); + spawnFood(); + spawnPoison(); + } + + std::pair randomCell() { + return { rand() % BOARD_SIZE, rand() % BOARD_SIZE }; + } + + void spawnFood() { + do { food = randomCell(); } + while (snake.collides(food) || food == poison); + } + + void spawnPoison() { + do { poison = randomCell(); } + while (snake.collides(poison) || poison == food); } - tcsetattr(STDIN_FILENO, TCSANOW, &oldt); -} + std::pair getNextHead(std::pair current, char dir) { + if (dir == 'r') return {current.first, (current.second + 1) % BOARD_SIZE}; + if (dir == 'l') return {current.first, current.second == 0 ? BOARD_SIZE-1 : current.second-1}; + if (dir == 'd') return {(current.first+1) % BOARD_SIZE, current.second}; + return {current.first == 0 ? BOARD_SIZE-1 : current.first-1, current.second}; // 'u' + } + + void update(char dir, bool& game_over) { + auto head = getNextHead(snake.head(), dir); + + if (snake.collides(head) || head == poison) { + game_over = true; + scoreManager.add(score); + return; + } + + if (head == food) { + snake.grow(head); + score += 10; + spawnFood(); + spawnPoison(); + } else { + snake.move(head); + } + } -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); + void run(std::atomic& running) { + bool game_over = false; + while (running) { + update(direction, game_over); + if (game_over) break; + + render(); + + while (paused) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + if (!running) break; } - return next; - + if (!running) break; + + int sleep_ms = 400 - score*5; + if (sleep_ms < 100) sleep_ms = 100; + std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms)); + } + + // ✅ Always save score whether quit or crash + if (!game_over) { + // quit with ESC + scoreManager.add(score); + } + + std::cout << "\033[2J\033[H"; + std::cout << (game_over ? "==== GAME OVER ====\n" : "==== GAME QUIT ====\n"); + std::cout << "Your Score: " << score << "\n\n"; + scoreManager.display(); + running = false; } +}; +// ================= INPUT HANDLER ================= +inline void input_handler(std::atomic& running) { + while (running) { + char ch = _getch(); // blocking, always reads every key -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(); + // Arrow keys (224) or function keys (0) + if (ch == 224 || ch == 0) { + ch = _getch(); + switch (ch) { + case 72: if (direction != 'd') direction = 'u'; break; + case 80: if (direction != 'u') direction = 'd'; break; + case 75: if (direction != 'r') direction = 'l'; break; + case 77: if (direction != 'l') direction = 'r'; break; + } + } + else { + if (ch == 27) running = false; // ESC + else if (ch == ' ') paused = !paused.load(); // SPACE + else if (ch == 'w' || ch == 'W') if (direction != 'd') direction = 'u'; + else if (ch == 's' || ch == 'S') if (direction != 'u') direction = 'd'; + else if (ch == 'a' || ch == 'A') if (direction != 'r') direction = 'l'; + else if (ch == 'd' || ch == 'D') if (direction != 'l') direction = 'r'; } - 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..f764fc0 100644 --- a/snake_test.cpp +++ b/snake_test.cpp @@ -1,49 +1,24 @@ #include #include "snake.h" - -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)); - +TEST(SnakeTest, MovementCollision) { + Snake s; + auto h = s.head(); + EXPECT_FALSE(s.collides({h.first+1,h.second})); + s.grow({1,0}); + EXPECT_TRUE(s.collides({1,0})); } - -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(GameTest, NextHeadWrapAround) { + Game g; + EXPECT_EQ(g.getNextHead({5,14},'r'), std::make_pair(5,0)); + EXPECT_EQ(g.getNextHead({0,5},'u'), std::make_pair(14,5)); } -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)); +TEST(ScoreManagerTest, Top10Scores) { + ScoreManager sm; + for(int i=1;i<=15;i++) sm.add(i*10); + sm.load(); + EXPECT_LE(sm.getScores().size(), 10); + EXPECT_GE(sm.getScores()[0], sm.getScores()[9]); } - -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)); - -} - - -/** - * g++ -o my_tests snake_test.cpp -lgtest -lgtest_main -pthread; - * This command is a two-part shell command. Let's break it down. - - The first part is the compilation: - g++ -o my_tests hello_gtest.cpp -lgtest -lgtest_main -pthread - - - * 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