diff --git a/main.cpp b/main.cpp index ef65093..496036f 100644 --- a/main.cpp +++ b/main.cpp @@ -1,9 +1,13 @@ #include "snake.h" +#include +#include -int main(int argc, char *argv[]) { +char direction = 'r'; + +int main(int argc, char *argv[]) +{ thread input_thread(input_handler); - thread game_thread(game_play); + 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..7769954 100644 --- a/snake.h +++ b/snake.h @@ -1,101 +1,279 @@ +#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'; +constexpr int BOARD_SIZE = 10; +extern char direction; + +// ------------------ Snake Class ------------------ +class Snake +{ +private: + deque> body; + +public: + Snake() + { + body.push_back({0, 0}); // start at (0,0) + } + + void grow(pair newHead) + { + body.push_back(newHead); + } + + void move(pair newHead) + { + body.push_back(newHead); + body.pop_front(); + } + + pair getHead() const + { + return body.back(); + } + + int getSize() const + { + return body.size(); + } + + const deque> &getBody() const + { + return body; + } +}; -void input_handler(){ - // change terminal settings +// ------------------ Input Handling ------------------ +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) { + while (true) + { char input = getchar(); - if (keymap.find(input) != keymap.end()) { - // This now correctly modifies the single, shared 'direction' variable + if (keymap.find(input) != keymap.end()) + { direction = keymap[input]; - }else if (input == 'q'){ + } + 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') + direction = 'P'; + else + direction = 'r'; + } } tcsetattr(STDIN_FILENO, TCSANOW, &oldt); } - -void render_game(int size, deque> &snake, pair food){ - for(size_t i=0;i food, + pair poison, int score) +{ + const auto &body = snake.getBody(); + for (int i = 0; i < size; ++i) + { + for (int j = 0; j < size; ++j) + { + if (i == food.first && j == food.second) cout << "🍎"; - }else if (find(snake.begin(), snake.end(), make_pair(int(i), int(j))) != snake.end()) { + else if (i == poison.first && j == poison.second) + cout << "☠️"; + else if (find(body.begin(), body.end(), make_pair(i, j)) != body.end()) cout << "🐍"; - }else{ + else cout << "⬜"; - } + } + cout << '\n'; } - cout << endl; + cout << "Length: " << snake.getSize() << " Score: " << score << "\n"; } + +// ------------------ Game Helpers ------------------ +inline pair get_next_head(pair current, char direction) +{ + if (direction == 'r') + return {current.first, (current.second + 1) % BOARD_SIZE}; + else if (direction == 'l') + return {current.first, current.second == 0 ? BOARD_SIZE - 1 : current.second - 1}; + else if (direction == 'd') + return {(current.first + 1) % BOARD_SIZE, current.second}; + else if (direction == 'u') + return {current.first == 0 ? BOARD_SIZE - 1 : current.first - 1, current.second}; + else if (direction == 'P') // <-- pause means no movement + return current; + else + return current; // safe fallback } -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 pair spawn_food(const deque> &body) +{ + vector> freeCells; + for (int i = 0; i < BOARD_SIZE; ++i) + for (int j = 0; j < BOARD_SIZE; ++j) + if (find(body.begin(), body.end(), make_pair(i, j)) == body.end()) + freeCells.push_back({i, j}); + + if (freeCells.empty()) + return {-1, -1}; + + return freeCells[rand() % freeCells.size()]; +} + +// ------------------ Score Management ------------------ +inline void save_score(int score) +{ + ofstream file("scores.txt", ios::app); + if (file.is_open()) { - 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; - + file << score << "\n"; + file.close(); + } } +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; +} -void game_play(){ +// ------------------ Game Loop ------------------ +inline void game_play() +{ system("clear"); - deque> snake; - snake.push_back(make_pair(0,0)); + Snake snake; + pair food = spawn_food(snake.getBody()); + pair poison = spawn_food(snake.getBody()); + + int score = 0, level = 1, baseDelay = 500; + + while (true) + { + auto currentHead = snake.getHead(); + auto nextHead = get_next_head(currentHead, direction); - 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()) { + + if (direction == 'P') + { + this_thread::sleep_for(chrono::milliseconds(100)); + continue; + } + + bool willGrow = (nextHead == food); + bool collision = false; + const auto &body = snake.getBody(); + + if (willGrow) + { + if (find(body.begin(), body.end(), nextHead) != body.end()) + collision = true; + } + else + { + auto itStart = body.begin(); + if (!body.empty()) + ++itStart; + if (find(itStart, body.end(), nextHead) != body.end()) + collision = true; + } + + if (collision) + { system("clear"); - cout << "Game Over" << endl; + cout << "Game Over\n"; + cout << "Final Length: " << snake.getSize() << " Final Score: " << score << "\n"; + save_score(score); + + cout << "\n=== Top 10 Scores ===\n"; + auto topScores = load_top_scores(); + for (int i = 0; i < topScores.size(); i++) + cout << (i + 1) << ". " << topScores[i] << "\n"; 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); + + if (nextHead == poison) + { + system("clear"); + cout << "Snake ate poison ☠️ Game Over!\n"; + cout << "Final Length: " << snake.getSize() << " Final Score: " << score << "\n"; + save_score(score); + + cout << "\n=== Top 10 Scores ===\n"; + auto topScores = load_top_scores(); + for (int i = 0; i < topScores.size(); i++) + cout << (i + 1) << ". " << topScores[i] << "\n"; + exit(0); + } + + if (willGrow) + { + snake.grow(nextHead); + score++; + food = spawn_food(snake.getBody()); + + if (score % 3 == 0) + poison = spawn_food(snake.getBody()); + + if (food.first == -1) + { + system("clear"); + cout << "You Win! Final Length: " << snake.getSize() << " Final Score: " << score << "\n"; + save_score(score); + + cout << "\n=== Top 10 Scores ===\n"; + auto topScores = load_top_scores(); + for (int i = 0; i < topScores.size(); i++) + cout << (i + 1) << ". " << topScores[i] << "\n"; + exit(0); + } + } + else + { + snake.move(nextHead); + } + + level = (score / 5) + 1; + render_game(BOARD_SIZE, snake, food, poison, score); + cout << "Level: " << level << "\n"; + + int delay_ms = max(50, baseDelay - (level - 1) * 50 - snake.getSize() * 5); + this_thread::sleep_for(chrono::milliseconds(delay_ms)); } } + +#endif \ No newline at end of file diff --git a/snake_test.cpp b/snake_test.cpp index 42f8561..bb6251b 100644 --- a/snake_test.cpp +++ b/snake_test.cpp @@ -1,33 +1,95 @@ -#include #include "snake.h" +#include +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)); - +// ---------------- Movement Tests ---------------- +TEST(SnakeTest, NextHeadRight) +{ + pair pos = {0, 0}; + auto next = get_next_head(pos, 'r'); + EXPECT_EQ(next, make_pair(0, 1)); } +TEST(SnakeTest, NextHeadWrapLeft) +{ + pair pos = {0, 0}; + auto next = get_next_head(pos, 'l'); + EXPECT_EQ(next, make_pair(0, BOARD_SIZE - 1)); +} -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(SnakeTest, NextHeadDown) +{ + pair pos = {0, 0}; + auto next = get_next_head(pos, 'd'); + EXPECT_EQ(next, make_pair(1, 0)); } -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(SnakeTest, NextHeadUpWrap) +{ + pair pos = {0, 0}; + auto next = get_next_head(pos, 'u'); + EXPECT_EQ(next, make_pair(BOARD_SIZE - 1, 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(SnakeTest, FoodNotInSnake) +{ + deque> snake = {{0, 0}, {0, 1}, {0, 2}}; + auto food = spawn_food(snake); + EXPECT_TRUE(find(snake.begin(), snake.end(), food) == snake.end()); } +// Poison food spawn & collision +TEST(SnakeTest, PoisonNotInSnake) +{ + deque> snake = {{0, 0}, {0, 1}}; + auto poison = spawn_food(snake); + EXPECT_TRUE(find(snake.begin(), snake.end(), poison) == snake.end()); +} +// play/pause functionality test +TEST(SnakeTest, PauseStopsMovement) +{ + deque> snake = {{0, 0}}; + char pauseDir = 'P'; + auto next = get_next_head(snake.back(), pauseDir); + EXPECT_EQ(next, snake.back()); +} +TEST(SnakeTest, SaveAndLoadScores) +{ + save_score(100); + save_score(50); + auto scores = load_top_scores(); + EXPECT_TRUE(find(scores.begin(), scores.end(), 100) != scores.end()); + EXPECT_TRUE(find(scores.begin(), scores.end(), 50) != scores.end()); +} +// ---------------- Difficulty Level Tests ---------------- +TEST(SnakeTest, DelayDecreasesWithLevel) +{ + int baseDelay = 500; + int level1 = max(50, baseDelay - (1 - 1) * 50 - 1 * 5); + int level5 = max(50, baseDelay - (5 - 1) * 50 - 10 * 5); -/** + EXPECT_LT(level5, level1); // delay should reduce at higher levels +} + +// ---------------- Snake Class Tests ---------------- +TEST(SnakeTest, SnakeGrowsOnFood) +{ + Snake s; + int oldSize = s.getSize(); + s.grow({0, 1}); // add a new head + EXPECT_EQ(s.getSize(), oldSize + 1); +} + +TEST(SnakeTest, SnakeMovesCorrectly) +{ + Snake s; + int oldSize = s.getSize(); + s.move({0, 1}); // head moves forward + EXPECT_EQ(s.getSize(), oldSize); // size remains same after move + EXPECT_EQ(s.getHead(), make_pair(0, 1)); +} +/** * g++ -o my_tests snake_test.cpp -lgtest -lgtest_main -pthread; * This command is a two-part shell command. Let's break it down. @@ -45,5 +107,5 @@ TEST(SnakeBehaviour, NextHeadDown) { 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