diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..cea4d3f --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,18 @@ +{ + "configurations": [ + { + "name": "windows-gcc-x64", + "includePath": [ + "${workspaceFolder}/**" + ], + "compilerPath": "gcc", + "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..7c0ffdb --- /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": "c:/Users/User/snake-cli", + "program": "c:/Users/User/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..c9e66f1 --- /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/2022/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..1177ac9 100644 --- a/main.cpp +++ b/main.cpp @@ -1,9 +1,13 @@ #include "snake.h" +#include -int main(int argc, char *argv[]) { - thread input_thread(input_handler); - thread game_thread(game_play); +int main() +{ + std::thread input_thread(input_handler); + + game_play(); + input_thread.join(); - game_thread.join(); -return 0; + + return 0; } \ No newline at end of file diff --git a/snake.h b/snake.h index ebe1192..fbd1180 100644 --- a/snake.h +++ b/snake.h @@ -1,101 +1,274 @@ +#ifndef SNAKE_H +#define SNAKE_H + #include #include #include #include #include #include -#include // for system clear +#include #include #include #include +#include +#include + using namespace std; using std::chrono::system_clock; using namespace std::this_thread; -char direction='r'; +inline char direction = 'r'; +constexpr int BOARD_SIZE = 10; -void input_handler(){ - // change terminal settings +inline void input_handler() +{ 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) { + + map keymap = {{'d', 'r'}, {'a', 'l'}, {'w', 'u'}, {'s', 'd'}}; + char lastValidDirection = 'r'; + + 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'){ + + if (keymap.count(input)) + { + char newDirection = keymap[input]; + if ((newDirection == 'r' && direction != 'l') || + (newDirection == 'l' && direction != 'r') || + (newDirection == 'u' && direction != 'd') || + (newDirection == 'd' && direction != 'u') || + direction == 'P') + { + direction = newDirection; + lastValidDirection = newDirection; + } + } + else if (input == 'q') + { + tcsetattr(STDIN_FILENO, TCSANOW, &oldt); exit(0); } - // You could add an exit condition here, e.g., if (input == 'q') break; + else if (input == 'p') + { + if (direction != 'P') + { + lastValidDirection = direction; + direction = 'P'; + } + else + { + direction = lastValidDirection; + } + } } tcsetattr(STDIN_FILENO, TCSANOW, &oldt); } +inline pair get_next_head(pair current, char direction) +{ + int next_r = current.first; + int next_c = current.second; -void render_game(int size, deque> &snake, pair food){ - for(size_t i=0;i spawn_item(const deque> &snake) +{ + vector> freeCells; + for (int i = 0; i < BOARD_SIZE; ++i) + for (int j = 0; j < BOARD_SIZE; ++j) + if (find(snake.begin(), snake.end(), make_pair(i, j)) == snake.end()) + freeCells.push_back({i, j}); + + if (freeCells.empty()) + return {-1, -1}; + + return freeCells[rand() % freeCells.size()]; +} + +inline void save_score(int score) +{ + ofstream file("scores.txt", ios::app); + if (file.is_open()) + { + file << score << "\n"; + file.close(); + } + else + { + cerr << "Warning: Could not open scores.txt for saving.\n"; } - cout << endl; } + +inline vector load_top_scores() +{ + ifstream file("scores.txt"); + vector scores; + int s; + while (file >> s) + scores.push_back(s); + file.close(); + + sort(scores.begin(), scores.end(), greater()); + + if (scores.size() > 10) + scores.resize(10); + + return scores; } -pair 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') +inline void render_game(int size, deque> &snake, pair food, + pair poison, int score, int level) +{ + cout << "\033[H"; + + for (int k = 0; k < size + 2; ++k) cout << "🧱"; + cout << "\n"; + + for (int i = 0; i < size; ++i) { - 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); + cout << "🧱"; + for (int j = 0; j < size; ++j) + { + pair current_cell = {i, j}; + if (current_cell == food) + cout << "🍎"; + else if (current_cell == poison) + cout << "☠️"; + else if (find(snake.begin(), snake.end(), current_cell) != snake.end()) + { + if (current_cell == snake.back()) + cout << "🟢"; + else + cout << "🟩"; + } + else + cout << " "; } - return next; - -} + cout << "🧱\n"; + } + for (int k = 0; k < size + 2; ++k) cout << "🧱"; + cout << "\n\n"; + cout << "🎮 Score: " << score << " | Length: " << snake.size() << " | Speed Level: " << level << "\n"; + cout << "Controls: (W, A, S, D) | Pause: (P) | Quit: (Q)\n"; +} -void game_play(){ +inline void game_play() +{ system("clear"); + srand(time(0)); + 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()) { + snake.push_back({0, 0}); + snake.push_back({0, 1}); + + pair food = spawn_item(snake); + pair poison = spawn_item(snake); + + int score = 0; + int level = 1; + const int baseDelay = 300; + + while (true) + { + if (direction == 'P') + { + render_game(BOARD_SIZE, snake, food, poison, score, level); + cout << "\n\n\t\t\t\t\t\tPAUSED (Press P to resume)"; + this_thread::sleep_for(chrono::milliseconds(100)); + continue; + } + + pair currentHead = snake.back(); + pair nextHead = get_next_head(currentHead, direction); + + bool willGrow = (nextHead == food); + bool collision = false; + + if (willGrow) + { + if (find(snake.begin(), snake.end(), nextHead) != snake.end()) + collision = true; + } + else + { + if (snake.size() > 1 && find(snake.begin() + 1, snake.end(), nextHead) != snake.end()) + collision = true; + } + + if (collision) + { 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); + cout << "Game Over! 💥 Self-collision!\n"; + break; + } + + if (nextHead == poison) + { + system("clear"); + cout << "Game Over! ☠️ Snake ate poison!\n"; + break; + } + + snake.push_back(nextHead); + + if (willGrow) + { + score++; + level = (score / 5) + 1; + + food = spawn_item(snake); + + if (score % 3 == 0) + poison = spawn_item(snake); + else if (poison.first == -1) + poison = spawn_item(snake); + + if (food.first == -1) + { + system("clear"); + cout << "You Win! 🎉 Board Full!\n"; + break; + } + } + else + { snake.pop_front(); } - render_game(10, snake, food); - cout << "length of snake: " << snake.size() << endl; - - sleep_for(500ms); + + render_game(BOARD_SIZE, snake, food, poison, score, level); + + int delay_ms = max(50, baseDelay - (level - 1) * 30 - (int)snake.size() * 5); + this_thread::sleep_for(chrono::milliseconds(delay_ms)); } + + cout << "Final Length: " << snake.size() << " | Final Score: " << score << "\n"; + save_score(score); + + cout << "\n=== Top 10 Scores ===\n"; + vector topScores = load_top_scores(); + for (int i = 0; i < topScores.size(); i++) + cout << " " << (i + 1) << ". " << topScores[i] << "\n"; + + this_thread::sleep_for(chrono::seconds(3)); + exit(0); } + +#endif // SNAKE_H diff --git a/snake_test.cpp b/snake_test.cpp index 42f8561..a3a5168 100644 --- a/snake_test.cpp +++ b/snake_test.cpp @@ -1,49 +1,72 @@ #include #include "snake.h" +// The global 'direction' variable must be defined for the linker, +// even though get_next_head doesn't use it directly in its logic. +char direction = 'r'; -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)); - -} - +// --- Next Head Calculation Tests --- -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 non-wrapping movement to the right +TEST(NextHeadTest, NextHeadRightNonWrap) +{ + pair pos = {5, 5}; + auto next = get_next_head(pos, 'r'); + EXPECT_EQ(next, make_pair(5, 6)); } -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 wrapping movement to the right (from BOARD_SIZE-1 to 0) +TEST(NextHeadTest, NextHeadRightWrap) +{ + pair pos = {0, BOARD_SIZE - 1}; // e.g., (0, 9) + auto next = get_next_head(pos, 'r'); + EXPECT_EQ(next, make_pair(0, 0)); } -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)); - +// Test non-wrapping movement to the left +TEST(NextHeadTest, NextHeadLeftNonWrap) +{ + pair pos = {5, 5}; + auto next = get_next_head(pos, 'l'); + EXPECT_EQ(next, make_pair(5, 4)); } +// Test wrapping movement to the left (from 0 to BOARD_SIZE-1) +TEST(NextHeadTest, NextHeadLeftWrap) +{ + pair pos = {0, 0}; + auto next = get_next_head(pos, 'l'); + EXPECT_EQ(next, make_pair(0, BOARD_SIZE - 1)); // e.g., (0, 9) +} -/** - * g++ -o my_tests snake_test.cpp -lgtest -lgtest_main -pthread; - * This command is a two-part shell command. Let's break it down. +// Test non-wrapping movement down +TEST(NextHeadTest, NextHeadDownNonWrap) +{ + pair pos = {5, 5}; + auto next = get_next_head(pos, 'd'); + EXPECT_EQ(next, make_pair(6, 5)); +} - The first part is the compilation: - g++ -o my_tests hello_gtest.cpp -lgtest -lgtest_main -pthread +// Test wrapping movement down (from BOARD_SIZE-1 to 0) +TEST(NextHeadTest, NextHeadDownWrap) +{ + pair pos = {BOARD_SIZE - 1, 0}; // e.g., (9, 0) + auto next = get_next_head(pos, 'd'); + EXPECT_EQ(next, make_pair(0, 0)); +} +// Test non-wrapping movement up +TEST(NextHeadTest, NextHeadUpNonWrap) +{ + pair pos = {5, 5}; + auto next = get_next_head(pos, 'u'); + EXPECT_EQ(next, make_pair(4, 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 wrapping movement up (from 0 to BOARD_SIZE-1) +TEST(NextHeadTest, NextHeadUpWrap) +{ + pair pos = {0, 0}; + auto next = get_next_head(pos, 'u'); + EXPECT_EQ(next, make_pair(BOARD_SIZE - 1, 0)); // e.g., (9, 0) +} \ No newline at end of file