diff --git a/main.cpp b/main.cpp index ef65093..b9a4658 100644 --- a/main.cpp +++ b/main.cpp @@ -1,9 +1,111 @@ #include "snake.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +static void render_game(int size, const std::deque &snake, + const SnakeGame::Pos &food, const SnakeGame::Pos &poison) { + // Move cursor to top-left and draw + std::cout << "\033[H"; + for (int i = 0; i < size; ++i) { + for (int j = 0; j < size; ++j) { + if (i == food.first && j == food.second) { + std::cout << "🍎"; + } else if (i == poison.first && j == poison.second) { + std::cout << "☠️"; + } else if (std::find(snake.begin(), snake.end(), std::make_pair(i,j)) != snake.end()) { + std::cout << "🐍"; + } else { + std::cout << "⬜"; + } + } + std::cout << "\n"; + } +} 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 + SnakeGame game(10); + std::atomic direction('r'); + std::atomic paused(false); + std::atomic stopFlag(false); + + // Input thread: non-blocking read from stdin (raw mode) + std::thread input_thread([&](){ + struct termios oldt, newt; + tcgetattr(STDIN_FILENO, &oldt); + newt = oldt; + newt.c_lflag &= ~(ICANON | ECHO); + tcsetattr(STDIN_FILENO, TCSANOW, &newt); + + // make stdin non-blocking + int oldf = fcntl(STDIN_FILENO, F_GETFL, 0); + fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK); + + std::map keymap = { {'d','r'}, {'a','l'}, {'w','u'}, {'s','d'} }; + + while (!stopFlag.load()) { + int ch = getchar(); + if (ch == EOF) { + std::this_thread::sleep_for(10ms); + continue; + } + char c = static_cast(ch); + if (keymap.find(c) != keymap.end()) { + direction.store(keymap[c]); + } else if (c == 'p') { + paused = !paused.load(); + } else if (c == 'q') { + stopFlag = true; + break; + } + } + + // restore flags and termios + fcntl(STDIN_FILENO, F_SETFL, oldf); + tcsetattr(STDIN_FILENO, TCSANOW, &oldt); + }); + + // clear screen once + std::cout << "\033[2J\033[H"; + + // main game loop + while (!stopFlag.load()) { + if (paused.load()) { + std::cout << "Game Paused - press 'p' to resume\n"; + std::this_thread::sleep_for(200ms); + continue; + } + + auto status = game.step(direction.load()); + + if (status == SnakeGame::Status::SelfCollision) { + std::cout << "Game Over! You ate yourself 🐍\n"; + std::cout << "Final Score: " << game.getScore() << "\n"; + game.updateHighScores("highscores.txt"); + break; + } else if (status == SnakeGame::Status::AtePoison) { + std::cout << "Game Over! You ate poison ☠️\n"; + std::cout << "Final Score: " << game.getScore() << "\n"; + game.updateHighScores("highscores.txt"); + break; + } + + render_game(game.getGridSize(), game.getSnake(), game.getFood(), game.getPoison()); + std::cout << "Score: " << game.getScore() << " Length: " << game.getSnake().size() + << " Speed: " << game.getSpeed() << "ms\n"; + + std::this_thread::sleep_for(std::chrono::milliseconds(game.getSpeed())); + } + + // make sure input thread finishes + if (input_thread.joinable()) input_thread.join(); + + return 0; +} diff --git a/snake.h b/snake.h index ebe1192..f03e910 100644 --- a/snake.h +++ b/snake.h @@ -1,101 +1,148 @@ -#include -#include -#include -#include -#include -#include -#include // for system clear -#include +#ifndef SNAKE_H +#define SNAKE_H + #include +#include +#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); +#include +#include + +/* + Header-only refactor of the game logic into SnakeGame. + All methods defined inline so there's no separate .cpp file required. +*/ + +class SnakeGame { +public: + using Pos = std::pair; + enum class Status { OK, AteFood, AtePoison, SelfCollision }; + + // Construct with grid size and optional RNG seed (useful for deterministic tests) + explicit SnakeGame(int gridSize = 10, unsigned rngSeed = std::random_device{}()) + : gridSize_(gridSize), + snake_(), + food_(-1, -1), + poison_(-1, -1), + score_(0), + foodEaten_(0), + speed_(500), + rng_(rngSeed), + dist_(0, gridSize - 1) + { + snake_.push_back({0,0}); + food_ = generateUniqueFood(snake_, {-1,-1}); + poison_ = generateUniqueFood(snake_, food_); + } + + // Compute next head with wrap-around + Pos getNextHead(const Pos ¤t, char direction) const { + int r = current.first; + int c = current.second; + if (direction == 'r') { + c = (c + 1) % gridSize_; + } else if (direction == 'l') { + c = (c + gridSize_ - 1) % gridSize_; + } else if (direction == 'd') { + r = (r + 1) % gridSize_; + } else if (direction == 'u') { + r = (r + gridSize_ - 1) % gridSize_; } - // You could add an exit condition here, e.g., if (input == 'q') break; + return {r, c}; } - tcsetattr(STDIN_FILENO, TCSANOW, &oldt); -} - - -void render_game(int size, deque> &snake, pair food){ - for(size_t i=0;i &snake, const Pos &other) { + return generateUniqueFood(snake, other); } - cout << endl; -} -} - -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') - { - 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); + + // Single simulation step; returns status + Status step(char direction) { + Pos currentHead = snake_.back(); + Pos next = getNextHead(currentHead, direction); + + // Self collision? + if (std::find(snake_.begin(), snake_.end(), next) != snake_.end()) { + return Status::SelfCollision; } - 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(); + + if (next == food_) { + // grow + snake_.push_back(next); + score_ += 10; + ++foodEaten_; + // regenerate food and ensure poison doesn't collide + food_ = generateUniqueFood(snake_, poison_); + poison_ = generateUniqueFood(snake_, food_); + if (foodEaten_ % 10 == 0 && speed_ > 100) speed_ -= 50; + return Status::AteFood; + } else if (next == poison_) { + return Status::AtePoison; + } else { + // normal move + snake_.push_back(next); + snake_.pop_front(); + return Status::OK; } - render_game(10, snake, food); - cout << "length of snake: " << snake.size() << endl; - - sleep_for(500ms); } -} + + // Accessors + const std::deque& getSnake() const { return snake_; } + Pos getFood() const { return food_; } + Pos getPoison() const { return poison_; } + int getScore() const { return score_; } + int getSpeed() const { return speed_; } + int getGridSize() const { return gridSize_; } + + // Update highscores file and return top scores (descending) + std::vector updateHighScores(const std::string &path) const { + std::vector scores; + std::ifstream infile(path); + int s; + while (infile >> s) scores.push_back(s); + infile.close(); + + scores.push_back(score_); + std::sort(scores.begin(), scores.end(), std::greater()); + if (scores.size() > 10) scores.resize(10); + + std::ofstream outfile(path); + for (int sc : scores) outfile << sc << "\n"; + outfile.close(); + + return scores; + } + + // Test helpers (to set internal state deterministically in tests) + void setFood(const Pos &p) { food_ = p; } + void setPoison(const Pos &p) { poison_ = p; } + void setSnake(const std::deque &s) { snake_ = s; } + void setScore(int sc) { score_ = sc; } + void setSpeed(int sp) { speed_ = sp; } + +private: + int gridSize_; + std::deque snake_; + Pos food_; + Pos poison_; + int score_; + int foodEaten_; + int speed_; + + // RNG for food/poison generation + mutable std::mt19937 rng_; + std::uniform_int_distribution dist_; + + // internal helper to pick a free cell + Pos generateUniqueFood(const std::deque &snake, const Pos &other) const { + // If grid is full, this would loop indefinitely; tests will avoid that case. + Pos candidate; + do { + candidate = { dist_(rng_), dist_(rng_) }; + } while (std::find(snake.begin(), snake.end(), candidate) != snake.end() || candidate == other); + return candidate; + } +}; + +#endif // SNAKE_H diff --git a/snake_test.cpp b/snake_test.cpp index 42f8561..6685e72 100644 --- a/snake_test.cpp +++ b/snake_test.cpp @@ -1,49 +1,97 @@ #include #include "snake.h" +#include +#include +using Pos = SnakeGame::Pos; +using Status = SnakeGame::Status; -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(NextHead, MovesAndWrapsRightLeftUpDown) { + SnakeGame g(10, 1); + EXPECT_EQ(g.getNextHead(Pos{3,3}, 'r'), Pos{3,4}); + EXPECT_EQ(g.getNextHead(Pos{3,9}, 'r'), Pos{3,0}); + EXPECT_EQ(g.getNextHead(Pos{0,0}, 'l'), Pos{0,9}); + EXPECT_EQ(g.getNextHead(Pos{0,0}, 'u'), Pos{9,0}); + EXPECT_EQ(g.getNextHead(Pos{9,9}, 'd'), Pos{0,9}); } - -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(GenerateFood, AvoidsSnakeAndOther) { + // 3x3 grid, fill all except (2,2) + SnakeGame g(3, 42); + std::deque snake; + for (int r=0;r<3;r++){ + for (int c=0;c<3;c++){ + if (r == 2 && c == 2) continue; + snake.push_back({r,c}); + } + } + Pos other = {-1,-1}; + Pos food = g.generateFood(snake, other); + EXPECT_EQ(food, Pos{2,2}); } -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(Step, EatFoodGrowsAndIncreasesScore) { + SnakeGame g(4, 77); + std::deque snake = {{0,0}}; + g.setSnake(snake); + g.setFood({0,1}); + auto res = g.step('r'); + EXPECT_EQ(res, Status::AteFood); + EXPECT_EQ(g.getScore(), 10); + EXPECT_EQ(g.getSnake().back(), Pos{0,1}); + EXPECT_EQ(g.getSnake().size(), 2); } -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(Step, EatPoisonEndsGame) { + SnakeGame g(4, 77); + std::deque snake = {{0,0}}; + g.setSnake(snake); + g.setPoison({0,1}); + auto res = g.step('r'); + EXPECT_EQ(res, Status::AtePoison); } +TEST(Step, SelfCollisionDetected) { + SnakeGame g(4, 99); + // snake positions such that head at (1,1) and moving left collides with (1,0) + std::deque snake = {{0,1},{0,0},{1,0},{1,1}}; + g.setSnake(snake); + auto res = g.step('l'); + EXPECT_EQ(res, Status::SelfCollision); +} -/** - * 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 - +TEST(Step, SpeedDecreasesAfterTenFood) { + SnakeGame g(5, 123); + // Prepare snake so we can repeatedly set food directly and step + g.setSnake({{0,0}}); + g.setSpeed(500); + for (int i = 0; i < 9; ++i) { + // place food at next + Pos next = g.getNextHead(g.getSnake().back(), 'r'); + g.setFood(next); + EXPECT_EQ(g.step('r'), Status::AteFood); + // put snake back so next iteration can reuse pattern + } + // 10th eat: speed should reduce by 50 (from 500 to 450) + // place food at next + Pos next = g.getNextHead(g.getSnake().back(), 'r'); + g.setFood(next); + EXPECT_EQ(g.step('r'), Status::AteFood); + EXPECT_EQ(g.getSpeed(), 450); +} - * 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(UpdateHighScores, WritesTopSorted) { + SnakeGame g(4, 111); + std::string path = "tmp_highscores_test.txt"; + { + std::ofstream f(path); + f << 100 << "\n" << 50 << "\n" << 200 << "\n"; + } + g.setScore(150); + auto top = g.updateHighScores(path); + ASSERT_FALSE(top.empty()); + // top should be sorted descending and include new score 150 + EXPECT_TRUE(std::is_sorted(top.begin(), top.end(), std::greater())); + EXPECT_TRUE(std::find(top.begin(), top.end(), 150) != top.end()); + std::remove(path.c_str()); +}