#include "terminal-game-utils.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include struct termios orig_termios; class Snake { private: struct mSnakeUnit { int x; int y; std::string appearance; }; struct mCell { int x; int y; bool operator==(const mCell &other) const { return (x == other.x) && (y == other.y); } }; const std::map HEAD_ARROWS = { {'l', "◁"}, {'d', "▽"}, {'u', "△"}, {'r', "▷"}}; const int GRACE_FRAMES = 3; const int BOARD_OFFSET_X = 2; const int BOARD_OFFSET_Y = 1; const int WIDTH, HEIGHT, STARTING_DELAY, MAX_SPEED; const double ACCELERATION = 0; mSnakeUnit mLastDequeued; mSnakeUnit mApple; std::deque mSnake; std::queue mKeyQueue; std::vector mUnoccupied; std::vector mOccupied; int mGraceFrames = GRACE_FRAMES; char mSnakeDirection = 'r'; int mApplesAte = 0; bool mRunning = true; std::string mRepeat(int times, std::string str) { std::string returned; for (int i = 0; i < times; i++) returned += str; return returned; } std::uint8_t mGetDirectionByte(mSnakeUnit pos, mSnakeUnit goal) { if (pos.x > goal.x) return 0b0001; if (pos.x < goal.x) return 0b1000; if (pos.y > goal.y) return 0b0100; if (pos.y < goal.y) return 0b0010; return 0b0000; } mSnakeUnit mPopSnake() { mSnakeUnit tr = mSnake.front(); mCell val(tr.x, tr.y); mUnoccupy(val); mSnake.pop_front(); return tr; } void mPushSnake(mSnakeUnit input) { mCell val(input.x, input.y); mOccupy(val); mSnake.push_back(input); } void mOccupy(int x, int y) { mCell val(x, y); std::erase(mUnoccupied, val); mOccupied.push_back(val); } void mOccupy(mCell val) { std::erase(mUnoccupied, val); mOccupied.push_back(val); } void mUnoccupy(int x, int y) { mCell val(x, y); std::erase(mOccupied, val); mUnoccupied.push_back(val); } void mUnoccupy(mCell val) { std::erase(mOccupied, val); mUnoccupied.push_back(val); } std::string mGetTurningCharacter(mSnakeUnit Before, mSnakeUnit Current, mSnakeUnit After) { std::uint8_t BeforeDirection = mGetDirectionByte(Current, Before); std::uint8_t AfterDirection = mGetDirectionByte(Current, After); std::uint8_t CharacterBytes = BeforeDirection + AfterDirection; if (CharacterBytes == 0b1001) return "═"; if (CharacterBytes == 0b0110) return "║"; if (CharacterBytes == 0b1010) return "╔"; if (CharacterBytes == 0b0011) return "╗"; if (CharacterBytes == 0b1100) return "╚"; if (CharacterBytes == 0b0101) return "╝"; return ""; } void mDrawBoardOutlines() { tgu::MoveCursor(0, 0); tgu::ClearTerminal(); tgu::ClearTerminal(); tgu::MoveCursor(0, 0); std::string horizontalLine = mRepeat(WIDTH * 2 + 1, "═"); std::string horizontalSpaces = std::string(WIDTH * 2 - 1, ' '); std::cout << "╔" << horizontalLine + "╗\n\r"; for (int i = 0; i < HEIGHT; i++) { std::cout << "║ " + horizontalSpaces << " ║\n\r"; } std::cout << "╚" << horizontalLine + "╝"; } char mFlipDirection(char c) { if (c == 'l') return 'r'; else if (c == 'd') return 'u'; else if (c == 'u') return 'd'; else if (c == 'r') return 'l'; else return ' '; } void mSetSnakeDirection() { tgu::GameKey popped = mKeyQueue.front(); mKeyQueue.pop(); char PotentialSnakeDirection; if (popped == tgu::GameKey::W || popped == tgu::GameKey::UPARROW) PotentialSnakeDirection = 'u'; else if (popped == tgu::GameKey::A || popped == tgu::GameKey::LEFTARROW) PotentialSnakeDirection = 'l'; else if (popped == tgu::GameKey::S || popped == tgu::GameKey::DOWNARROW) PotentialSnakeDirection = 'd'; else if (popped == tgu::GameKey::D || popped == tgu::GameKey::RIGHTARROW) { PotentialSnakeDirection = 'r'; } else if (popped == tgu::GameKey::Q) { tgu::ClearTerminal(); tgu::MoveCursor(0, 0); exit(0); } else return; if (PotentialSnakeDirection == mFlipDirection(mSnakeDirection)) return; else mSnakeDirection = PotentialSnakeDirection; } std::tuple mGetDirectionOffset(char c) { tgu::MoveCursor(30, 30); if (c == 'l') return std::tuple(-1, 0); if (c == 'd') return std::tuple(0, 1); if (c == 'u') return std::tuple(0, -1); if (c == 'r') { return std::tuple(1, 0); } std::cout << "Some ting wong. Direction is " << c; return std::tuple(0, 0); } void mDie() { std::cout << "Die about it"; tgu::ClearTerminal(); tgu::MoveCursor(0, 0); exit(0); } bool mProcessApple(int &PotentialX, int &PotentialY) { if (mApple.x == PotentialX && mApple.y == PotentialY) { mAddApple(); return true; } else { return false; } } void mAdvanceSnake(int &PotentialX, int &PotentialY) { mPushSnake({PotentialX, PotentialY, HEAD_ARROWS.at(mSnakeDirection)}); mSnake[mSnake.size() - 2].appearance = mGetTurningCharacter( mSnake[mSnake.size() - 3], mSnake[mSnake.size() - 2], mSnake[mSnake.size() - 1]); } bool IsInSnake(int x, int y) { for (mSnakeUnit &u : mSnake) { if (u.x == x && u.y == y) return true; } return false; } bool mCheckDeathTick(int &PotentialX, int &PotentialY) { if (PotentialX <= -1 || PotentialY <= -1 || PotentialX >= WIDTH || PotentialY >= HEIGHT || IsInSnake(PotentialX, PotentialY)) { if (mGraceFrames <= 0) { mRunning = false; mDie(); } else { mGraceFrames = 0; } return true; } else { return false; } } void mSnakeStep() { std::tuple Offset = mGetDirectionOffset(mSnakeDirection); int PotentialX = mSnake.back().x + std::get<0>(Offset); int PotentialY = mSnake.back().y + std::get<1>(Offset); bool AteApple = mProcessApple(PotentialX, PotentialY); bool DeathTicked = mCheckDeathTick(PotentialX, PotentialY); if (!DeathTicked) { mGraceFrames = mGraceFrames + 1 >= GRACE_FRAMES ? GRACE_FRAMES : mGraceFrames + 1; mAdvanceSnake(PotentialX, PotentialY); if (!AteApple) { mLastDequeued = mPopSnake(); } } else { mSnake.back().appearance = HEAD_ARROWS.at(mSnakeDirection); mLastDequeued = mSnakeUnit(-1, -1, " "); } } void mGameTick() { if (mKeyQueue.size() != 0) { mSetSnakeDirection(); } mSnakeStep(); } void mInputLoop() { tgu::GameKey key; while (mRunning) { key = tgu::GetKeyPress(); if (mKeyQueue.size() <= 2) mKeyQueue.push(key); } } bool mHorizontalConnectors(mSnakeUnit a, mSnakeUnit b) { std::string horizontals = "╔╗╚╝◁▷"; if (a.y != b.y) return false; if (horizontals.find(a.appearance) != std::string::npos && horizontals.find(b.appearance) != std::string::npos) return true; return false; } void mDrawSnake() { tgu::TextRGB(0, 255, 0); if (mLastDequeued.x != -1) { tgu::MoveCursor(mLastDequeued.y + BOARD_OFFSET_Y, (mLastDequeued.x * 2) + BOARD_OFFSET_X - 1); std::cout << " "; } for (int i = 0; i < mSnake.size(); i++) { mSnakeUnit &s = mSnake[i]; if (s.appearance == "═") { tgu::MoveCursor(s.y + BOARD_OFFSET_Y, (s.x * 2) + BOARD_OFFSET_X - 1); std::cout << mRepeat(3, s.appearance); } else { tgu::MoveCursor(s.y + BOARD_OFFSET_Y, (s.x * 2) + BOARD_OFFSET_X); std::cout << s.appearance; } if (i != 0 && mHorizontalConnectors(s, mSnake[i - 1])) { int x = std::max(s.x, mSnake[i - 1].x) * 2 + BOARD_OFFSET_X - 1; tgu::MoveCursor(s.y + BOARD_OFFSET_Y, x); std::cout << "═"; } } tgu::TextRed(); tgu::MoveCursor(mApple.y + BOARD_OFFSET_Y, mApple.x * 2 + BOARD_OFFSET_X); std::cout << mApple.appearance; tgu::TerminalColorReset(); std::cout << std::flush; } void mInitializeApple() { int iPos = rand() % mUnoccupied.size(); mCell apple(mUnoccupied[iPos].x, mUnoccupied[iPos].y); mOccupy(apple.x, apple.y); mApple = {apple.x, apple.y, "■"}; } void mAddApple() { mApplesAte++; int iPos = rand() % mUnoccupied.size(); mCell apple(mUnoccupied[iPos].x, mUnoccupied[iPos].y); mUnoccupy(mApple.x, mApple.y); mOccupy(apple.x, apple.y); mApple = {mUnoccupied[iPos].x, mUnoccupied[iPos].y, mApple.appearance}; } void mGameLoop() { while (mRunning) { int CalculatedDelay = STARTING_DELAY - (mApplesAte * ACCELERATION); int TickDelay = CalculatedDelay >= MAX_SPEED ? CalculatedDelay : MAX_SPEED; std::this_thread::sleep_for(std::chrono::milliseconds(TickDelay)); mGameTick(); mDrawSnake(); } } public: struct GameSettings { int width = 14; int height = 14; int startingdelay = 400; int maxspeed = 200; int acceleration = 10; }; Snake(const GameSettings &settings) : WIDTH(settings.width), HEIGHT(settings.height), STARTING_DELAY(settings.startingdelay), ACCELERATION(settings.acceleration), MAX_SPEED(settings.maxspeed) { if (WIDTH < 5 || HEIGHT < 5) { std::cout << "Minimum board size of 5x5 not met."; return; } } void mFillUnoccupied() { for (int i = 0; i < WIDTH; i++) { for (int j = 0; j < HEIGHT; j++) { mUnoccupied.push_back({i, j}); } } } void StartGame() { tgu::EnableRawMode(); tgu::MakeCursorInvisible(); tgu::EnterGameBuffer(); tgu::ClearTerminal(); std::cout << std::flush; mDrawBoardOutlines(); mFillUnoccupied(); mPushSnake({1, 2, "═"}); mPushSnake({2, 2, "═"}); mPushSnake({3, 2, "═"}); mPushSnake({4, 2, "⇒"}); mSnakeDirection = 'r'; mInitializeApple(); mDrawSnake(); auto InputLoopResponse = std::async(std::launch::async, [this] { mInputLoop(); }); mGameLoop(); } }; int main() { Snake hi({.startingdelay = 100}); hi.StartGame(); }