From aa22b91e8b4898183ca1228a260628e911f97928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20Waage?= Date: Mon, 6 Sep 2021 14:51:01 +0200 Subject: [PATCH 01/15] Adding header files to their own folder in the project. --- lib/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 394d3f5..9fb6f84 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -5,7 +5,7 @@ if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") find_package(OpenGL REQUIRED COMPONENTS OpenGL GLX) endif () -file(GLOB_RECURSE sources CONFIGURE_DEPENDS "*.cpp") +file(GLOB_RECURSE sources CONFIGURE_DEPENDS "*.cpp" "*.hpp") add_library(libpacman ${sources}) From fe32b18d0cee98cc3abc694bffba945ad1a93bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20Waage?= Date: Mon, 6 Sep 2021 15:38:40 +0200 Subject: [PATCH 02/15] Isolating SFML to the Canvas class. --- lib/include/Board.hpp | 2 -- lib/include/Canvas.hpp | 5 +++++ lib/include/Game.hpp | 2 +- lib/include/Position.hpp | 7 ------- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/include/Board.hpp b/lib/include/Board.hpp index 783a344..fe30302 100644 --- a/lib/include/Board.hpp +++ b/lib/include/Board.hpp @@ -1,7 +1,5 @@ #pragma once -#include - #include "Direction.hpp" #include "Position.hpp" #include diff --git a/lib/include/Canvas.hpp b/lib/include/Canvas.hpp index 04231c7..eb315b2 100644 --- a/lib/include/Canvas.hpp +++ b/lib/include/Canvas.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "GameState.hpp" #include "Position.hpp" #include "Score.hpp" @@ -7,6 +9,9 @@ namespace pacman { +using Rect = sf::Rect; +using Sprite = sf::Sprite; + class Canvas { public: Canvas(); diff --git a/lib/include/Game.hpp b/lib/include/Game.hpp index 74d7d48..db1e224 100644 --- a/lib/include/Game.hpp +++ b/lib/include/Game.hpp @@ -1,8 +1,8 @@ #pragma once -#include "Canvas.hpp" #include "GameState.hpp" #include "InputState.hpp" +#include "Canvas.hpp" namespace pacman { diff --git a/lib/include/Position.hpp b/lib/include/Position.hpp index c77bfc8..85c6563 100644 --- a/lib/include/Position.hpp +++ b/lib/include/Position.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include namespace pacman { @@ -16,10 +15,6 @@ struct GridPosition { constexpr GridPosition(size_t x, size_t y) : x(x), y(y) {} }; -using Rect = sf::Rect; - -using Sprite = sf::Sprite; - inline GridPosition positionToGridPosition(Position pos) { return { size_t(std::round(pos.x)), size_t(std::round(pos.y)) }; } @@ -44,6 +39,4 @@ constexpr bool operator!=(const Position & a, const Position & b) { return !(a == b); } - - } // namespace pacman From 27517cdfa6d1123483ee9c2779a6d8a47bb70985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20Waage?= Date: Mon, 6 Sep 2021 15:54:19 +0200 Subject: [PATCH 03/15] Seperating test files from the main test cpp file. --- test/CMakeLists.txt | 4 +++- test/testPacMan.cpp | 8 ++++++++ test/testPosition.cpp | 6 ++++++ test/tests.cpp | 7 ------- 4 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 test/testPacMan.cpp create mode 100644 test/testPosition.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 520bbd3..f52fc52 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,7 +3,9 @@ find_package(GTest REQUIRED) include(GoogleTest) -add_executable(pacman_tests tests.cpp) +file(GLOB_RECURSE sources CONFIGURE_DEPENDS "*.cpp") + +add_executable(pacman_tests ${sources}) target_link_libraries(pacman_tests GTest::GTest libpacman) gtest_discover_tests(pacman_tests TEST_PREFIX pacman:) diff --git a/test/testPacMan.cpp b/test/testPacMan.cpp new file mode 100644 index 0000000..573df68 --- /dev/null +++ b/test/testPacMan.cpp @@ -0,0 +1,8 @@ +#include "PacMan.hpp" +#include + +TEST(PacManTest, InitialPosition) { + pacman::PacMan pacMan; + EXPECT_EQ(pacMan.position().x, 13.5); + EXPECT_EQ(pacMan.position().y, 23); +} diff --git a/test/testPosition.cpp b/test/testPosition.cpp new file mode 100644 index 0000000..17b36dd --- /dev/null +++ b/test/testPosition.cpp @@ -0,0 +1,6 @@ +#include "Position.hpp" +#include + +TEST(PacManTest, Position) { + EXPECT_EQ(1, 1); +} diff --git a/test/tests.cpp b/test/tests.cpp index 2322486..e61612e 100644 --- a/test/tests.cpp +++ b/test/tests.cpp @@ -1,12 +1,5 @@ -#include "PacMan.hpp" #include -TEST(PacManTest, InitialPosition) { - pacman::PacMan pacMan; - EXPECT_EQ(pacMan.position().x, 13.5); - EXPECT_EQ(pacMan.position().y, 23); -} - int main(int argc, char * argv[]) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); From 4c47509d1bed40cca0a4977f8543638baa75e782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20Waage?= Date: Mon, 6 Sep 2021 16:00:48 +0200 Subject: [PATCH 04/15] Cleanup of how tests are discovered. Prefix not needed or used. --- test/CMakeLists.txt | 3 +-- test/testPosition.cpp | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f52fc52..21c8d26 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -8,5 +8,4 @@ file(GLOB_RECURSE sources CONFIGURE_DEPENDS "*.cpp") add_executable(pacman_tests ${sources}) target_link_libraries(pacman_tests GTest::GTest libpacman) -gtest_discover_tests(pacman_tests TEST_PREFIX pacman:) -add_test(NAME monolithic COMMAND pacman_tests) +gtest_discover_tests(pacman_tests) \ No newline at end of file diff --git a/test/testPosition.cpp b/test/testPosition.cpp index 17b36dd..d008af8 100644 --- a/test/testPosition.cpp +++ b/test/testPosition.cpp @@ -1,6 +1,6 @@ #include "Position.hpp" #include -TEST(PacManTest, Position) { +TEST(PositionTest, Init) { EXPECT_EQ(1, 1); } From 00bfd15074025dd6a1966a3f0ff54d912fd11a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20Waage?= Date: Mon, 6 Sep 2021 16:24:30 +0200 Subject: [PATCH 05/15] Adding epsilon test for Position. Also adding tests for GridPosition and Position. --- lib/include/Position.hpp | 8 +++++--- test/testPosition.cpp | 44 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/lib/include/Position.hpp b/lib/include/Position.hpp index 85c6563..a3f1a97 100644 --- a/lib/include/Position.hpp +++ b/lib/include/Position.hpp @@ -31,11 +31,13 @@ constexpr bool operator!=(const GridPosition & a, const GridPosition & b) { return !(a == b); } -constexpr bool operator==(const Position & a, const Position & b) { - return a.x == b.x && a.y == b.y; +inline bool operator==(const Position & a, const Position & b) { + // This is ok as a test unless x and y become very large. + constexpr double epsilon = std::numeric_limits::epsilon(); + return std::abs(a.x - b.x) <= epsilon && std::abs(a.y - b.y) <= epsilon; } -constexpr bool operator!=(const Position & a, const Position & b) { +inline bool operator!=(const Position & a, const Position & b) { return !(a == b); } diff --git a/test/testPosition.cpp b/test/testPosition.cpp index d008af8..fbc8503 100644 --- a/test/testPosition.cpp +++ b/test/testPosition.cpp @@ -1,6 +1,46 @@ #include "Position.hpp" #include -TEST(PositionTest, Init) { - EXPECT_EQ(1, 1); +TEST(PositionTest, PositionInit) { + pacman::Position pos; + EXPECT_DOUBLE_EQ(pos.x, 0.0); + EXPECT_DOUBLE_EQ(pos.y, 0.0); + + pacman::Position pos2{ 10.0, 20.0 }; + EXPECT_DOUBLE_EQ(pos2.x, 10.0); + EXPECT_DOUBLE_EQ(pos2.y, 20.0); +} + +TEST(PositionTest, GridPositionInit) { + pacman::GridPosition gridPos{ 10, 20 }; + EXPECT_EQ(gridPos.x, 10); + EXPECT_EQ(gridPos.y, 20); +} + +TEST(PositionTest, ConvertPositionToGridPosition) { + pacman::Position pos{ 10.0, 20.0 }; + const auto gridPos = pacman::positionToGridPosition(pos); + EXPECT_EQ(gridPos.x, 10); + EXPECT_EQ(gridPos.y, 20); +} + +TEST(PositionTest, ConvertGridPositionToPosition) { + pacman::GridPosition gridPos{ 10, 20 }; + const auto pos = pacman::gridPositionToPosition(gridPos); + EXPECT_DOUBLE_EQ(pos.x, 10.0); + EXPECT_DOUBLE_EQ(pos.y, 20.0); +} + +TEST(PositionTest, PositionEquality) { + pacman::Position pos1{ 10.0, 20.0 }; + pacman::Position pos2{ 10.0, 20.0 }; + EXPECT_TRUE(pos1 == pos2); + + pacman::Position pos3{ 9.9, 19.9 }; + EXPECT_FALSE(pos1 == pos3); + + pos3.x += 0.1; + pos3.y += 0.1; + + EXPECT_TRUE(pos1 == pos3); } From d0bd69aee1fdddb3a53717266eefb95ba0871f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20Waage?= Date: Mon, 6 Sep 2021 16:27:29 +0200 Subject: [PATCH 06/15] Removing unnecessary forward decl. --- lib/include/PacMan.hpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/include/PacMan.hpp b/lib/include/PacMan.hpp index 113b1d7..555df17 100644 --- a/lib/include/PacMan.hpp +++ b/lib/include/PacMan.hpp @@ -8,15 +8,10 @@ namespace pacman { -class Board; -class InputState; - class PacMan { public: GridPosition currentSprite() const; - Position position() const; - GridPosition positionInGrid() const; void update(std::chrono::milliseconds time_delta, Direction input_direction); From aa45121bec1d825960ce30c0bec5fefe10a4034f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20Waage?= Date: Mon, 6 Sep 2021 16:40:42 +0200 Subject: [PATCH 07/15] Minor formatting and warning fixes. --- lib/Ghost.cpp | 2 +- lib/PacMan.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/Ghost.cpp b/lib/Ghost.cpp index f7a5971..ea47fbe 100644 --- a/lib/Ghost.cpp +++ b/lib/Ghost.cpp @@ -184,7 +184,7 @@ void Ghost::updateDirection(const GameState & gameState) { return a.distance_to_target < b.distance_to_target; }); - auto move = *optimal_move; + const auto& move = *optimal_move; direction = move.direction; last_grid_position = current_grid_position; } diff --git a/lib/PacMan.cpp b/lib/PacMan.cpp index b08bdf9..f3739f4 100644 --- a/lib/PacMan.cpp +++ b/lib/PacMan.cpp @@ -18,6 +18,7 @@ GridPosition PacMan::positionInGrid() const { void PacMan::die() { if (dead) return; + dead = true; } @@ -33,8 +34,10 @@ void PacMan::update(std::chrono::milliseconds time_delta, Direction input_direct updateAnimationPosition(time_delta, false); return; } + if (input_direction != Direction::NONE) desired_direction = input_direction; + const auto old = pos; updateMazePosition(time_delta); const bool paused = pos == old; @@ -50,7 +53,6 @@ void PacMan::updateAnimationPosition(std::chrono::milliseconds time_delta, bool } void PacMan::updateMazePosition(std::chrono::milliseconds time_delta) { - if (isPortal(positionInGrid(), direction)) { pos = gridPositionToPosition(teleport(positionInGrid())); return; From cbe579859071af672d9ec85025da93dfd970b590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20Waage?= Date: Tue, 7 Sep 2021 11:25:45 +0200 Subject: [PATCH 08/15] Adding a couple of Ghost tests. --- test/testGhost.cpp | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 test/testGhost.cpp diff --git a/test/testGhost.cpp b/test/testGhost.cpp new file mode 100644 index 0000000..b841cd3 --- /dev/null +++ b/test/testGhost.cpp @@ -0,0 +1,54 @@ +#include "Blinky.hpp" +#include "Clyde.hpp" +#include "Inky.hpp" +#include "Pinky.hpp" +#include + +template +static void ghostInitHelper(const T& ghost, const double x, const double y) { + const pacman::Position pos{ x, y }; + EXPECT_EQ(ghost.position(), pos); + + const pacman::GridPosition gridPos = pacman::positionToGridPosition(pos); + EXPECT_EQ(ghost.positionInGrid(), gridPos); + + EXPECT_FALSE(ghost.isEyes()); + EXPECT_FALSE(ghost.isFrightened()); +} + +TEST(GhostTest, Init) { + pacman::Blinky blinky; + ghostInitHelper(blinky, 13.5, 11); + + pacman::Clyde clyde; + ghostInitHelper(clyde, 15.5, 14); + + pacman::Inky inky; + ghostInitHelper(inky, 13.5, 14); + + pacman::Pinky pinky; + ghostInitHelper(pinky, 11.5, 14); +} + +template +static void ghostFrightenHelper(T& ghost) { + EXPECT_FALSE(ghost.isFrightened()); + ghost.frighten(); + EXPECT_TRUE(ghost.isFrightened()); + ghost.reset(); + EXPECT_FALSE(ghost.isFrightened()); +} + +TEST(GhostTest, Frighten) { + pacman::Blinky blinky; + ghostFrightenHelper(blinky); + + pacman::Clyde clyde; + ghostFrightenHelper(clyde); + + pacman::Inky inky; + ghostFrightenHelper(inky); + + pacman::Pinky pinky; + ghostFrightenHelper(pinky); +} \ No newline at end of file From ee4b21605663eb95b0ea2be39128db4b75f460ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20Waage?= Date: Tue, 7 Sep 2021 15:30:48 +0200 Subject: [PATCH 09/15] Adding ghost dead test. --- test/testGhost.cpp | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/testGhost.cpp b/test/testGhost.cpp index b841cd3..9837ae2 100644 --- a/test/testGhost.cpp +++ b/test/testGhost.cpp @@ -51,4 +51,27 @@ TEST(GhostTest, Frighten) { pacman::Pinky pinky; ghostFrightenHelper(pinky); -} \ No newline at end of file +} + +template +static void ghostDeadHelper(T & ghost) { + EXPECT_FALSE(ghost.isEyes()); + ghost.die(); + EXPECT_TRUE(ghost.isEyes()); + ghost.reset(); + EXPECT_FALSE(ghost.isEyes()); +} + +TEST(GhostTest, Dead) { + pacman::Blinky blinky; + ghostDeadHelper(blinky); + + pacman::Clyde clyde; + ghostDeadHelper(clyde); + + pacman::Inky inky; + ghostDeadHelper(inky); + + pacman::Pinky pinky; + ghostDeadHelper(pinky); +} From 55fbb53591f1470313364e86ef083fab2d6d6044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20Waage?= Date: Tue, 7 Sep 2021 16:00:19 +0200 Subject: [PATCH 10/15] Refactoring Game and GameState. Moving game state update logic into that class. Game is now only event loop. --- lib/Canvas.cpp | 6 +-- lib/Game.cpp | 106 +++----------------------------------- lib/GameState.cpp | 89 ++++++++++++++++++++++++++++++++ lib/include/Canvas.hpp | 3 +- lib/include/Game.hpp | 9 ---- lib/include/GameState.hpp | 14 +++++ lib/include/Score.hpp | 4 +- test/testGame.cpp | 2 + 8 files changed, 119 insertions(+), 114 deletions(-) create mode 100644 lib/GameState.cpp create mode 100644 test/testGame.cpp diff --git a/lib/Canvas.cpp b/lib/Canvas.cpp index 431c371..b925a48 100644 --- a/lib/Canvas.cpp +++ b/lib/Canvas.cpp @@ -29,7 +29,7 @@ Canvas::Canvas() game_font = loadFont("retro_font.ttf"); } -void Canvas::update(const GameState & gameState, const Score & score) { +void Canvas::update(const GameState & gameState) { clear(); renderMaze(); @@ -41,8 +41,8 @@ void Canvas::update(const GameState & gameState, const Score & score) { renderGhost(gameState.inky); renderGhost(gameState.clyde); - renderScore(score.points); - renderLives(score.lives); + renderScore(gameState.score.points); + renderLives(gameState.score.lives); renderPacMan(gameState.pacMan); diff --git a/lib/Game.cpp b/lib/Game.cpp index 7d085ca..02a9d74 100644 --- a/lib/Game.cpp +++ b/lib/Game.cpp @@ -5,124 +5,32 @@ namespace pacman { -constexpr int DEFAULT_LIVES = 3; -constexpr int NORMAL_PELLET_POINTS = 10; -constexpr int POWER_PELLET_POINTS = 50; -constexpr int GHOST_POINTS = 200; - -Game::Game() { - score.lives = DEFAULT_LIVES; -} - void Game::run() { - const std::chrono::milliseconds delta_time(1000 / 60); std::chrono::milliseconds accumulator(0); auto current_time = std::chrono::system_clock::now(); - InputState inputState; - while (true) { auto newTime = std::chrono::system_clock::now(); auto frameTime = std::chrono::duration_cast(newTime - current_time); + current_time = newTime; accumulator += frameTime; - processEvents(inputState); - if (inputState.close) + + processEvents(gameState.inputState); + if (gameState.inputState.close) return; + while (accumulator >= delta_time) { - step(delta_time, inputState); + gameState.step(delta_time); accumulator -= delta_time; } - canvas.update(gameState, score); - } -} - -void Game::killPacMan() { - gameState.pacMan.die(); - score.lives--; - timeSinceDeath = std::chrono::milliseconds(1); -} - -bool Game::pacManDying() const { - return timeSinceDeath.count() != 0; -} - -void Game::handleDeathAnimation(std::chrono::milliseconds delta) { - timeSinceDeath += delta; - - if (timeSinceDeath.count() > 1000) { - gameState.blinky.reset(); - gameState.pinky.reset(); - gameState.inky.reset(); - gameState.clyde.reset(); - gameState.pacMan.reset(); - timeSinceDeath = std::chrono::milliseconds(0); - } -} - -void Game::step(std::chrono::milliseconds delta, InputState inputState) { - - gameState.pacMan.update(delta, inputState.direction()); - - if (pacManDying()) { - handleDeathAnimation(delta); - return; - } - - if (!gameState.pacMan.hasDirection()) - return; - - gameState.blinky.update(delta, gameState); - gameState.pinky.update(delta, gameState); - gameState.inky.update(delta, gameState); - gameState.clyde.update(delta, gameState); - - checkCollision(gameState.blinky); - checkCollision(gameState.pinky); - checkCollision(gameState.inky); - checkCollision(gameState.clyde); - - - eatPellets(); -} - -void Game::checkCollision(Ghost & ghost) { - if (pacManDying() || ghost.isEyes()) - return; - - if (ghost.positionInGrid() != gameState.pacMan.positionInGrid()) - return; - - if (ghost.isFrightened()) { - ghost.die(); - score.points += GHOST_POINTS; - } else { - killPacMan(); - } -} - -void Game::eatPellets() { - const auto pos = gameState.pacMan.positionInGrid(); - if (gameState.pellets.eatPelletAtPosition(pos)) { - score.eatenPellets++; - score.points += NORMAL_PELLET_POINTS; - } - - if (gameState.superPellets.eatPelletAtPosition(pos)) { - score.eatenPellets++; - score.points += POWER_PELLET_POINTS; - - gameState.blinky.frighten(); - gameState.pinky.frighten(); - gameState.inky.frighten(); - gameState.clyde.frighten(); + canvas.update(gameState); } } void Game::processEvents(InputState & inputState) { - auto event = canvas.pollEvent(); if (event && event.value().type == sf::Event::Closed) { inputState.close = true; diff --git a/lib/GameState.cpp b/lib/GameState.cpp new file mode 100644 index 0000000..ae5d930 --- /dev/null +++ b/lib/GameState.cpp @@ -0,0 +1,89 @@ +#include "GameState.hpp" + +namespace pacman { + +constexpr int GHOST_POINTS = 200; +constexpr int NORMAL_PELLET_POINTS = 10; +constexpr int POWER_PELLET_POINTS = 50; + +void GameState::step(std::chrono::milliseconds delta) { + pacMan.update(delta, inputState.direction()); + + if (isPacManDying()) { + handleDeathAnimation(delta); + return; + } + + if (!pacMan.hasDirection()) + return; + + blinky.update(delta, *this); // waage: urgh, I wanna remove this + pinky.update(delta, *this); // ghosts know what they want, which is usually pacman's location + inky.update(delta, *this); + clyde.update(delta, *this); + + checkCollision(blinky); + checkCollision(pinky); + checkCollision(inky); + checkCollision(clyde); + + eatPellets(); +} + +void GameState::checkCollision(Ghost & ghost) { + if (isPacManDying() || ghost.isEyes()) + return; + + if (ghost.positionInGrid() != pacMan.positionInGrid()) + return; + + if (ghost.isFrightened()) { + ghost.die(); + score.points += GHOST_POINTS; + } else { + killPacMan(); + } +} + +void GameState::handleDeathAnimation(std::chrono::milliseconds delta) { + timeSinceDeath += delta; + + if (timeSinceDeath.count() > 1000) { + blinky.reset(); + pinky.reset(); + inky.reset(); + clyde.reset(); + pacMan.reset(); + timeSinceDeath = std::chrono::milliseconds(0); + } +} + +void GameState::eatPellets() { + const auto pos = pacMan.positionInGrid(); + if (pellets.eatPelletAtPosition(pos)) { + score.eatenPellets++; + score.points += NORMAL_PELLET_POINTS; + } + + if (superPellets.eatPelletAtPosition(pos)) { + score.eatenPellets++; + score.points += POWER_PELLET_POINTS; + + blinky.frighten(); + pinky.frighten(); + inky.frighten(); + clyde.frighten(); + } +} + +void GameState::killPacMan() { + pacMan.die(); + score.lives--; + timeSinceDeath = std::chrono::milliseconds(1); +} + +bool GameState::isPacManDying() const { + return timeSinceDeath.count() != 0; +} + +} \ No newline at end of file diff --git a/lib/include/Canvas.hpp b/lib/include/Canvas.hpp index eb315b2..3dc529e 100644 --- a/lib/include/Canvas.hpp +++ b/lib/include/Canvas.hpp @@ -4,7 +4,6 @@ #include "GameState.hpp" #include "Position.hpp" -#include "Score.hpp" #include namespace pacman { @@ -15,7 +14,7 @@ using Sprite = sf::Sprite; class Canvas { public: Canvas(); - void update(const GameState & gameState, const Score & score); + void update(const GameState & gameState); std::optional pollEvent(); private: diff --git a/lib/include/Game.hpp b/lib/include/Game.hpp index db1e224..c6b9b78 100644 --- a/lib/include/Game.hpp +++ b/lib/include/Game.hpp @@ -8,22 +8,13 @@ namespace pacman { class Game { public: - Game(); void run(); private: Canvas canvas; GameState gameState; - Score score; - std::chrono::milliseconds timeSinceDeath{}; - void step(std::chrono::milliseconds delta, InputState inputState); - void eatPellets(); void processEvents(InputState & inputState); - void checkCollision(Ghost & ghost); - void killPacMan(); - bool pacManDying() const; - void handleDeathAnimation(std::chrono::milliseconds delta); }; } // namespace pacman diff --git a/lib/include/GameState.hpp b/lib/include/GameState.hpp index 8a1aa88..794ca48 100644 --- a/lib/include/GameState.hpp +++ b/lib/include/GameState.hpp @@ -9,17 +9,31 @@ #include "Pinky.hpp" #include "Score.hpp" #include "SuperPellets.hpp" +#include "InputState.hpp" namespace pacman { struct GameState { + void step(std::chrono::milliseconds delta); + Blinky blinky; Pinky pinky; Inky inky; Clyde clyde; + PacMan pacMan; + InputState inputState; Pellets pellets; SuperPellets superPellets; + + Score score; + std::chrono::milliseconds timeSinceDeath{}; + + void checkCollision(Ghost & ghost); + void handleDeathAnimation(std::chrono::milliseconds delta); + void eatPellets(); + void killPacMan(); + bool isPacManDying() const; }; } // namespace pacman diff --git a/lib/include/Score.hpp b/lib/include/Score.hpp index 1978fa5..3ef475e 100644 --- a/lib/include/Score.hpp +++ b/lib/include/Score.hpp @@ -1,8 +1,10 @@ #pragma once namespace pacman { +constexpr int DEFAULT_LIVES = 3; + struct Score { - int lives = 0; + int lives = DEFAULT_LIVES; int points = 0; int eatenPellets = 0; }; diff --git a/test/testGame.cpp b/test/testGame.cpp new file mode 100644 index 0000000..74c51ca --- /dev/null +++ b/test/testGame.cpp @@ -0,0 +1,2 @@ +#include "PacMan.hpp" +#include From b79b2a29e81b087b6609c79fbffead0a275449bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20Waage?= Date: Wed, 8 Sep 2021 10:21:37 +0200 Subject: [PATCH 11/15] Using std::erase for pellets possible in C++20 --- lib/Pellets.cpp | 6 +----- lib/SuperPellets.cpp | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/Pellets.cpp b/lib/Pellets.cpp index 39de56d..566b227 100644 --- a/lib/Pellets.cpp +++ b/lib/Pellets.cpp @@ -7,11 +7,7 @@ Pellets::Pellets() : positions(initialPelletPositions()) {} bool Pellets::eatPelletAtPosition(GridPosition p) { - auto it = std::find(positions.begin(), positions.end(), p); - if (it == positions.end()) - return false; - positions.erase(it); - return true; + return std::erase(positions, p) > 0; } } // namespace pacman diff --git a/lib/SuperPellets.cpp b/lib/SuperPellets.cpp index 0e851e6..9fae338 100644 --- a/lib/SuperPellets.cpp +++ b/lib/SuperPellets.cpp @@ -7,11 +7,7 @@ SuperPellets::SuperPellets() : positions(initialSuperPelletPositions()) {} bool SuperPellets::eatPelletAtPosition(GridPosition p) { - auto it = std::find(positions.begin(), positions.end(), p); - if (it == positions.end()) - return false; - positions.erase(it); - return true; + return std::erase(positions, p) > 0; } } // namespace pacman From 60b3cdeb4020f6234d840fb53fedcbb8c708ed80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20Waage?= Date: Wed, 8 Sep 2021 14:49:43 +0200 Subject: [PATCH 12/15] Formatting and const fixes. Also check for extra canGo calls. --- lib/Game.cpp | 6 +++--- lib/Ghost.cpp | 6 +++++- lib/PacMan.cpp | 47 +++++++++++++++++++++++-------------------- lib/include/Ghost.hpp | 15 +++++++------- 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/lib/Game.cpp b/lib/Game.cpp index 02a9d74..0499c58 100644 --- a/lib/Game.cpp +++ b/lib/Game.cpp @@ -11,8 +11,8 @@ void Game::run() { auto current_time = std::chrono::system_clock::now(); while (true) { - auto newTime = std::chrono::system_clock::now(); - auto frameTime = std::chrono::duration_cast(newTime - current_time); + const auto newTime = std::chrono::system_clock::now(); + const auto frameTime = std::chrono::duration_cast(newTime - current_time); current_time = newTime; accumulator += frameTime; @@ -31,7 +31,7 @@ void Game::run() { } void Game::processEvents(InputState & inputState) { - auto event = canvas.pollEvent(); + const auto event = canvas.pollEvent(); if (event && event.value().type == sf::Event::Closed) { inputState.close = true; return; diff --git a/lib/Ghost.cpp b/lib/Ghost.cpp index ea47fbe..4dfaf59 100644 --- a/lib/Ghost.cpp +++ b/lib/Ghost.cpp @@ -75,7 +75,7 @@ void Ghost::update(std::chrono::milliseconds time_delta, const GameState & gameS if (state == State::Scatter || state == State::Chase) { timeChase += time_delta; - auto newState = defaultStateAtDuration(std::chrono::duration_cast(timeChase)); + const auto newState = defaultStateAtDuration(std::chrono::duration_cast(timeChase)); if (newState != state) { direction = oppositeDirection(direction); state = newState; @@ -204,13 +204,17 @@ void Ghost::updateAnimation(std::chrono::milliseconds time_delta) { Ghost::State Ghost::defaultStateAtDuration(std::chrono::seconds seconds) { // This array denotes the duration of each state, alternating between scatter and chase std::array changes = { /*scatter*/ 7, 20, 7, 20, 5, 20, 5 }; + // To know the current state we first compute the cumulative time using std::partial_sum // This gives us {7, 27, 34, 54, 59, 79, 84} std::partial_sum(std::begin(changes), std::end(changes), std::begin(changes)); + // Then we look for the first value in the array greater than the time spent in chase/scatter states auto it = std::upper_bound(std::begin(changes), std::end(changes), seconds.count()); + // We get the position of that iterator in the array auto count = std::distance(std::begin(changes), it); + // Because the first positition is scatter, all the even positions will be scatter // all the odd positions will be chase return count % 2 == 0 ? State::Scatter : State::Chase; diff --git a/lib/PacMan.cpp b/lib/PacMan.cpp index f3739f4..25d1b89 100644 --- a/lib/PacMan.cpp +++ b/lib/PacMan.cpp @@ -81,31 +81,34 @@ void PacMan::updateMazePosition(std::chrono::milliseconds time_delta) { return isWalkableForPacMan(moveToPosition(pos, move_direction)); }; - if (canGo(desired_direction)) { + if (desired_direction != direction && canGo(desired_direction)) { direction = desired_direction; } - if (canGo(direction)) { - switch (direction) { - case Direction::NONE: - break; - case Direction::LEFT: - pos.x -= position_delta; - pos.y = std::floor(pos.y); - break; - case Direction::RIGHT: - pos.x += position_delta; - pos.y = std::floor(pos.y); - break; - case Direction::UP: - pos.x = std::floor(pos.x); - pos.y -= position_delta; - break; - case Direction::DOWN: - pos.x = std::floor(pos.x); - pos.y += position_delta; - break; - } + if (!canGo(direction)) { + return; + } + + switch (direction) { + case Direction::LEFT: + pos.x -= position_delta; + pos.y = std::floor(pos.y); + break; + case Direction::RIGHT: + pos.x += position_delta; + pos.y = std::floor(pos.y); + break; + case Direction::UP: + pos.x = std::floor(pos.x); + pos.y -= position_delta; + break; + case Direction::DOWN: + pos.x = std::floor(pos.x); + pos.y += position_delta; + break; + case Direction::NONE: + default: + break; } } diff --git a/lib/include/Ghost.hpp b/lib/include/Ghost.hpp index 982c62e..4d473e6 100644 --- a/lib/include/Ghost.hpp +++ b/lib/include/Ghost.hpp @@ -23,9 +23,7 @@ public: virtual ~Ghost() = default; GridPosition currentSprite() const; - Position position() const; - GridPosition positionInGrid() const; void update(std::chrono::milliseconds time_delta, const GameState & gameState); @@ -41,12 +39,6 @@ private: void updateDirection(const GameState & gameState); protected: - State defaultStateAtDuration(std::chrono::seconds seconds); - - virtual double speed(const GameState & gameState) const = 0; - virtual Position target(const GameState & gameState) const = 0; - virtual Position initialPosition() const = 0; - Atlas::Ghost spriteSet; Direction direction = Direction::NONE; double timeForAnimation = 0; @@ -56,6 +48,13 @@ protected: std::chrono::milliseconds timeChase = {}; Position pos; GridPosition last_grid_position = { 0, 0 }; + + State defaultStateAtDuration(std::chrono::seconds seconds); + + virtual double speed(const GameState & gameState) const = 0; + virtual Position target(const GameState & gameState) const = 0; + virtual Position initialPosition() const = 0; + bool isInPen() const; }; From 613831f1c02b1ac3612c80a88e80b7f05e030e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20Waage?= Date: Thu, 9 Sep 2021 10:56:29 +0200 Subject: [PATCH 13/15] Fixes for ghost movement where they would occationally exit the stage when cornered. Closes #25 --- lib/Board.cpp | 6 +++--- lib/Ghost.cpp | 12 +++++++++--- lib/Inky.cpp | 4 ++-- lib/PacMan.cpp | 8 ++++---- lib/PacManAnimation.cpp | 4 +--- lib/include/Atlas.hpp | 4 ++-- lib/include/Board.hpp | 7 +++---- lib/include/PacManAnimation.hpp | 2 +- lib/include/Position.hpp | 8 ++++---- 9 files changed, 29 insertions(+), 26 deletions(-) diff --git a/lib/Board.cpp b/lib/Board.cpp index db04da9..0de0e91 100644 --- a/lib/Board.cpp +++ b/lib/Board.cpp @@ -54,7 +54,7 @@ constexpr std::array, ROWS> board = {{ // clang-format on static Cell cellAtPosition(GridPosition point) { - if (point.x >= COLUMNS || point.y >= ROWS) + if (point.x < 0 || point.x >= COLUMNS || point.y < 0 || point.y >= ROWS) return Cell::wall; return Cell(board[point.y][point.x]); } @@ -64,10 +64,10 @@ bool isWalkableForPacMan(GridPosition point) { } bool isWalkableForGhost(GridPosition point, GridPosition origin, bool isEyes) { - Cell cell = cellAtPosition(point); + const Cell cell = cellAtPosition(point); if (cell == Cell::wall) return false; - return isEyes || (isInPen(origin) || !isInPen(point)); + return isEyes || isInPen(origin) || !isInPen(point); } bool isInPen(GridPosition point) { diff --git a/lib/Ghost.cpp b/lib/Ghost.cpp index 4dfaf59..68f4eec 100644 --- a/lib/Ghost.cpp +++ b/lib/Ghost.cpp @@ -95,6 +95,9 @@ void Ghost::updatePosition(std::chrono::milliseconds time_delta, const GameState double position_delta = (0.004 * time_delta.count()) * speed(gameState); + const auto old_position = pos; + const GridPosition old_grid_position = positionToGridPosition(old_position); + switch (direction) { case Direction::NONE: break; @@ -119,6 +122,10 @@ void Ghost::updatePosition(std::chrono::milliseconds time_delta, const GameState if (isPortal(positionInGrid(), direction)) { pos = gridPositionToPosition(teleport(positionInGrid())); } + else if (!isWalkableForGhost(positionInGrid(), old_grid_position, isEyes())) { + pos = old_position; + direction = oppositeDirection(direction); + } } /* @@ -160,7 +167,6 @@ void Ghost::updateDirection(const GameState & gameState) { const Position target_position = target(gameState); for (auto & move : possible_moves) { - if (isPortal(current_grid_position, move.direction)) move.position = gridPositionToPosition(teleport(current_grid_position)); @@ -172,7 +178,7 @@ void Ghost::updateDirection(const GameState & gameState) { if (opposite_direction) continue; - const GridPosition grid_position = { size_t(move.position.x), size_t(move.position.y) }; + const GridPosition grid_position = { int64_t(move.position.x), int64_t(move.position.y) }; const bool can_walk = isWalkableForGhost(grid_position, current_grid_position, isEyes()); if (!can_walk) continue; @@ -184,7 +190,7 @@ void Ghost::updateDirection(const GameState & gameState) { return a.distance_to_target < b.distance_to_target; }); - const auto& move = *optimal_move; + const auto & move = *optimal_move; direction = move.direction; last_grid_position = current_grid_position; } diff --git a/lib/Inky.cpp b/lib/Inky.cpp index 6b258ac..ab8d268 100644 --- a/lib/Inky.cpp +++ b/lib/Inky.cpp @@ -53,8 +53,8 @@ Position Inky::target(const GameState & gameState) const { // And selects a point on the line crossing blinky and this position that is at twice that distance // away from blinky - targetPosition.x += size_t((targetPosition.x - blinkyPosition.x) / distanceBetweenBlinkyAndTarget) * 2; - targetPosition.y += size_t((targetPosition.y - blinkyPosition.y) / distanceBetweenBlinkyAndTarget) * 2; + targetPosition.x += int64_t((targetPosition.x - blinkyPosition.x) / distanceBetweenBlinkyAndTarget) * 2; + targetPosition.y += int64_t((targetPosition.y - blinkyPosition.y) / distanceBetweenBlinkyAndTarget) * 2; return gridPositionToPosition(targetPosition); } diff --git a/lib/PacMan.cpp b/lib/PacMan.cpp index 25d1b89..a78ea5f 100644 --- a/lib/PacMan.cpp +++ b/lib/PacMan.cpp @@ -64,13 +64,13 @@ void PacMan::updateMazePosition(std::chrono::milliseconds time_delta) { auto moveToPosition = [position_delta](Position point, Direction move_direction) { switch (move_direction) { case Direction::LEFT: - return GridPosition{ std::size_t(point.x - position_delta), std::size_t(point.y) }; + return GridPosition{ int64_t(point.x - position_delta), int64_t(point.y) }; case Direction::RIGHT: - return GridPosition{ std::size_t(point.x + pacman_size), std::size_t(point.y) }; + return GridPosition{ int64_t(point.x + pacman_size), int64_t(point.y) }; case Direction::UP: - return GridPosition{ std::size_t(point.x), std::size_t(point.y - position_delta) }; + return GridPosition{ int64_t(point.x), int64_t(point.y - position_delta) }; case Direction::DOWN: - return GridPosition{ std::size_t(point.x), std::size_t(point.y + pacman_size) }; + return GridPosition{ int64_t(point.x), int64_t(point.y + pacman_size) }; case Direction::NONE: default: return positionToGridPosition(point); diff --git a/lib/PacManAnimation.cpp b/lib/PacManAnimation.cpp index ea67a25..37e2b5e 100644 --- a/lib/PacManAnimation.cpp +++ b/lib/PacManAnimation.cpp @@ -32,15 +32,13 @@ void PacManAnimation::updateAnimationPosition(std::chrono::milliseconds time_del return; animation_position_delta += (0.02) * double(time_delta.count()); - - animation_position = int(animation_position + animation_position_delta); + animation_position = int64_t(animation_position + animation_position_delta); if (!dead) animation_position = animation_position % 4; if(animation_position_delta > 1) animation_position_delta = animation_position_delta - 1; - } void PacManAnimation::pause() { diff --git a/lib/include/Atlas.hpp b/lib/include/Atlas.hpp index 530d885..d5226f3 100644 --- a/lib/include/Atlas.hpp +++ b/lib/include/Atlas.hpp @@ -47,8 +47,8 @@ constexpr GridPosition eyeSprite(Direction direction) { constexpr GridPosition ghostSprite(Ghost ghost, Direction direction, bool alternative) { assert(ghost >= Ghost::blinky && ghost <= Ghost::clyde && "Invalid Ghost"); - auto y = static_cast(ghost); - size_t x = 0; + auto y = static_cast(ghost); + int64_t x = 0; switch (direction) { case Direction::RIGHT: x = 0; diff --git a/lib/include/Board.hpp b/lib/include/Board.hpp index fe30302..d3fc6ec 100644 --- a/lib/include/Board.hpp +++ b/lib/include/Board.hpp @@ -18,8 +18,7 @@ std::vector initialPelletPositions(); std::vector initialSuperPelletPositions(); -inline Position penDoorPosition() { - return { 13, 11 }; } - inline Position initialPacManPosition() { return { 13.5, 23 }; } +inline Position penDoorPosition() { return { 13, 11 }; } +inline Position initialPacManPosition() { return { 13.5, 23 }; } - } // namespace pacman +} // namespace pacman diff --git a/lib/include/PacManAnimation.hpp b/lib/include/PacManAnimation.hpp index 42dcde0..4c4e520 100644 --- a/lib/include/PacManAnimation.hpp +++ b/lib/include/PacManAnimation.hpp @@ -18,7 +18,7 @@ public: void pause(); private: - size_t animation_position = 0; + int64_t animation_position = 0; double animation_position_delta = 0.0; }; diff --git a/lib/include/Position.hpp b/lib/include/Position.hpp index a3f1a97..40eb798 100644 --- a/lib/include/Position.hpp +++ b/lib/include/Position.hpp @@ -10,13 +10,13 @@ struct Position { }; struct GridPosition { - size_t x; - size_t y; - constexpr GridPosition(size_t x, size_t y) : x(x), y(y) {} + int64_t x; + int64_t y; + constexpr GridPosition(int64_t x, int64_t y) : x(x), y(y) {} }; inline GridPosition positionToGridPosition(Position pos) { - return { size_t(std::round(pos.x)), size_t(std::round(pos.y)) }; + return { int64_t(std::round(pos.x)), int64_t(std::round(pos.y)) }; } inline Position gridPositionToPosition(GridPosition pos) { From 2f62f7ae1d27657a5d27f1dc16c45ee22583f58a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20Waage?= Date: Thu, 9 Sep 2021 10:59:59 +0200 Subject: [PATCH 14/15] Reverting pellet C++20 changes since we don't target 20 fully. --- lib/Pellets.cpp | 6 +++++- lib/SuperPellets.cpp | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/Pellets.cpp b/lib/Pellets.cpp index 566b227..39de56d 100644 --- a/lib/Pellets.cpp +++ b/lib/Pellets.cpp @@ -7,7 +7,11 @@ Pellets::Pellets() : positions(initialPelletPositions()) {} bool Pellets::eatPelletAtPosition(GridPosition p) { - return std::erase(positions, p) > 0; + auto it = std::find(positions.begin(), positions.end(), p); + if (it == positions.end()) + return false; + positions.erase(it); + return true; } } // namespace pacman diff --git a/lib/SuperPellets.cpp b/lib/SuperPellets.cpp index 9fae338..0e851e6 100644 --- a/lib/SuperPellets.cpp +++ b/lib/SuperPellets.cpp @@ -7,7 +7,11 @@ SuperPellets::SuperPellets() : positions(initialSuperPelletPositions()) {} bool SuperPellets::eatPelletAtPosition(GridPosition p) { - return std::erase(positions, p) > 0; + auto it = std::find(positions.begin(), positions.end(), p); + if (it == positions.end()) + return false; + positions.erase(it); + return true; } } // namespace pacman From 1ad8a1ec8e52fcd519fbfbcfc45a481bf250546c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=93lafur=20Waage?= Date: Thu, 9 Sep 2021 11:15:31 +0200 Subject: [PATCH 15/15] Adding simple deterministic fuzz test for GameState. --- test/testGame.cpp | 2 -- test/testGameState.cpp | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) delete mode 100644 test/testGame.cpp create mode 100644 test/testGameState.cpp diff --git a/test/testGame.cpp b/test/testGame.cpp deleted file mode 100644 index 74c51ca..0000000 --- a/test/testGame.cpp +++ /dev/null @@ -1,2 +0,0 @@ -#include "PacMan.hpp" -#include diff --git a/test/testGameState.cpp b/test/testGameState.cpp new file mode 100644 index 0000000..618c0e6 --- /dev/null +++ b/test/testGameState.cpp @@ -0,0 +1,26 @@ +#include "GameState.hpp" +#include +#include + +TEST(GameStateTest, Fuzz) { + pacman::GameState gameState; + //fmt::print("{}\n", gameState.pellets.currentPositions().size()); + + int pacManDeathCount = 0; + bool canCountDeath = false; + for (std::size_t i = 0; i < 50000; ++i) { + gameState.inputState.up = i % 7 ? true : false; + gameState.inputState.down = i % 11 ? true : false; + gameState.inputState.left = i % 13 ? true : false; + gameState.inputState.right = i % 17 ? true : false; + + canCountDeath = !gameState.isPacManDying(); + gameState.step(std::chrono::milliseconds(1000 / 30)); + if (canCountDeath && gameState.isPacManDying()) { + pacManDeathCount++; + } + } + + //fmt::print("{}\n", pacManDeathCount); + //fmt::print("{}\n", gameState.pellets.currentPositions().size()); +} \ No newline at end of file