From b6f605d2fd8cb122357fd9ff5180c2b3fa309235 Mon Sep 17 00:00:00 2001 From: YetAnotherFactsEnjoyer Date: Wed, 8 Apr 2026 16:00:10 +0200 Subject: [PATCH 01/12] [FEAT] replace stub with playable game skeleton --- src/games/Snake/SnakeModule.cpp | 117 ++++++++++++++++++++++++++------ 1 file changed, 95 insertions(+), 22 deletions(-) diff --git a/src/games/Snake/SnakeModule.cpp b/src/games/Snake/SnakeModule.cpp index de2bbc3..ba2550a 100644 --- a/src/games/Snake/SnakeModule.cpp +++ b/src/games/Snake/SnakeModule.cpp @@ -1,29 +1,102 @@ #include "IGame.hpp" +#include +#include +#include + +namespace { +struct GridPos { + int x; + int y; +}; + 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() + { + 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; + } + + void update() override + { + } + + void onInput(Arcade::InputAction) override + { + } + + std::vector getDisplay() const override + { + std::vector cells; + + appendText(cells, 0, 0, "Snake", 4); + appendText(cells, 0, 1, "Score: " + std::to_string(_score), 3); + appendText(cells, 0, 2, "Skeleton version", 7); + + for (int x = 0; x < kBoardWidth; ++x) { + cells.push_back(makeCell(x, kTopOffset, '#', 5)); + cells.push_back(makeCell(x, kTopOffset + kBoardHeight - 1, '#', 5)); + } + for (int y = 1; y < kBoardHeight - 1; ++y) { + cells.push_back(makeCell(0, kTopOffset + y, '#', 5)); + cells.push_back(makeCell(kBoardWidth - 1, kTopOffset + y, '#', 5)); + } + + 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', i == 0 ? 2 : 3)); + } + + 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; + + std::deque _snake; + int _score{0}; + + static Arcade::Cell makeCell(int x, int y, char character, int color) + { + return Arcade::Cell{static_cast(x), static_cast(y), character, color}; + } + + static void appendText(std::vector &cells, int x, int y, const std::string &text, int color) + { + for (std::size_t i = 0; i < text.size(); ++i) { + cells.push_back(makeCell(x + static_cast(i), y, text[i], color)); + } + } }; +} -extern "C" Arcade::IGame* createGame() { - return new SnakeModule(); +extern "C" Arcade::IGame *createGame() +{ + return new SnakeModule(); } From 83e61bd7b0f13bbe05922cd1290105e6b9ff1ee9 Mon Sep 17 00:00:00 2001 From: YetAnotherFactsEnjoyer Date: Wed, 8 Apr 2026 16:06:50 +0200 Subject: [PATCH 02/12] [FEAT] add timed movement and direction handling --- src/games/Snake/SnakeModule.cpp | 86 ++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/src/games/Snake/SnakeModule.cpp b/src/games/Snake/SnakeModule.cpp index ba2550a..d897beb 100644 --- a/src/games/Snake/SnakeModule.cpp +++ b/src/games/Snake/SnakeModule.cpp @@ -1,5 +1,6 @@ #include "IGame.hpp" +#include #include #include #include @@ -10,6 +11,13 @@ struct GridPos { int y; }; +enum class Direction { + Up, + Down, + Left, + Right +}; + class SnakeModule : public Arcade::IGame { public: SnakeModule() @@ -29,14 +37,44 @@ class SnakeModule : public Arcade::IGame { _snake.push_back({midX - 2, midY}); _snake.push_back({midX - 3, midY}); _score = 0; + _direction = Direction::Right; + _pendingDirection = Direction::Right; + _lastStep = std::chrono::steady_clock::now(); } void update() override { + const auto now = std::chrono::steady_clock::now(); + if (now - _lastStep < kStepDelay) { + return; + } + _lastStep = now; + _direction = _pendingDirection; + advance(); } - void onInput(Arcade::InputAction) override + 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 @@ -78,9 +116,13 @@ class SnakeModule : public Arcade::IGame { 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; int _score{0}; + Direction _direction{Direction::Right}; + Direction _pendingDirection{Direction::Right}; + std::chrono::steady_clock::time_point _lastStep{}; static Arcade::Cell makeCell(int x, int y, char character, int color) { @@ -93,8 +135,48 @@ class SnakeModule : public Arcade::IGame { cells.push_back(makeCell(x + static_cast(i), y, text[i], color)); } } + + 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 = minX; + if (next.x > maxX) + next.x = maxX; + if (next.y < minY) + next.y = minY; + if (next.y > maxY) + next.y = maxY; + + _snake.push_front(next); + _snake.pop_back(); + } }; -} +} // namespace extern "C" Arcade::IGame *createGame() { From 6d642f48ca996b67652b34aa308b3c7f14cdc70e Mon Sep 17 00:00:00 2001 From: YetAnotherFactsEnjoyer Date: Wed, 8 Apr 2026 16:10:44 +0200 Subject: [PATCH 03/12] [FEAT] implement food spawning and snake growth --- src/games/Snake/SnakeModule.cpp | 39 +++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/games/Snake/SnakeModule.cpp b/src/games/Snake/SnakeModule.cpp index d897beb..9a2a138 100644 --- a/src/games/Snake/SnakeModule.cpp +++ b/src/games/Snake/SnakeModule.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -21,6 +22,7 @@ enum class Direction { class SnakeModule : public Arcade::IGame { public: SnakeModule() + : _rng(std::random_device{}()) { reset(); } @@ -40,6 +42,7 @@ class SnakeModule : public Arcade::IGame { _direction = Direction::Right; _pendingDirection = Direction::Right; _lastStep = std::chrono::steady_clock::now(); + spawnFood(); } void update() override @@ -94,6 +97,8 @@ class SnakeModule : public Arcade::IGame { cells.push_back(makeCell(kBoardWidth - 1, kTopOffset + y, '#', 5)); } + cells.push_back(makeCell(_food.x, kTopOffset + _food.y, '*', 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', i == 0 ? 2 : 3)); @@ -119,10 +124,12 @@ class SnakeModule : public Arcade::IGame { static constexpr auto kStepDelay = std::chrono::milliseconds(140); std::deque _snake; + GridPos _food{}; int _score{0}; 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, int color) { @@ -136,6 +143,29 @@ class SnakeModule : public Arcade::IGame { } } + 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; + } + void advance() { if (_snake.empty()) { @@ -173,10 +203,15 @@ class SnakeModule : public Arcade::IGame { next.y = maxY; _snake.push_front(next); - _snake.pop_back(); + if (next.x == _food.x && next.y == _food.y) { + _score += 10; + spawnFood(); + } else { + _snake.pop_back(); + } } }; -} // namespace +} extern "C" Arcade::IGame *createGame() { From 7aea34abbbeba618db34426177f0afc2a5cb52a7 Mon Sep 17 00:00:00 2001 From: YetAnotherFactsEnjoyer Date: Wed, 8 Apr 2026 16:14:43 +0200 Subject: [PATCH 04/12] [FEAT] add wall and self collision game over --- src/games/Snake/SnakeModule.cpp | 47 ++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/games/Snake/SnakeModule.cpp b/src/games/Snake/SnakeModule.cpp index 9a2a138..0f8d711 100644 --- a/src/games/Snake/SnakeModule.cpp +++ b/src/games/Snake/SnakeModule.cpp @@ -39,6 +39,7 @@ class SnakeModule : public Arcade::IGame { _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(); @@ -47,6 +48,9 @@ class SnakeModule : public Arcade::IGame { void update() override { + if (_gameOver) { + return; + } const auto now = std::chrono::steady_clock::now(); if (now - _lastStep < kStepDelay) { return; @@ -86,7 +90,7 @@ class SnakeModule : public Arcade::IGame { appendText(cells, 0, 0, "Snake", 4); appendText(cells, 0, 1, "Score: " + std::to_string(_score), 3); - appendText(cells, 0, 2, "Skeleton version", 7); + appendText(cells, 0, 2, _gameOver ? "Game Over - press R to restart" : "Eat food and avoid collisions", 7); for (int x = 0; x < kBoardWidth; ++x) { cells.push_back(makeCell(x, kTopOffset, '#', 5)); @@ -126,6 +130,7 @@ class SnakeModule : public Arcade::IGame { 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{}; @@ -166,6 +171,16 @@ class SnakeModule : public Arcade::IGame { _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()) { @@ -193,17 +208,29 @@ class SnakeModule : public Arcade::IGame { const int minY = 1; const int maxY = kBoardHeight - 2; - if (next.x < minX) - next.x = minX; - if (next.x > maxX) - next.x = maxX; - if (next.y < minY) - next.y = minY; - if (next.y > maxY) - next.y = maxY; + 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 (next.x == _food.x && next.y == _food.y) { + if (grows) { _score += 10; spawnFood(); } else { From 31d057fe4b10c8487acbaf8e157963b1f734555f Mon Sep 17 00:00:00 2001 From: YetAnotherFactsEnjoyer Date: Wed, 8 Apr 2026 16:18:53 +0200 Subject: [PATCH 05/12] [FEAT] improve HUD and board rendering --- src/games/Snake/SnakeModule.cpp | 66 +++++++++++++++------------------ 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/src/games/Snake/SnakeModule.cpp b/src/games/Snake/SnakeModule.cpp index 0f8d711..08c4385 100644 --- a/src/games/Snake/SnakeModule.cpp +++ b/src/games/Snake/SnakeModule.cpp @@ -29,8 +29,7 @@ class SnakeModule : public Arcade::IGame { ~SnakeModule() override = default; - void reset() override - { + void reset() override { _snake.clear(); const int midX = kBoardWidth / 2; const int midY = kBoardHeight / 2; @@ -46,22 +45,18 @@ class SnakeModule : public Arcade::IGame { spawnFood(); } - void update() override - { - if (_gameOver) { + void update() override { + if (_gameOver) return; - } const auto now = std::chrono::steady_clock::now(); - if (now - _lastStep < kStepDelay) { + if (now - _lastStep < kStepDelay) return; - } _lastStep = now; _direction = _pendingDirection; advance(); } - void onInput(Arcade::InputAction action) override - { + void onInput(Arcade::InputAction action) override { switch (action) { case Arcade::InputAction::Up: if (_direction != Direction::Down) @@ -90,7 +85,8 @@ class SnakeModule : public Arcade::IGame { appendText(cells, 0, 0, "Snake", 4); appendText(cells, 0, 1, "Score: " + std::to_string(_score), 3); - appendText(cells, 0, 2, _gameOver ? "Game Over - press R to restart" : "Eat food and avoid collisions", 7); + appendText(cells, 0, 2, "Length: " + std::to_string(_snake.size()), 6); + appendText(cells, 0, 3, _gameOver ? "Game Over - press R to restart" : "Arrows: move | R: restart | M: menu", 7); for (int x = 0; x < kBoardWidth; ++x) { cells.push_back(makeCell(x, kTopOffset, '#', 5)); @@ -101,23 +97,31 @@ class SnakeModule : public Arcade::IGame { cells.push_back(makeCell(kBoardWidth - 1, kTopOffset + y, '#', 5)); } - cells.push_back(makeCell(_food.x, kTopOffset + _food.y, '*', 4)); + for (int y = 1; y < kBoardHeight - 1; ++y) { + for (int x = 1; x < kBoardWidth - 1; ++x) { + cells.push_back(makeCell(x, kTopOffset + y, '.', 1)); + } + } + + cells.push_back(makeCell(_food.x, kTopOffset + _food.y, '$', 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', i == 0 ? 2 : 3)); + cells.push_back(makeCell(part.x, kTopOffset + part.y, i == 0 ? 'O' : 'o', i == 0 ? 2 : 3)); + } + + if (_gameOver) { + appendText(cells, 6, kTopOffset + kBoardHeight / 2, "GAME OVER", 4); } return cells; } - int getScore() const override - { + int getScore() const override { return _score; } - std::string getName() const override - { + std::string getName() const override { return "Snake"; } @@ -136,30 +140,25 @@ class SnakeModule : public Arcade::IGame { std::chrono::steady_clock::time_point _lastStep{}; mutable std::mt19937 _rng; - static Arcade::Cell makeCell(int x, int y, char character, int color) - { + static Arcade::Cell makeCell(int x, int y, char character, int color) { return Arcade::Cell{static_cast(x), static_cast(y), character, color}; } - static void appendText(std::vector &cells, int x, int y, const std::string &text, int color) - { + static void appendText(std::vector &cells, int x, int y, const std::string &text, int color) { for (std::size_t i = 0; i < text.size(); ++i) { cells.push_back(makeCell(x + static_cast(i), y, text[i], color)); } } - bool isOnSnake(const GridPos &pos) const - { + bool isOnSnake(const GridPos &pos) const { for (const GridPos &part : _snake) { - if (part.x == pos.x && part.y == pos.y) { + if (part.x == pos.x && part.y == pos.y) return true; - } } return false; } - void spawnFood() - { + void spawnFood() { std::uniform_int_distribution distX(1, kBoardWidth - 2); std::uniform_int_distribution distY(1, kBoardHeight - 2); @@ -171,22 +170,17 @@ class SnakeModule : public Arcade::IGame { _food = candidate; } - bool isBodyCollision(const GridPos &pos) const - { + bool isBodyCollision(const GridPos &pos) const { for (const GridPos &part : _snake) { - if (part.x == pos.x && part.y == pos.y) { + if (part.x == pos.x && part.y == pos.y) return true; - } } return false; } - void advance() - { - if (_snake.empty()) { + void advance() { + if (_snake.empty()) return; - } - GridPos next = _snake.front(); switch (_direction) { case Direction::Up: From 07ea243bc12aa2a183eb5693019186a3c3913229 Mon Sep 17 00:00:00 2001 From: YetAnotherFactsEnjoyer Date: Sat, 11 Apr 2026 11:57:09 +0200 Subject: [PATCH 06/12] [FEAT] replace stub with maze-based game skeleton --- src/games/Pacman/PacmanModule.cpp | 178 ++++++++++++++++++++++++++---- 1 file changed, 156 insertions(+), 22 deletions(-) diff --git a/src/games/Pacman/PacmanModule.cpp b/src/games/Pacman/PacmanModule.cpp index 06dc584..2d27312 100644 --- a/src/games/Pacman/PacmanModule.cpp +++ b/src/games/Pacman/PacmanModule.cpp @@ -1,29 +1,163 @@ #include "IGame.hpp" +#include +#include +#include + +namespace { +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}; +}; + 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 { + } + + 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; + + appendText(cells, 0, 0, "Pacman", 4); + appendText(cells, 0, 1, "Score: " + std::to_string(_score) + " Level: " + std::to_string(_level), 3); + appendText(cells, 0, 2, "Skeleton version", 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, '#', 5)); + else if (tile == '.') + cells.push_back(makeCell(x, kTopOffset + y, '.', 4)); + else if (tile == 'o') + cells.push_back(makeCell(x, kTopOffset + y, 'o', 2)); + } + } + + for (const Ghost &ghost : _ghosts) + cells.push_back(makeCell(ghost.pos.x, kTopOffset + ghost.pos.y, 'G', 2)); + + cells.push_back(makeCell(_pacman.x, kTopOffset + _pacman.y, 'C', 4)); + + if (_gameOver) + appendText(cells, 4, kTopOffset + kMapHeight / 2, "Game Over - press r to restart", 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}; + + static Arcade::Cell makeCell(int x, int y, char character, int color) { + return Arcade::Cell{static_cast(x), static_cast(y), character, color}; + } + + static void appendText(std::vector &cells, int x, int y, const std::string &text, int color) { + for (std::size_t i = 0; i < text.size(); ++i) + cells.push_back(makeCell(x + static_cast(i), y, text[i], color)); + } + + static std::vector baseMap() { + return { + "###################", + "#o.......#.......o#", + "#.###.##.#.##.###.#", + "#.................#", + "#.###.#.###.#.###.#", + "........#.#........", + "#.###.#.###.#.###.#", + "#.....#.....#.....#", + "#.###.##.#.##.###.#", + "#o.................", + "###################" + }; + } + + 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; + } + + void startLevel() { + _map = baseMap(); + _pacmanStart = {9, 7}; + _pacman = _pacmanStart; + _playerDirection = Arcade::InputAction::Left; + _requestedDirection = _playerDirection; + _ghosts = { + Ghost{{9, 5}, {9, 5}, Arcade::InputAction::Left}, + Ghost{{8, 5}, {8, 5}, Arcade::InputAction::Right}, + Ghost{{10, 5}, {10, 5}, Arcade::InputAction::Up} + }; + _pelletsLeft = countPellets(); + } }; +} -extern "C" Arcade::IGame* createGame() { - return new PacmanModule(); +extern "C" Arcade::IGame *createGame() { + return new PacmanModule(); } From 23126d4866f4dcfd87e4f1413ce447230c3101f9 Mon Sep 17 00:00:00 2001 From: YetAnotherFactsEnjoyer Date: Sat, 11 Apr 2026 11:58:54 +0200 Subject: [PATCH 07/12] [FEAT] implement player movement and pellet consumption --- src/games/Pacman/PacmanModule.cpp | 68 ++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/src/games/Pacman/PacmanModule.cpp b/src/games/Pacman/PacmanModule.cpp index 2d27312..12a14ee 100644 --- a/src/games/Pacman/PacmanModule.cpp +++ b/src/games/Pacman/PacmanModule.cpp @@ -1,6 +1,7 @@ #include "IGame.hpp" #include +#include #include #include @@ -34,10 +35,20 @@ class PacmanModule : public Arcade::IGame { _gameOver = false; _playerDirection = Arcade::InputAction::Left; _requestedDirection = _playerDirection; + _lastStep = std::chrono::steady_clock::now(); startLevel(); } void update() override { + if (_gameOver) + return; + + auto now = std::chrono::steady_clock::now(); + + if (now - _lastStep < kStepDelay) + return; + _lastStep = now; + movePacman(); } void onInput(Arcade::InputAction action) override { @@ -58,24 +69,27 @@ class PacmanModule : public Arcade::IGame { appendText(cells, 0, 0, "Pacman", 4); appendText(cells, 0, 1, "Score: " + std::to_string(_score) + " Level: " + std::to_string(_level), 3); - appendText(cells, 0, 2, "Skeleton version", 7); + appendText(cells, 0, 2, "Arrows: move | R: restart | M: menu", 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, '#', 5)); else if (tile == '.') cells.push_back(makeCell(x, kTopOffset + y, '.', 4)); else if (tile == 'o') cells.push_back(makeCell(x, kTopOffset + y, 'o', 2)); + else + cells.push_back(makeCell(x, kTopOffset + y, ' ', 1)); } } for (const Ghost &ghost : _ghosts) cells.push_back(makeCell(ghost.pos.x, kTopOffset + ghost.pos.y, 'G', 2)); - cells.push_back(makeCell(_pacman.x, kTopOffset + _pacman.y, 'C', 4)); + cells.push_back(makeCell(_pacman.x, kTopOffset + _pacman.y, 'C', 6)); if (_gameOver) appendText(cells, 4, kTopOffset + kMapHeight / 2, "Game Over - press r to restart", 2); @@ -95,6 +109,7 @@ class PacmanModule : public Arcade::IGame { static constexpr int kMapWidth = 19; static constexpr int kMapHeight = 11; static constexpr int kTopOffset = 4; + static constexpr auto kStepDelay = std::chrono::milliseconds(160); std::vector _map; GridPos _pacman{}; @@ -106,6 +121,7 @@ class PacmanModule : public Arcade::IGame { int _level{1}; int _pelletsLeft{0}; bool _gameOver{false}; + std::chrono::steady_clock::time_point _lastStep{}; static Arcade::Cell makeCell(int x, int y, char character, int color) { return Arcade::Cell{static_cast(x), static_cast(y), character, color}; @@ -155,6 +171,54 @@ class PacmanModule : public Arcade::IGame { }; _pelletsLeft = countPellets(); } + + bool isWalkable(const GridPos &pos) const { + if (pos.y < 0 || pos.y >= kMapHeight || pos.x < 0 || pos.x >= kMapWidth) + return false; + return _map[static_cast(pos.y)][static_cast(pos.x)] != '#'; + } + + GridPos nextPosition(const GridPos &from, Arcade::InputAction direction) const { + GridPos next = from; + + if (direction == Arcade::InputAction::Up) + --next.y; + else if (direction == Arcade::InputAction::Down) + ++next.y; + else if (direction == Arcade::InputAction::Left) + --next.x; + else if (direction == Arcade::InputAction::Right) + ++next.x; + return next; + } + + void consumeTile() { + char &tile = _map[static_cast(_pacman.y)][static_cast(_pacman.x)]; + + if (tile == '.') { + tile = ' '; + _score += 10; + --_pelletsLeft; + } else if (tile == 'o') { + tile = ' '; + _score += 50; + --_pelletsLeft; + } + } + + void movePacman() { + GridPos requested = nextPosition(_pacman, _requestedDirection); + + if (isWalkable(requested)) + _playerDirection = _requestedDirection; + + GridPos next = nextPosition(_pacman, _playerDirection); + + if (!isWalkable(next)) + return; + _pacman = next; + consumeTile(); + } }; } From 009c744f87126f172980ed5fcc6522faab1008b8 Mon Sep 17 00:00:00 2001 From: YetAnotherFactsEnjoyer Date: Sat, 11 Apr 2026 12:00:52 +0200 Subject: [PATCH 08/12] [FEAT] add tunnel wrapping and level progression --- src/games/Pacman/PacmanModule.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/games/Pacman/PacmanModule.cpp b/src/games/Pacman/PacmanModule.cpp index 12a14ee..2bd35c4 100644 --- a/src/games/Pacman/PacmanModule.cpp +++ b/src/games/Pacman/PacmanModule.cpp @@ -170,6 +170,15 @@ class PacmanModule : public Arcade::IGame { Ghost{{10, 5}, {10, 5}, Arcade::InputAction::Up} }; _pelletsLeft = countPellets(); + _lastStep = std::chrono::steady_clock::now(); + } + + int wrapX(int x) const { + if (x < 0) + return kMapWidth - 1; + if (x >= kMapWidth) + return 0; + return x; } bool isWalkable(const GridPos &pos) const { @@ -189,6 +198,7 @@ class PacmanModule : public Arcade::IGame { --next.x; else if (direction == Arcade::InputAction::Right) ++next.x; + next.x = wrapX(next.x); return next; } @@ -218,6 +228,11 @@ class PacmanModule : public Arcade::IGame { return; _pacman = next; consumeTile(); + + if (_pelletsLeft <= 0) { + ++_level; + startLevel(); + } } }; } From 134ea25c2c8f8df8e27404a9bbce4afd9790b39f Mon Sep 17 00:00:00 2001 From: YetAnotherFactsEnjoyer Date: Sat, 11 Apr 2026 12:02:43 +0200 Subject: [PATCH 09/12] [FEAT] add ghost movement and frightened mode --- src/games/Pacman/PacmanModule.cpp | 97 +++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 4 deletions(-) diff --git a/src/games/Pacman/PacmanModule.cpp b/src/games/Pacman/PacmanModule.cpp index 2bd35c4..360f4ee 100644 --- a/src/games/Pacman/PacmanModule.cpp +++ b/src/games/Pacman/PacmanModule.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -33,6 +34,7 @@ class PacmanModule : public Arcade::IGame { _level = 1; _score = 0; _gameOver = false; + _frightenedSteps = 0; _playerDirection = Arcade::InputAction::Left; _requestedDirection = _playerDirection; _lastStep = std::chrono::steady_clock::now(); @@ -49,6 +51,10 @@ class PacmanModule : public Arcade::IGame { return; _lastStep = now; movePacman(); + moveGhosts(); + + if (_frightenedSteps > 0) + --_frightenedSteps; } void onInput(Arcade::InputAction action) override { @@ -69,7 +75,7 @@ class PacmanModule : public Arcade::IGame { appendText(cells, 0, 0, "Pacman", 4); appendText(cells, 0, 1, "Score: " + std::to_string(_score) + " Level: " + std::to_string(_level), 3); - appendText(cells, 0, 2, "Arrows: move | R: restart | M: menu", 7); + appendText(cells, 0, 2, _frightenedSteps > 0 ? "Power mode active" : "Arrows: move | R: restart | M: menu", 7); for (int y = 0; y < kMapHeight; ++y) { for (int x = 0; x < kMapWidth; ++x) { @@ -86,10 +92,13 @@ class PacmanModule : public Arcade::IGame { } } - for (const Ghost &ghost : _ghosts) - cells.push_back(makeCell(ghost.pos.x, kTopOffset + ghost.pos.y, 'G', 2)); + for (const Ghost &ghost : _ghosts) { + char c = _frightenedSteps > 0 ? 'g' : 'G'; + int color = _frightenedSteps > 0 ? 6 : 2; + cells.push_back(makeCell(ghost.pos.x, kTopOffset + ghost.pos.y, c, color)); + } - cells.push_back(makeCell(_pacman.x, kTopOffset + _pacman.y, 'C', 6)); + cells.push_back(makeCell(_pacman.x, kTopOffset + _pacman.y, 'C', 4)); if (_gameOver) appendText(cells, 4, kTopOffset + kMapHeight / 2, "Game Over - press r to restart", 2); @@ -110,6 +119,7 @@ class PacmanModule : public Arcade::IGame { static constexpr int kMapHeight = 11; static constexpr int kTopOffset = 4; static constexpr auto kStepDelay = std::chrono::milliseconds(160); + static constexpr int kFrightenedDuration = 35; std::vector _map; GridPos _pacman{}; @@ -120,6 +130,7 @@ class PacmanModule : public Arcade::IGame { int _score{0}; int _level{1}; int _pelletsLeft{0}; + int _frightenedSteps{0}; bool _gameOver{false}; std::chrono::steady_clock::time_point _lastStep{}; @@ -170,6 +181,7 @@ class PacmanModule : public Arcade::IGame { Ghost{{10, 5}, {10, 5}, Arcade::InputAction::Up} }; _pelletsLeft = countPellets(); + _frightenedSteps = 0; _lastStep = std::chrono::steady_clock::now(); } @@ -202,6 +214,72 @@ class PacmanModule : public Arcade::IGame { return next; } + int distanceToPacman(const GridPos &pos) const { + return std::abs(pos.x - _pacman.x) + std::abs(pos.y - _pacman.y); + } + + Arcade::InputAction oppositeDirection(Arcade::InputAction direction) const { + if (direction == Arcade::InputAction::Up) + return Arcade::InputAction::Down; + if (direction == Arcade::InputAction::Down) + return Arcade::InputAction::Up; + if (direction == Arcade::InputAction::Left) + return Arcade::InputAction::Right; + return Arcade::InputAction::Left; + } + + std::vector validDirections(const Ghost &ghost) const { + std::vector dirs; + std::vector all = { + Arcade::InputAction::Up, + Arcade::InputAction::Down, + Arcade::InputAction::Left, + Arcade::InputAction::Right + }; + + for (Arcade::InputAction dir : all) { + GridPos next = nextPosition(ghost.pos, dir); + + if (isWalkable(next)) + dirs.push_back(dir); + } + return dirs; + } + + Arcade::InputAction chooseGhostDirection(const Ghost &ghost) const { + std::vector dirs = validDirections(ghost); + + if (dirs.empty()) + return ghost.direction; + + if (dirs.size() > 1) { + Arcade::InputAction reverse = oppositeDirection(ghost.direction); + dirs.erase(std::remove(dirs.begin(), dirs.end(), reverse), dirs.end()); + + if (dirs.empty()) + dirs.push_back(reverse); + } + + Arcade::InputAction best = dirs.front(); + int bestScore = distanceToPacman(nextPosition(ghost.pos, best)); + + for (Arcade::InputAction dir : dirs) { + int score = distanceToPacman(nextPosition(ghost.pos, dir)); + + if (_frightenedSteps > 0) { + if (score > bestScore) { + best = dir; + bestScore = score; + } + } else if (score < bestScore) { + best = dir; + bestScore = score; + } + } + + return best; + } + void consumeTile() { char &tile = _map[static_cast(_pacman.y)][static_cast(_pacman.x)]; @@ -213,6 +291,7 @@ class PacmanModule : public Arcade::IGame { tile = ' '; _score += 50; --_pelletsLeft; + _frightenedSteps = kFrightenedDuration; } } @@ -234,6 +313,16 @@ class PacmanModule : public Arcade::IGame { startLevel(); } } + + void moveGhosts() { + for (Ghost &ghost : _ghosts) { + ghost.direction = chooseGhostDirection(ghost); + GridPos next = nextPosition(ghost.pos, ghost.direction); + + if (isWalkable(next)) + ghost.pos = next; + } + } }; } From b63adf1c4bb4a6e1fb2b6a0ad208190567ffd016 Mon Sep 17 00:00:00 2001 From: YetAnotherFactsEnjoyer Date: Sat, 11 Apr 2026 12:04:08 +0200 Subject: [PATCH 10/12] [FEAT] add ghost collisions, eating ghosts and game over --- src/games/Pacman/PacmanModule.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/games/Pacman/PacmanModule.cpp b/src/games/Pacman/PacmanModule.cpp index 360f4ee..03be0d6 100644 --- a/src/games/Pacman/PacmanModule.cpp +++ b/src/games/Pacman/PacmanModule.cpp @@ -51,7 +51,12 @@ class PacmanModule : public Arcade::IGame { return; _lastStep = now; movePacman(); + resolveCollisions(); + + if (_gameOver) + return; moveGhosts(); + resolveCollisions(); if (_frightenedSteps > 0) --_frightenedSteps; @@ -323,6 +328,21 @@ class PacmanModule : public Arcade::IGame { ghost.pos = next; } } + + void resolveCollisions() { + for (Ghost &ghost : _ghosts) { + if (!(ghost.pos == _pacman)) + continue; + if (_frightenedSteps > 0) { + _score += 200; + ghost.pos = ghost.start; + ghost.direction = Arcade::InputAction::Left; + } else { + _gameOver = true; + return; + } + } + } }; } From 03ab5f9862a260bfd00324d68c7b8244fe24cd68 Mon Sep 17 00:00:00 2001 From: YetAnotherFactsEnjoyer Date: Sat, 11 Apr 2026 12:19:05 +0200 Subject: [PATCH 11/12] [FEAT] correct map layout ghost spawning and frightened mode timing --- src/games/Pacman/PacmanModule.cpp | 303 ++++++++++++++++-------------- 1 file changed, 167 insertions(+), 136 deletions(-) diff --git a/src/games/Pacman/PacmanModule.cpp b/src/games/Pacman/PacmanModule.cpp index 03be0d6..281f6d6 100644 --- a/src/games/Pacman/PacmanModule.cpp +++ b/src/games/Pacman/PacmanModule.cpp @@ -1,12 +1,16 @@ #include "IGame.hpp" #include +#include #include #include #include #include namespace { +using Clock = std::chrono::steady_clock; +using namespace std::chrono_literals; + struct GridPos { int x; int y; @@ -20,6 +24,7 @@ struct Ghost { GridPos pos{}; GridPos start{}; Arcade::InputAction direction{Arcade::InputAction::Left}; + Clock::time_point releaseAt{}; }; class PacmanModule : public Arcade::IGame { @@ -34,10 +39,8 @@ class PacmanModule : public Arcade::IGame { _level = 1; _score = 0; _gameOver = false; - _frightenedSteps = 0; _playerDirection = Arcade::InputAction::Left; _requestedDirection = _playerDirection; - _lastStep = std::chrono::steady_clock::now(); startLevel(); } @@ -45,21 +48,29 @@ class PacmanModule : public Arcade::IGame { if (_gameOver) return; - auto now = std::chrono::steady_clock::now(); - - if (now - _lastStep < kStepDelay) - return; - _lastStep = now; - movePacman(); - resolveCollisions(); + auto now = Clock::now(); + auto playerDelay = levelDelay(160ms, 12ms); + auto ghostDelay = isFrightened() ? levelDelay(300ms, 6ms) : levelDelay(220ms, 8ms); - if (_gameOver) - return; - moveGhosts(); - resolveCollisions(); + if (now - _lastPlayerStep >= playerDelay) { + _lastPlayerStep = now; + movePacman(); + resolveCollisions(); + if (_gameOver) + return; + if (_pelletsLeft == 0) { + _score += 250; + ++_level; + startLevel(); + return; + } + } - if (_frightenedSteps > 0) - --_frightenedSteps; + if (now - _lastGhostStep >= ghostDelay) { + _lastGhostStep = now; + moveGhosts(); + resolveCollisions(); + } } void onInput(Arcade::InputAction action) override { @@ -77,32 +88,30 @@ class PacmanModule : public Arcade::IGame { std::vector getDisplay() const override { std::vector cells; + cells.reserve(static_cast(kMapWidth * kMapHeight + 200)); appendText(cells, 0, 0, "Pacman", 4); appendText(cells, 0, 1, "Score: " + std::to_string(_score) + " Level: " + std::to_string(_level), 3); - appendText(cells, 0, 2, _frightenedSteps > 0 ? "Power mode active" : "Arrows: move | R: restart | M: menu", 7); + appendText(cells, 0, 2, isFrightened() ? "Power mode active" : "Arrows move | m menu | r restart", 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, '#', 5)); else if (tile == '.') cells.push_back(makeCell(x, kTopOffset + y, '.', 4)); else if (tile == 'o') cells.push_back(makeCell(x, kTopOffset + y, 'o', 2)); - else - cells.push_back(makeCell(x, kTopOffset + y, ' ', 1)); } } + auto now = Clock::now(); for (const Ghost &ghost : _ghosts) { - char c = _frightenedSteps > 0 ? 'g' : 'G'; - int color = _frightenedSteps > 0 ? 6 : 2; - cells.push_back(makeCell(ghost.pos.x, kTopOffset + ghost.pos.y, c, color)); + if (now < ghost.releaseAt) + continue; + cells.push_back(makeCell(ghost.pos.x, kTopOffset + ghost.pos.y, isFrightened() ? 'g' : 'G', isFrightened() ? 6 : 2)); } - cells.push_back(makeCell(_pacman.x, kTopOffset + _pacman.y, 'C', 4)); if (_gameOver) @@ -123,8 +132,6 @@ class PacmanModule : public Arcade::IGame { static constexpr int kMapWidth = 19; static constexpr int kMapHeight = 11; static constexpr int kTopOffset = 4; - static constexpr auto kStepDelay = std::chrono::milliseconds(160); - static constexpr int kFrightenedDuration = 35; std::vector _map; GridPos _pacman{}; @@ -135,9 +142,10 @@ class PacmanModule : public Arcade::IGame { int _score{0}; int _level{1}; int _pelletsLeft{0}; - int _frightenedSteps{0}; bool _gameOver{false}; - std::chrono::steady_clock::time_point _lastStep{}; + Clock::time_point _frightenedUntil{}; + Clock::time_point _lastPlayerStep{}; + Clock::time_point _lastGhostStep{}; static Arcade::Cell makeCell(int x, int y, char character, int color) { return Arcade::Cell{static_cast(x), static_cast(y), character, color}; @@ -155,42 +163,66 @@ class PacmanModule : public Arcade::IGame { "#.###.##.#.##.###.#", "#.................#", "#.###.#.###.#.###.#", - "........#.#........", - "#.###.#.###.#.###.#", - "#.....#.....#.....#", + "o.......#.#.......o", + "#.###.#.....#.###.#", + "#.....#.. ..#.....#", "#.###.##.#.##.###.#", - "#o.................", + "#........#........#", "###################" }; } - 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; + 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{{9, 5}, {9, 5}, Arcade::InputAction::Left}, - Ghost{{8, 5}, {8, 5}, Arcade::InputAction::Right}, - Ghost{{10, 5}, {10, 5}, Arcade::InputAction::Up} + 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(); - _frightenedSteps = 0; - _lastStep = std::chrono::steady_clock::now(); + _frightenedUntil = Clock::time_point{}; + _lastPlayerStep = now; + _lastGhostStep = now; } - int wrapX(int x) const { + 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) @@ -198,145 +230,144 @@ class PacmanModule : public Arcade::IGame { return x; } - bool isWalkable(const GridPos &pos) const { - if (pos.y < 0 || pos.y >= kMapHeight || pos.x < 0 || pos.x >= kMapWidth) - return false; - return _map[static_cast(pos.y)][static_cast(pos.x)] != '#'; - } + GridPos stepFrom(const GridPos &start, Arcade::InputAction dir) const { + GridPos next = start; - GridPos nextPosition(const GridPos &from, Arcade::InputAction direction) const { - GridPos next = from; - - if (direction == Arcade::InputAction::Up) - --next.y; - else if (direction == Arcade::InputAction::Down) - ++next.y; - else if (direction == Arcade::InputAction::Left) - --next.x; - else if (direction == Arcade::InputAction::Right) - ++next.x; + 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; } - int distanceToPacman(const GridPos &pos) const { - return std::abs(pos.x - _pacman.x) + std::abs(pos.y - _pacman.y); + 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; + } } - Arcade::InputAction oppositeDirection(Arcade::InputAction direction) const { - if (direction == Arcade::InputAction::Up) - return Arcade::InputAction::Down; - if (direction == Arcade::InputAction::Down) - return Arcade::InputAction::Up; - if (direction == Arcade::InputAction::Left) - return Arcade::InputAction::Right; - return Arcade::InputAction::Left; + 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)); } - std::vector validDirections(const Ghost &ghost) const { + 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; - std::vector all = { + constexpr std::array allDirs = { Arcade::InputAction::Up, Arcade::InputAction::Down, Arcade::InputAction::Left, Arcade::InputAction::Right }; - for (Arcade::InputAction dir : all) { - GridPos next = nextPosition(ghost.pos, dir); - - if (isWalkable(next)) + for (Arcade::InputAction dir : allDirs) { + if (canMove(ghost.pos, dir)) dirs.push_back(dir); } return dirs; } - Arcade::InputAction chooseGhostDirection(const Ghost &ghost) const { - std::vector dirs = validDirections(ghost); - + Arcade::InputAction chooseGhostDirection(const Ghost &ghost) { + std::vector dirs = availableDirections(ghost); if (dirs.empty()) - return ghost.direction; + return Arcade::InputAction::None; + Arcade::InputAction reverse = opposite(ghost.direction); if (dirs.size() > 1) { - Arcade::InputAction reverse = oppositeDirection(ghost.direction); 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 = distanceToPacman(nextPosition(ghost.pos, best)); + 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) { - int score = distanceToPacman(nextPosition(ghost.pos, dir)); - - if (_frightenedSteps > 0) { - if (score > bestScore) { - best = dir; - bestScore = score; - } - } else if (score < bestScore) { + 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 = score; + bestScore = distance; } } - return best; } - void consumeTile() { - char &tile = _map[static_cast(_pacman.y)][static_cast(_pacman.x)]; - - if (tile == '.') { - tile = ' '; - _score += 10; - --_pelletsLeft; - } else if (tile == 'o') { - tile = ' '; - _score += 50; - --_pelletsLeft; - _frightenedSteps = kFrightenedDuration; - } - } - - void movePacman() { - GridPos requested = nextPosition(_pacman, _requestedDirection); - - if (isWalkable(requested)) - _playerDirection = _requestedDirection; - - GridPos next = nextPosition(_pacman, _playerDirection); - - if (!isWalkable(next)) - return; - _pacman = next; - consumeTile(); - - if (_pelletsLeft <= 0) { - ++_level; - startLevel(); - } - } - void moveGhosts() { - for (Ghost &ghost : _ghosts) { - ghost.direction = chooseGhostDirection(ghost); - GridPos next = nextPosition(ghost.pos, ghost.direction); + auto now = Clock::now(); - if (isWalkable(next)) - ghost.pos = next; + 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 (!(ghost.pos == _pacman)) + if (now < ghost.releaseAt || !(ghost.pos == _pacman)) continue; - if (_frightenedSteps > 0) { - _score += 200; + if (isFrightened()) { ghost.pos = ghost.start; ghost.direction = Arcade::InputAction::Left; + ghost.releaseAt = now + 1500ms; + _score += 200; } else { _gameOver = true; return; From 5f9af47743a11ec9b394c83b0ad08db87ca5ef18 Mon Sep 17 00:00:00 2001 From: YetAnotherMechanicusEnjoyer Date: Sun, 12 Apr 2026 12:58:30 +0200 Subject: [PATCH 12/12] [FIX] Colors in Pacman & Snake modules --- src/games/Pacman/PacmanModule.cpp | 27 +++++++++++++------------ src/games/Snake/SnakeModule.cpp | 33 ++++++++++++++++--------------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/games/Pacman/PacmanModule.cpp b/src/games/Pacman/PacmanModule.cpp index 281f6d6..53d4534 100644 --- a/src/games/Pacman/PacmanModule.cpp +++ b/src/games/Pacman/PacmanModule.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -90,19 +91,19 @@ class PacmanModule : public Arcade::IGame { std::vector cells; cells.reserve(static_cast(kMapWidth * kMapHeight + 200)); - appendText(cells, 0, 0, "Pacman", 4); - appendText(cells, 0, 1, "Score: " + std::to_string(_score) + " Level: " + std::to_string(_level), 3); - appendText(cells, 0, 2, isFrightened() ? "Power mode active" : "Arrows move | m menu | r restart", 7); + 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, '#', 5)); + cells.push_back(makeCell(x, kTopOffset + y, '#', 0, 5)); else if (tile == '.') - cells.push_back(makeCell(x, kTopOffset + y, '.', 4)); + cells.push_back(makeCell(x, kTopOffset + y, '.', 0, 4)); else if (tile == 'o') - cells.push_back(makeCell(x, kTopOffset + y, 'o', 2)); + cells.push_back(makeCell(x, kTopOffset + y, 'o', 0, 2)); } } @@ -110,12 +111,12 @@ class PacmanModule : public Arcade::IGame { for (const Ghost &ghost : _ghosts) { if (now < ghost.releaseAt) continue; - cells.push_back(makeCell(ghost.pos.x, kTopOffset + ghost.pos.y, isFrightened() ? 'g' : 'G', isFrightened() ? 6 : 2)); + 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', 4)); + 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", 2); + appendText(cells, 4, kTopOffset + kMapHeight / 2, "Game Over - press r to restart", 0, 2); return cells; } @@ -147,13 +148,13 @@ class PacmanModule : public Arcade::IGame { Clock::time_point _lastPlayerStep{}; Clock::time_point _lastGhostStep{}; - static Arcade::Cell makeCell(int x, int y, char character, int color) { - return Arcade::Cell{static_cast(x), static_cast(y), character, color}; + 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, int color) { + 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)); + cells.push_back(makeCell(x + static_cast(i), y, text[i], color, textColor)); } static std::vector baseMap() { diff --git a/src/games/Snake/SnakeModule.cpp b/src/games/Snake/SnakeModule.cpp index 08c4385..216184c 100644 --- a/src/games/Snake/SnakeModule.cpp +++ b/src/games/Snake/SnakeModule.cpp @@ -1,6 +1,7 @@ #include "IGame.hpp" #include +#include #include #include #include @@ -83,35 +84,35 @@ class SnakeModule : public Arcade::IGame { { std::vector cells; - appendText(cells, 0, 0, "Snake", 4); - appendText(cells, 0, 1, "Score: " + std::to_string(_score), 3); - appendText(cells, 0, 2, "Length: " + std::to_string(_snake.size()), 6); - appendText(cells, 0, 3, _gameOver ? "Game Over - press R to restart" : "Arrows: move | R: restart | M: menu", 7); + 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, '#', 5)); - cells.push_back(makeCell(x, kTopOffset + kBoardHeight - 1, '#', 5)); + 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, '#', 5)); - cells.push_back(makeCell(kBoardWidth - 1, kTopOffset + y, '#', 5)); + 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, '.', 1)); + cells.push_back(makeCell(x, kTopOffset + y, ' ', 0, 1)); } } - cells.push_back(makeCell(_food.x, kTopOffset + _food.y, '$', 4)); + 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', i == 0 ? 2 : 3)); + 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", 4); + appendText(cells, 6, kTopOffset + kBoardHeight / 2, "GAME OVER", 0, 4); } return cells; @@ -140,13 +141,13 @@ class SnakeModule : public Arcade::IGame { std::chrono::steady_clock::time_point _lastStep{}; mutable std::mt19937 _rng; - static Arcade::Cell makeCell(int x, int y, char character, int color) { - return Arcade::Cell{static_cast(x), static_cast(y), character, color}; + 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, int color) { + 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)); + cells.push_back(makeCell(x + static_cast(i), y, text[i], color, text_color)); } }