diff --git a/src/games/Pacman/PacmanModule.cpp b/src/games/Pacman/PacmanModule.cpp index 06dc584..53d4534 100644 --- a/src/games/Pacman/PacmanModule.cpp +++ b/src/games/Pacman/PacmanModule.cpp @@ -1,29 +1,383 @@ #include "IGame.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { +using Clock = std::chrono::steady_clock; +using namespace std::chrono_literals; + +struct GridPos { + int x; + int y; + + bool operator==(const GridPos &other) const noexcept { + return x == other.x && y == other.y; + } +}; + +struct Ghost { + GridPos pos{}; + GridPos start{}; + Arcade::InputAction direction{Arcade::InputAction::Left}; + Clock::time_point releaseAt{}; +}; + class PacmanModule : public Arcade::IGame { public: - PacmanModule() = default; - ~PacmanModule() = default; - void reset() override { - return; - }; - void update() override { - return; - }; - void onInput(Arcade::InputAction) override { - return; - }; - std::vector getDisplay() const override { - return std::vector(); - }; - int getScore() const override { - return 0; - }; - std::string getName() const override { - return std::string("Pacman"); - }; + PacmanModule() { + reset(); + } + + ~PacmanModule() override = default; + + void reset() override { + _level = 1; + _score = 0; + _gameOver = false; + _playerDirection = Arcade::InputAction::Left; + _requestedDirection = _playerDirection; + startLevel(); + } + + void update() override { + if (_gameOver) + return; + + auto now = Clock::now(); + auto playerDelay = levelDelay(160ms, 12ms); + auto ghostDelay = isFrightened() ? levelDelay(300ms, 6ms) : levelDelay(220ms, 8ms); + + if (now - _lastPlayerStep >= playerDelay) { + _lastPlayerStep = now; + movePacman(); + resolveCollisions(); + if (_gameOver) + return; + if (_pelletsLeft == 0) { + _score += 250; + ++_level; + startLevel(); + return; + } + } + + if (now - _lastGhostStep >= ghostDelay) { + _lastGhostStep = now; + moveGhosts(); + resolveCollisions(); + } + } + + void onInput(Arcade::InputAction action) override { + switch (action) { + case Arcade::InputAction::Up: + case Arcade::InputAction::Down: + case Arcade::InputAction::Left: + case Arcade::InputAction::Right: + _requestedDirection = action; + break; + default: + break; + } + } + + std::vector getDisplay() const override { + std::vector cells; + cells.reserve(static_cast(kMapWidth * kMapHeight + 200)); + + appendText(cells, 0, 0, "Pacman", 0, 4); + appendText(cells, 0, 1, "Score: " + std::to_string(_score) + " Level: " + std::to_string(_level), 0, 3); + appendText(cells, 0, 2, isFrightened() ? "Power mode active" : "Arrows move | m menu | r restart", 0, 7); + + for (int y = 0; y < kMapHeight; ++y) { + for (int x = 0; x < kMapWidth; ++x) { + char tile = _map[static_cast(y)][static_cast(x)]; + if (tile == '#') + cells.push_back(makeCell(x, kTopOffset + y, '#', 0, 5)); + else if (tile == '.') + cells.push_back(makeCell(x, kTopOffset + y, '.', 0, 4)); + else if (tile == 'o') + cells.push_back(makeCell(x, kTopOffset + y, 'o', 0, 2)); + } + } + + auto now = Clock::now(); + for (const Ghost &ghost : _ghosts) { + if (now < ghost.releaseAt) + continue; + cells.push_back(makeCell(ghost.pos.x, kTopOffset + ghost.pos.y, isFrightened() ? 'g' : 'G', 0, isFrightened() ? 6 : 2)); + } + cells.push_back(makeCell(_pacman.x, kTopOffset + _pacman.y, 'C', 0, 4)); + + if (_gameOver) + appendText(cells, 4, kTopOffset + kMapHeight / 2, "Game Over - press r to restart", 0, 2); + + return cells; + } + + int getScore() const override { + return _score; + } + + std::string getName() const override { + return "Pacman"; + } + +private: + static constexpr int kMapWidth = 19; + static constexpr int kMapHeight = 11; + static constexpr int kTopOffset = 4; + + std::vector _map; + GridPos _pacman{}; + GridPos _pacmanStart{}; + Arcade::InputAction _playerDirection{Arcade::InputAction::Left}; + Arcade::InputAction _requestedDirection{Arcade::InputAction::Left}; + std::vector _ghosts; + int _score{0}; + int _level{1}; + int _pelletsLeft{0}; + bool _gameOver{false}; + Clock::time_point _frightenedUntil{}; + Clock::time_point _lastPlayerStep{}; + Clock::time_point _lastGhostStep{}; + + static Arcade::Cell makeCell(int x, int y, char character, std::uint8_t color, std::uint8_t textColor) { + return Arcade::Cell{static_cast(x), static_cast(y), character, color, textColor}; + } + + static void appendText(std::vector &cells, int x, int y, const std::string &text, std::uint8_t color, std::uint8_t textColor) { + for (std::size_t i = 0; i < text.size(); ++i) + cells.push_back(makeCell(x + static_cast(i), y, text[i], color, textColor)); + } + + static std::vector baseMap() { + return { + "###################", + "#o.......#.......o#", + "#.###.##.#.##.###.#", + "#.................#", + "#.###.#.###.#.###.#", + "o.......#.#.......o", + "#.###.#.....#.###.#", + "#.....#.. ..#.....#", + "#.###.##.#.##.###.#", + "#........#........#", + "###################" + }; + } + + std::chrono::milliseconds levelDelay(std::chrono::milliseconds base, std::chrono::milliseconds reduction) const { + int delta = static_cast(reduction.count()) * (_level - 1); + int capped = std::max(70, static_cast(base.count()) - delta); + return std::chrono::milliseconds(capped); + } + + void startLevel() { + auto now = Clock::now(); + + _map = baseMap(); + _pacmanStart = {9, 7}; + _pacman = _pacmanStart; + _playerDirection = Arcade::InputAction::Left; + _requestedDirection = _playerDirection; + _ghosts = { + Ghost{{8, 6}, {8, 6}, Arcade::InputAction::Left, now + 10s}, + Ghost{{9, 6}, {9, 6}, Arcade::InputAction::Left, now + 12s}, + Ghost{{10, 6}, {10, 6}, Arcade::InputAction::Right, now + 14s} + }; + _pelletsLeft = countPellets(); + _frightenedUntil = Clock::time_point{}; + _lastPlayerStep = now; + _lastGhostStep = now; + } + + int countPellets() const { + int count = 0; + + for (const std::string &row : _map) { + count += static_cast(std::count(row.begin(), row.end(), '.')); + count += static_cast(std::count(row.begin(), row.end(), 'o')); + } + return count; + } + + bool isFrightened() const { + return Clock::now() < _frightenedUntil; + } + + char tileAt(const GridPos &pos) const { + if (pos.y < 0 || pos.y >= kMapHeight) + return '#'; + int wrappedX = wrapX(pos.x); + return _map[static_cast(pos.y)][static_cast(wrappedX)]; + } + + bool isWall(const GridPos &pos) const { + return tileAt(pos) == '#'; + } + + static int wrapX(int x) { + if (x < 0) + return kMapWidth - 1; + if (x >= kMapWidth) + return 0; + return x; + } + + GridPos stepFrom(const GridPos &start, Arcade::InputAction dir) const { + GridPos next = start; + + switch (dir) { + case Arcade::InputAction::Up: + --next.y; + break; + case Arcade::InputAction::Down: + ++next.y; + break; + case Arcade::InputAction::Left: + --next.x; + break; + case Arcade::InputAction::Right: + ++next.x; + break; + default: + break; + } + next.x = wrapX(next.x); + return next; + } + + static Arcade::InputAction opposite(Arcade::InputAction dir) { + switch (dir) { + case Arcade::InputAction::Up: + return Arcade::InputAction::Down; + case Arcade::InputAction::Down: + return Arcade::InputAction::Up; + case Arcade::InputAction::Left: + return Arcade::InputAction::Right; + case Arcade::InputAction::Right: + return Arcade::InputAction::Left; + default: + return Arcade::InputAction::None; + } + } + + bool canMove(const GridPos &from, Arcade::InputAction dir) const { + if (dir != Arcade::InputAction::Up && dir != Arcade::InputAction::Down + && dir != Arcade::InputAction::Left && dir != Arcade::InputAction::Right) + return false; + return !isWall(stepFrom(from, dir)); + } + + void consumeTile(const GridPos &pos) { + int x = wrapX(pos.x); + char &tile = _map[static_cast(pos.y)][static_cast(x)]; + + if (tile == '.') { + tile = ' '; + _score += 10; + --_pelletsLeft; + } else if (tile == 'o') { + tile = ' '; + _score += 50; + --_pelletsLeft; + _frightenedUntil = Clock::now() + 10s; + } + } + + void movePacman() { + if (canMove(_pacman, _requestedDirection)) + _playerDirection = _requestedDirection; + if (!canMove(_pacman, _playerDirection)) + return; + _pacman = stepFrom(_pacman, _playerDirection); + consumeTile(_pacman); + } + + std::vector availableDirections(const Ghost &ghost) const { + std::vector dirs; + constexpr std::array allDirs = { + Arcade::InputAction::Up, + Arcade::InputAction::Down, + Arcade::InputAction::Left, + Arcade::InputAction::Right + }; + + for (Arcade::InputAction dir : allDirs) { + if (canMove(ghost.pos, dir)) + dirs.push_back(dir); + } + return dirs; + } + + Arcade::InputAction chooseGhostDirection(const Ghost &ghost) { + std::vector dirs = availableDirections(ghost); + if (dirs.empty()) + return Arcade::InputAction::None; + + Arcade::InputAction reverse = opposite(ghost.direction); + if (dirs.size() > 1) { + dirs.erase(std::remove(dirs.begin(), dirs.end(), reverse), dirs.end()); + if (dirs.empty()) + dirs.push_back(reverse); + } + + bool frightened = isFrightened(); + Arcade::InputAction best = dirs.front(); + int bestScore = std::abs(stepFrom(ghost.pos, best).x - _pacman.x) + std::abs(stepFrom(ghost.pos, best).y - _pacman.y); + + for (Arcade::InputAction dir : dirs) { + GridPos next = stepFrom(ghost.pos, dir); + int distance = std::abs(next.x - _pacman.x) + std::abs(next.y - _pacman.y); + if ((!frightened && distance < bestScore) || (frightened && distance > bestScore)) { + best = dir; + bestScore = distance; + } + } + return best; + } + + void moveGhosts() { + auto now = Clock::now(); + + for (Ghost &ghost : _ghosts) { + if (now < ghost.releaseAt) + continue; + Arcade::InputAction chosen = chooseGhostDirection(ghost); + if (chosen == Arcade::InputAction::None) + continue; + ghost.direction = chosen; + ghost.pos = stepFrom(ghost.pos, chosen); + } + } + + void resolveCollisions() { + auto now = Clock::now(); + + for (Ghost &ghost : _ghosts) { + if (now < ghost.releaseAt || !(ghost.pos == _pacman)) + continue; + if (isFrightened()) { + ghost.pos = ghost.start; + ghost.direction = Arcade::InputAction::Left; + ghost.releaseAt = now + 1500ms; + _score += 200; + } else { + _gameOver = true; + return; + } + } + } }; +} -extern "C" Arcade::IGame* createGame() { - return new PacmanModule(); +extern "C" Arcade::IGame *createGame() { + return new PacmanModule(); } diff --git a/src/games/Snake/SnakeModule.cpp b/src/games/Snake/SnakeModule.cpp index de2bbc3..216184c 100644 --- a/src/games/Snake/SnakeModule.cpp +++ b/src/games/Snake/SnakeModule.cpp @@ -1,29 +1,241 @@ #include "IGame.hpp" +#include +#include +#include +#include +#include +#include + +namespace { +struct GridPos { + int x; + int y; +}; + +enum class Direction { + Up, + Down, + Left, + Right +}; + class SnakeModule : public Arcade::IGame { public: - SnakeModule() = default; - ~SnakeModule() = default; - void reset() override { - return; - }; - void update() override { - return; - }; - void onInput(Arcade::InputAction) override { - return; - }; - std::vector getDisplay() const override { - return std::vector(); - }; - int getScore() const override { - return 0; - }; - std::string getName() const override { - return std::string("Snake"); - }; + SnakeModule() + : _rng(std::random_device{}()) + { + reset(); + } + + ~SnakeModule() override = default; + + void reset() override { + _snake.clear(); + const int midX = kBoardWidth / 2; + const int midY = kBoardHeight / 2; + _snake.push_back({midX, midY}); + _snake.push_back({midX - 1, midY}); + _snake.push_back({midX - 2, midY}); + _snake.push_back({midX - 3, midY}); + _score = 0; + _gameOver = false; + _direction = Direction::Right; + _pendingDirection = Direction::Right; + _lastStep = std::chrono::steady_clock::now(); + spawnFood(); + } + + void update() override { + if (_gameOver) + return; + const auto now = std::chrono::steady_clock::now(); + if (now - _lastStep < kStepDelay) + return; + _lastStep = now; + _direction = _pendingDirection; + advance(); + } + + void onInput(Arcade::InputAction action) override { + switch (action) { + case Arcade::InputAction::Up: + if (_direction != Direction::Down) + _pendingDirection = Direction::Up; + break; + case Arcade::InputAction::Down: + if (_direction != Direction::Up) + _pendingDirection = Direction::Down; + break; + case Arcade::InputAction::Left: + if (_direction != Direction::Right) + _pendingDirection = Direction::Left; + break; + case Arcade::InputAction::Right: + if (_direction != Direction::Left) + _pendingDirection = Direction::Right; + break; + default: + break; + } + } + + std::vector getDisplay() const override + { + std::vector cells; + + appendText(cells, 0, 0, "Snake", 0, 4); + appendText(cells, 0, 1, "Score: " + std::to_string(_score), 0, 3); + appendText(cells, 0, 2, "Length: " + std::to_string(_snake.size()), 0, 6); + appendText(cells, 0, 3, _gameOver ? "Game Over - press R to restart" : "Arrows: move | R: restart | M: menu", 0, 7); + + for (int x = 0; x < kBoardWidth; ++x) { + cells.push_back(makeCell(x, kTopOffset, '#', 0, 5)); + cells.push_back(makeCell(x, kTopOffset + kBoardHeight - 1, '#', 0, 5)); + } + for (int y = 1; y < kBoardHeight - 1; ++y) { + cells.push_back(makeCell(0, kTopOffset + y, '#', 0, 5)); + cells.push_back(makeCell(kBoardWidth - 1, kTopOffset + y, '#', 0, 5)); + } + + for (int y = 1; y < kBoardHeight - 1; ++y) { + for (int x = 1; x < kBoardWidth - 1; ++x) { + cells.push_back(makeCell(x, kTopOffset + y, ' ', 0, 1)); + } + } + + cells.push_back(makeCell(_food.x, kTopOffset + _food.y, '$', 0, 4)); + + for (std::size_t i = 0; i < _snake.size(); ++i) { + const GridPos &part = _snake[i]; + cells.push_back(makeCell(part.x, kTopOffset + part.y, i == 0 ? 'O' : 'o', 0, i == 0 ? 2 : 3)); + } + + if (_gameOver) { + appendText(cells, 6, kTopOffset + kBoardHeight / 2, "GAME OVER", 0, 4); + } + + return cells; + } + + int getScore() const override { + return _score; + } + + std::string getName() const override { + return "Snake"; + } + +private: + static constexpr int kBoardWidth = 30; + static constexpr int kBoardHeight = 20; + static constexpr int kTopOffset = 4; + static constexpr auto kStepDelay = std::chrono::milliseconds(140); + + std::deque _snake; + GridPos _food{}; + int _score{0}; + bool _gameOver{false}; + Direction _direction{Direction::Right}; + Direction _pendingDirection{Direction::Right}; + std::chrono::steady_clock::time_point _lastStep{}; + mutable std::mt19937 _rng; + + static Arcade::Cell makeCell(int x, int y, char character, std::uint8_t color, std::uint8_t textColor) { + return Arcade::Cell{static_cast(x), static_cast(y), character, color, textColor}; + } + + static void appendText(std::vector &cells, int x, int y, const std::string &text, std::uint8_t color, std::uint8_t text_color) { + for (std::size_t i = 0; i < text.size(); ++i) { + cells.push_back(makeCell(x + static_cast(i), y, text[i], color, text_color)); + } + } + + bool isOnSnake(const GridPos &pos) const { + for (const GridPos &part : _snake) { + if (part.x == pos.x && part.y == pos.y) + return true; + } + return false; + } + + void spawnFood() { + std::uniform_int_distribution distX(1, kBoardWidth - 2); + std::uniform_int_distribution distY(1, kBoardHeight - 2); + + GridPos candidate{}; + do { + candidate = {distX(_rng), distY(_rng)}; + } while (isOnSnake(candidate)); + + _food = candidate; + } + + bool isBodyCollision(const GridPos &pos) const { + for (const GridPos &part : _snake) { + if (part.x == pos.x && part.y == pos.y) + return true; + } + return false; + } + + void advance() { + if (_snake.empty()) + return; + GridPos next = _snake.front(); + switch (_direction) { + case Direction::Up: + --next.y; + break; + case Direction::Down: + ++next.y; + break; + case Direction::Left: + --next.x; + break; + case Direction::Right: + ++next.x; + break; + } + + const int minX = 1; + const int maxX = kBoardWidth - 2; + const int minY = 1; + const int maxY = kBoardHeight - 2; + + if (next.x < minX || next.x > maxX || next.y < minY || next.y > maxY) { + _gameOver = true; + return; + } + + bool grows = (next.x == _food.x && next.y == _food.y); + + if (!grows && !_snake.empty()) { + GridPos tail = _snake.back(); + _snake.pop_back(); + if (isBodyCollision(next)) { + _snake.push_back(tail); + _gameOver = true; + return; + } + _snake.push_back(tail); + } else if (isBodyCollision(next)) { + _gameOver = true; + return; + } + + _snake.push_front(next); + if (grows) { + _score += 10; + spawnFood(); + } else { + _snake.pop_back(); + } + } }; +} -extern "C" Arcade::IGame* createGame() { - return new SnakeModule(); +extern "C" Arcade::IGame *createGame() +{ + return new SnakeModule(); }