Merge pull request #26 from mod-cpp/owaage_test_refactor

Testing and Code refactor.
This commit is contained in:
Ólafur Waage 2021-09-10 10:23:17 +02:00 committed by GitHub
commit 434875e344
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 362 additions and 204 deletions

View File

@ -54,7 +54,7 @@ constexpr std::array<std::array<int, COLUMNS>, ROWS> board = {{
// clang-format on // clang-format on
static Cell cellAtPosition(GridPosition point) { 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::wall;
return Cell(board[point.y][point.x]); return Cell(board[point.y][point.x]);
} }
@ -64,10 +64,10 @@ bool isWalkableForPacMan(GridPosition point) {
} }
bool isWalkableForGhost(GridPosition point, GridPosition origin, bool isEyes) { bool isWalkableForGhost(GridPosition point, GridPosition origin, bool isEyes) {
Cell cell = cellAtPosition(point); const Cell cell = cellAtPosition(point);
if (cell == Cell::wall) if (cell == Cell::wall)
return false; return false;
return isEyes || (isInPen(origin) || !isInPen(point)); return isEyes || isInPen(origin) || !isInPen(point);
} }
bool isInPen(GridPosition point) { bool isInPen(GridPosition point) {

View File

@ -5,7 +5,7 @@ if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
find_package(OpenGL REQUIRED COMPONENTS OpenGL GLX) find_package(OpenGL REQUIRED COMPONENTS OpenGL GLX)
endif () endif ()
file(GLOB_RECURSE sources CONFIGURE_DEPENDS "*.cpp") file(GLOB_RECURSE sources CONFIGURE_DEPENDS "*.cpp" "*.hpp")
add_library(libpacman ${sources}) add_library(libpacman ${sources})

View File

@ -29,7 +29,7 @@ Canvas::Canvas()
game_font = loadFont("retro_font.ttf"); game_font = loadFont("retro_font.ttf");
} }
void Canvas::update(const GameState & gameState, const Score & score) { void Canvas::update(const GameState & gameState) {
clear(); clear();
renderMaze(); renderMaze();
@ -41,8 +41,8 @@ void Canvas::update(const GameState & gameState, const Score & score) {
renderGhost(gameState.inky); renderGhost(gameState.inky);
renderGhost(gameState.clyde); renderGhost(gameState.clyde);
renderScore(score.points); renderScore(gameState.score.points);
renderLives(score.lives); renderLives(gameState.score.lives);
renderPacMan(gameState.pacMan); renderPacMan(gameState.pacMan);

View File

@ -5,125 +5,33 @@
namespace pacman { 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() { void Game::run() {
const std::chrono::milliseconds delta_time(1000 / 60); const std::chrono::milliseconds delta_time(1000 / 60);
std::chrono::milliseconds accumulator(0); std::chrono::milliseconds accumulator(0);
auto current_time = std::chrono::system_clock::now(); auto current_time = std::chrono::system_clock::now();
InputState inputState;
while (true) { while (true) {
auto newTime = std::chrono::system_clock::now(); const auto newTime = std::chrono::system_clock::now();
auto frameTime = std::chrono::duration_cast<std::chrono::milliseconds>(newTime - current_time); const auto frameTime = std::chrono::duration_cast<std::chrono::milliseconds>(newTime - current_time);
current_time = newTime; current_time = newTime;
accumulator += frameTime; accumulator += frameTime;
processEvents(inputState);
if (inputState.close) processEvents(gameState.inputState);
if (gameState.inputState.close)
return; return;
while (accumulator >= delta_time) { while (accumulator >= delta_time) {
step(delta_time, inputState); gameState.step(delta_time);
accumulator -= 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) { void Game::processEvents(InputState & inputState) {
const auto event = canvas.pollEvent();
auto event = canvas.pollEvent();
if (event && event.value().type == sf::Event::Closed) { if (event && event.value().type == sf::Event::Closed) {
inputState.close = true; inputState.close = true;
return; return;

89
lib/GameState.cpp Normal file
View File

@ -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;
}
}

View File

@ -75,7 +75,7 @@ void Ghost::update(std::chrono::milliseconds time_delta, const GameState & gameS
if (state == State::Scatter || state == State::Chase) { if (state == State::Scatter || state == State::Chase) {
timeChase += time_delta; timeChase += time_delta;
auto newState = defaultStateAtDuration(std::chrono::duration_cast<std::chrono::seconds>(timeChase)); const auto newState = defaultStateAtDuration(std::chrono::duration_cast<std::chrono::seconds>(timeChase));
if (newState != state) { if (newState != state) {
direction = oppositeDirection(direction); direction = oppositeDirection(direction);
state = newState; state = newState;
@ -95,6 +95,9 @@ void Ghost::updatePosition(std::chrono::milliseconds time_delta, const GameState
double position_delta = (0.004 * time_delta.count()) * speed(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) { switch (direction) {
case Direction::NONE: case Direction::NONE:
break; break;
@ -119,6 +122,10 @@ void Ghost::updatePosition(std::chrono::milliseconds time_delta, const GameState
if (isPortal(positionInGrid(), direction)) { if (isPortal(positionInGrid(), direction)) {
pos = gridPositionToPosition(teleport(positionInGrid())); 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); const Position target_position = target(gameState);
for (auto & move : possible_moves) { for (auto & move : possible_moves) {
if (isPortal(current_grid_position, move.direction)) if (isPortal(current_grid_position, move.direction))
move.position = gridPositionToPosition(teleport(current_grid_position)); move.position = gridPositionToPosition(teleport(current_grid_position));
@ -172,7 +178,7 @@ void Ghost::updateDirection(const GameState & gameState) {
if (opposite_direction) if (opposite_direction)
continue; 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()); const bool can_walk = isWalkableForGhost(grid_position, current_grid_position, isEyes());
if (!can_walk) if (!can_walk)
continue; continue;
@ -184,7 +190,7 @@ void Ghost::updateDirection(const GameState & gameState) {
return a.distance_to_target < b.distance_to_target; return a.distance_to_target < b.distance_to_target;
}); });
auto move = *optimal_move; const auto & move = *optimal_move;
direction = move.direction; direction = move.direction;
last_grid_position = current_grid_position; last_grid_position = current_grid_position;
} }
@ -204,13 +210,17 @@ void Ghost::updateAnimation(std::chrono::milliseconds time_delta) {
Ghost::State Ghost::defaultStateAtDuration(std::chrono::seconds seconds) { Ghost::State Ghost::defaultStateAtDuration(std::chrono::seconds seconds) {
// This array denotes the duration of each state, alternating between scatter and chase // This array denotes the duration of each state, alternating between scatter and chase
std::array changes = { /*scatter*/ 7, 20, 7, 20, 5, 20, 5 }; 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 // 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} // This gives us {7, 27, 34, 54, 59, 79, 84}
std::partial_sum(std::begin(changes), std::end(changes), std::begin(changes)); 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 // 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()); auto it = std::upper_bound(std::begin(changes), std::end(changes), seconds.count());
// We get the position of that iterator in the array // We get the position of that iterator in the array
auto count = std::distance(std::begin(changes), it); auto count = std::distance(std::begin(changes), it);
// Because the first positition is scatter, all the even positions will be scatter // Because the first positition is scatter, all the even positions will be scatter
// all the odd positions will be chase // all the odd positions will be chase
return count % 2 == 0 ? State::Scatter : State::Chase; return count % 2 == 0 ? State::Scatter : State::Chase;

View File

@ -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 // And selects a point on the line crossing blinky and this position that is at twice that distance
// away from blinky // away from blinky
targetPosition.x += size_t((targetPosition.x - blinkyPosition.x) / distanceBetweenBlinkyAndTarget) * 2; targetPosition.x += int64_t((targetPosition.x - blinkyPosition.x) / distanceBetweenBlinkyAndTarget) * 2;
targetPosition.y += size_t((targetPosition.y - blinkyPosition.y) / distanceBetweenBlinkyAndTarget) * 2; targetPosition.y += int64_t((targetPosition.y - blinkyPosition.y) / distanceBetweenBlinkyAndTarget) * 2;
return gridPositionToPosition(targetPosition); return gridPositionToPosition(targetPosition);
} }

View File

@ -18,6 +18,7 @@ GridPosition PacMan::positionInGrid() const {
void PacMan::die() { void PacMan::die() {
if (dead) if (dead)
return; return;
dead = true; dead = true;
} }
@ -33,8 +34,10 @@ void PacMan::update(std::chrono::milliseconds time_delta, Direction input_direct
updateAnimationPosition(time_delta, false); updateAnimationPosition(time_delta, false);
return; return;
} }
if (input_direction != Direction::NONE) if (input_direction != Direction::NONE)
desired_direction = input_direction; desired_direction = input_direction;
const auto old = pos; const auto old = pos;
updateMazePosition(time_delta); updateMazePosition(time_delta);
const bool paused = pos == old; 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) { void PacMan::updateMazePosition(std::chrono::milliseconds time_delta) {
if (isPortal(positionInGrid(), direction)) { if (isPortal(positionInGrid(), direction)) {
pos = gridPositionToPosition(teleport(positionInGrid())); pos = gridPositionToPosition(teleport(positionInGrid()));
return; return;
@ -62,13 +64,13 @@ void PacMan::updateMazePosition(std::chrono::milliseconds time_delta) {
auto moveToPosition = [position_delta](Position point, Direction move_direction) { auto moveToPosition = [position_delta](Position point, Direction move_direction) {
switch (move_direction) { switch (move_direction) {
case Direction::LEFT: 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: 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: 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: 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: case Direction::NONE:
default: default:
return positionToGridPosition(point); return positionToGridPosition(point);
@ -79,31 +81,34 @@ void PacMan::updateMazePosition(std::chrono::milliseconds time_delta) {
return isWalkableForPacMan(moveToPosition(pos, move_direction)); return isWalkableForPacMan(moveToPosition(pos, move_direction));
}; };
if (canGo(desired_direction)) { if (desired_direction != direction && canGo(desired_direction)) {
direction = desired_direction; direction = desired_direction;
} }
if (canGo(direction)) { if (!canGo(direction)) {
switch (direction) { return;
case Direction::NONE: }
break;
case Direction::LEFT: switch (direction) {
pos.x -= position_delta; case Direction::LEFT:
pos.y = std::floor(pos.y); pos.x -= position_delta;
break; pos.y = std::floor(pos.y);
case Direction::RIGHT: break;
pos.x += position_delta; case Direction::RIGHT:
pos.y = std::floor(pos.y); pos.x += position_delta;
break; pos.y = std::floor(pos.y);
case Direction::UP: break;
pos.x = std::floor(pos.x); case Direction::UP:
pos.y -= position_delta; pos.x = std::floor(pos.x);
break; pos.y -= position_delta;
case Direction::DOWN: break;
pos.x = std::floor(pos.x); case Direction::DOWN:
pos.y += position_delta; pos.x = std::floor(pos.x);
break; pos.y += position_delta;
} break;
case Direction::NONE:
default:
break;
} }
} }

View File

@ -32,15 +32,13 @@ void PacManAnimation::updateAnimationPosition(std::chrono::milliseconds time_del
return; return;
animation_position_delta += (0.02) * double(time_delta.count()); animation_position_delta += (0.02) * double(time_delta.count());
animation_position = int64_t(animation_position + animation_position_delta);
animation_position = int(animation_position + animation_position_delta);
if (!dead) if (!dead)
animation_position = animation_position % 4; animation_position = animation_position % 4;
if(animation_position_delta > 1) if(animation_position_delta > 1)
animation_position_delta = animation_position_delta - 1; animation_position_delta = animation_position_delta - 1;
} }
void PacManAnimation::pause() { void PacManAnimation::pause() {

View File

@ -47,8 +47,8 @@ constexpr GridPosition eyeSprite(Direction direction) {
constexpr GridPosition ghostSprite(Ghost ghost, Direction direction, bool alternative) { constexpr GridPosition ghostSprite(Ghost ghost, Direction direction, bool alternative) {
assert(ghost >= Ghost::blinky && ghost <= Ghost::clyde && "Invalid Ghost"); assert(ghost >= Ghost::blinky && ghost <= Ghost::clyde && "Invalid Ghost");
auto y = static_cast<size_t>(ghost); auto y = static_cast<int64_t>(ghost);
size_t x = 0; int64_t x = 0;
switch (direction) { switch (direction) {
case Direction::RIGHT: case Direction::RIGHT:
x = 0; x = 0;

View File

@ -1,7 +1,5 @@
#pragma once #pragma once
#include <SFML/Graphics.hpp>
#include "Direction.hpp" #include "Direction.hpp"
#include "Position.hpp" #include "Position.hpp"
#include <array> #include <array>
@ -20,8 +18,7 @@ std::vector<GridPosition> initialPelletPositions();
std::vector<GridPosition> initialSuperPelletPositions(); std::vector<GridPosition> initialSuperPelletPositions();
inline Position penDoorPosition() { inline Position penDoorPosition() { return { 13, 11 }; }
return { 13, 11 }; } inline Position initialPacManPosition() { return { 13.5, 23 }; }
inline Position initialPacManPosition() { return { 13.5, 23 }; }
} // namespace pacman } // namespace pacman

View File

@ -1,16 +1,20 @@
#pragma once #pragma once
#include <SFML/Graphics.hpp>
#include "GameState.hpp" #include "GameState.hpp"
#include "Position.hpp" #include "Position.hpp"
#include "Score.hpp"
#include <optional> #include <optional>
namespace pacman { namespace pacman {
using Rect = sf::Rect<int>;
using Sprite = sf::Sprite;
class Canvas { class Canvas {
public: public:
Canvas(); Canvas();
void update(const GameState & gameState, const Score & score); void update(const GameState & gameState);
std::optional<sf::Event> pollEvent(); std::optional<sf::Event> pollEvent();
private: private:

View File

@ -1,29 +1,20 @@
#pragma once #pragma once
#include "Canvas.hpp"
#include "GameState.hpp" #include "GameState.hpp"
#include "InputState.hpp" #include "InputState.hpp"
#include "Canvas.hpp"
namespace pacman { namespace pacman {
class Game { class Game {
public: public:
Game();
void run(); void run();
private: private:
Canvas canvas; Canvas canvas;
GameState gameState; GameState gameState;
Score score;
std::chrono::milliseconds timeSinceDeath{};
void step(std::chrono::milliseconds delta, InputState inputState);
void eatPellets();
void processEvents(InputState & inputState); void processEvents(InputState & inputState);
void checkCollision(Ghost & ghost);
void killPacMan();
bool pacManDying() const;
void handleDeathAnimation(std::chrono::milliseconds delta);
}; };
} // namespace pacman } // namespace pacman

View File

@ -9,17 +9,31 @@
#include "Pinky.hpp" #include "Pinky.hpp"
#include "Score.hpp" #include "Score.hpp"
#include "SuperPellets.hpp" #include "SuperPellets.hpp"
#include "InputState.hpp"
namespace pacman { namespace pacman {
struct GameState { struct GameState {
void step(std::chrono::milliseconds delta);
Blinky blinky; Blinky blinky;
Pinky pinky; Pinky pinky;
Inky inky; Inky inky;
Clyde clyde; Clyde clyde;
PacMan pacMan; PacMan pacMan;
InputState inputState;
Pellets pellets; Pellets pellets;
SuperPellets superPellets; 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 } // namespace pacman

View File

@ -23,9 +23,7 @@ public:
virtual ~Ghost() = default; virtual ~Ghost() = default;
GridPosition currentSprite() const; GridPosition currentSprite() const;
Position position() const; Position position() const;
GridPosition positionInGrid() const; GridPosition positionInGrid() const;
void update(std::chrono::milliseconds time_delta, const GameState & gameState); void update(std::chrono::milliseconds time_delta, const GameState & gameState);
@ -41,12 +39,6 @@ private:
void updateDirection(const GameState & gameState); void updateDirection(const GameState & gameState);
protected: 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; Atlas::Ghost spriteSet;
Direction direction = Direction::NONE; Direction direction = Direction::NONE;
double timeForAnimation = 0; double timeForAnimation = 0;
@ -56,6 +48,13 @@ protected:
std::chrono::milliseconds timeChase = {}; std::chrono::milliseconds timeChase = {};
Position pos; Position pos;
GridPosition last_grid_position = { 0, 0 }; 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; bool isInPen() const;
}; };

View File

@ -8,15 +8,10 @@
namespace pacman { namespace pacman {
class Board;
class InputState;
class PacMan { class PacMan {
public: public:
GridPosition currentSprite() const; GridPosition currentSprite() const;
Position position() const; Position position() const;
GridPosition positionInGrid() const; GridPosition positionInGrid() const;
void update(std::chrono::milliseconds time_delta, Direction input_direction); void update(std::chrono::milliseconds time_delta, Direction input_direction);

View File

@ -18,7 +18,7 @@ public:
void pause(); void pause();
private: private:
size_t animation_position = 0; int64_t animation_position = 0;
double animation_position_delta = 0.0; double animation_position_delta = 0.0;
}; };

View File

@ -1,6 +1,5 @@
#pragma once #pragma once
#include <SFML/Graphics.hpp>
#include <cmath> #include <cmath>
namespace pacman { namespace pacman {
@ -11,17 +10,13 @@ struct Position {
}; };
struct GridPosition { struct GridPosition {
size_t x; int64_t x;
size_t y; int64_t y;
constexpr GridPosition(size_t x, size_t y) : x(x), y(y) {} constexpr GridPosition(int64_t x, int64_t y) : x(x), y(y) {}
}; };
using Rect = sf::Rect<int>;
using Sprite = sf::Sprite;
inline GridPosition positionToGridPosition(Position pos) { 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) { inline Position gridPositionToPosition(GridPosition pos) {
@ -36,14 +31,14 @@ constexpr bool operator!=(const GridPosition & a, const GridPosition & b) {
return !(a == b); return !(a == b);
} }
constexpr bool operator==(const Position & a, const Position & b) { inline bool operator==(const Position & a, const Position & b) {
return a.x == b.x && a.y == b.y; // This is ok as a test unless x and y become very large.
constexpr double epsilon = std::numeric_limits<double>::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); return !(a == b);
} }
} // namespace pacman } // namespace pacman

View File

@ -1,8 +1,10 @@
#pragma once #pragma once
namespace pacman { namespace pacman {
constexpr int DEFAULT_LIVES = 3;
struct Score { struct Score {
int lives = 0; int lives = DEFAULT_LIVES;
int points = 0; int points = 0;
int eatenPellets = 0; int eatenPellets = 0;
}; };

View File

@ -3,8 +3,9 @@ find_package(GTest REQUIRED)
include(GoogleTest) 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) target_link_libraries(pacman_tests GTest::GTest libpacman)
gtest_discover_tests(pacman_tests TEST_PREFIX pacman:) gtest_discover_tests(pacman_tests)
add_test(NAME monolithic COMMAND pacman_tests)

26
test/testGameState.cpp Normal file
View File

@ -0,0 +1,26 @@
#include "GameState.hpp"
#include <gtest/gtest.h>
#include <fmt/format.h>
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());
}

77
test/testGhost.cpp Normal file
View File

@ -0,0 +1,77 @@
#include "Blinky.hpp"
#include "Clyde.hpp"
#include "Inky.hpp"
#include "Pinky.hpp"
#include <gtest/gtest.h>
template<typename T>
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<typename T>
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);
}
template<typename T>
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);
}

8
test/testPacMan.cpp Normal file
View File

@ -0,0 +1,8 @@
#include "PacMan.hpp"
#include <gtest/gtest.h>
TEST(PacManTest, InitialPosition) {
pacman::PacMan pacMan;
EXPECT_EQ(pacMan.position().x, 13.5);
EXPECT_EQ(pacMan.position().y, 23);
}

46
test/testPosition.cpp Normal file
View File

@ -0,0 +1,46 @@
#include "Position.hpp"
#include <gtest/gtest.h>
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);
}

View File

@ -1,12 +1,5 @@
#include "PacMan.hpp"
#include <gtest/gtest.h> #include <gtest/gtest.h>
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[]) { int main(int argc, char * argv[]) {
testing::InitGoogleTest(&argc, argv); testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS(); return RUN_ALL_TESTS();