Pacman and ghosts can eat each others

This commit is contained in:
Corentin Jabot 2021-06-28 12:42:21 +02:00 committed by Patricia Aas
parent ced6cd829c
commit 5b0e561afc
13 changed files with 278 additions and 99 deletions

View file

@ -22,6 +22,33 @@ namespace Atlas {
constexpr PositionInt pacman_down_wide = { 7, 0 }; constexpr PositionInt pacman_down_wide = { 7, 0 };
constexpr PositionInt pacman_down_narrow = { 8, 0 }; constexpr PositionInt pacman_down_narrow = { 8, 0 };
constexpr PositionInt ghost_frightened1 = { 0, 7 };
constexpr PositionInt ghost_frightened2 = { 1, 7 };
constexpr PositionInt ghost_frightened3 = { 2, 7 };
constexpr PositionInt ghost_frightened4 = { 3, 7 };
constexpr PositionInt eyeSprite(Direction direction) {
int x = 0;
switch (direction) {
case Direction::RIGHT:
x = 0;
break;
case Direction::DOWN:
x = 2;
break;
case Direction::LEFT:
x = 4;
break;
case Direction::UP:
x = 6;
break;
default:
x = 0;
break;
}
return { x, 6 };
}
constexpr PositionInt ghostSprite(Ghost ghost, Direction direction, bool alternative) { constexpr PositionInt 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");
int y = static_cast<int>(ghost); int y = static_cast<int>(ghost);

View file

@ -58,16 +58,15 @@ bool Board::isWalkableForGhost(Position point, float d, Direction direction) con
} }
bool Board::isWalkable(Position point) const { bool Board::isWalkable(Position point) const {
return board_state[int(point.y)][int(point.x)] != uint8_t(Cell::wall); return board_state[int(point.y)][int(point.x)] != uint8_t(Cell::wall);
} }
bool Board::isWalkableForGost(Position point, Position origin) const bool Board::isWalkableForGost(Position point, Position origin, bool isEyes) const {
{ return isWalkable(point) && (isEyes || (isInPen(origin) || !isInPen(point)));
return isWalkable(point) && (isInPen(origin) || !isInPen(point));
} }
bool Board::isInPen(Position point) const { bool Board::isInPen(Position point) const {
return board_state[int(point.y)][int(point.x)] == uint8_t(Cell::pen); return board_state[int(point.y)][int(point.x)] == uint8_t(Cell::pen);
} }
bool Board::isWalkable(Position point, float position_delta, Direction direction, bool pacman) const { bool Board::isWalkable(Position point, float position_delta, Direction direction, bool pacman) const {

View file

@ -26,7 +26,7 @@ public:
[[nodiscard]] bool isWalkableForPacMan(Position point, float d, Direction direction) const; [[nodiscard]] bool isWalkableForPacMan(Position point, float d, Direction direction) const;
[[nodiscard]] bool isWalkableForGhost(Position point, float d, Direction direction) const; [[nodiscard]] bool isWalkableForGhost(Position point, float d, Direction direction) const;
[[nodiscard]] bool isWalkableForGost(Position point, Position origin) const; [[nodiscard]] bool isWalkableForGost(Position point, Position origin, bool isEyes) const;
[[nodiscard]] bool isWalkable(Position point) const; [[nodiscard]] bool isWalkable(Position point) const;
[[nodiscard]] bool isInPen(Position point) const; [[nodiscard]] bool isInPen(Position point) const;

View file

@ -24,15 +24,17 @@ void Canvas::update(const Game & game) {
renderMaze(); renderMaze();
renderPellets(game.pellets); renderPellets(game.pellets);
renderSuperPellets(game.superPellets); renderSuperPellets(game.superPellets);
renderPacMan(game.pacMan);
std::apply([&](const auto&... ghost) { std::apply([&](const auto &... ghost) {
(renderGhost(ghost),...); (renderGhost(ghost), ...);
}, game.ghosts); },
game.ghosts);
renderScore(game.score.points); renderScore(game.score.points);
renderLives(game.score.lives); renderLives(game.score.lives);
renderPacMan(game.pacMan);
render(); render();
} }

View file

@ -8,14 +8,18 @@ enum class Direction {
DOWN DOWN
}; };
inline Direction oppositeDirection(Direction d) inline Direction oppositeDirection(Direction d) {
{ switch (d) {
switch (d) { case Direction::LEFT:
case Direction::LEFT: return Direction::RIGHT; return Direction::RIGHT;
case Direction::RIGHT: return Direction::LEFT; case Direction::RIGHT:
case Direction::UP: return Direction::DOWN; return Direction::LEFT;
case Direction::DOWN: return Direction::UP; case Direction::UP:
case Direction::NONE: return d; return Direction::DOWN;
} case Direction::DOWN:
return d; return Direction::UP;
case Direction::NONE:
return d;
}
return d;
} }

View file

@ -5,7 +5,7 @@
constexpr int DEFAULT_LIVES = 3; constexpr int DEFAULT_LIVES = 3;
constexpr int NORMAL_PELLET_POINTS = 10; constexpr int NORMAL_PELLET_POINTS = 10;
constexpr int POWER_PELLET_POINTS = 50; constexpr int POWER_PELLET_POINTS = 50;
//constexpr int GHOST_POINTS[] = {200, 400, 800, 1600}; constexpr int GHOST_POINTS = 200;
Game::Game() Game::Game()
: pacMan(board), : pacMan(board),
@ -15,7 +15,6 @@ Game::Game()
score.lives = DEFAULT_LIVES; score.lives = DEFAULT_LIVES;
} }
auto Game::now() { auto Game::now() {
return std::chrono::system_clock::now(); return std::chrono::system_clock::now();
} }
@ -48,15 +47,54 @@ void Game::run() {
} }
void Game::step(std::chrono::milliseconds delta, InputState inputState) { void Game::step(std::chrono::milliseconds delta, InputState inputState) {
pacMan.update(delta, inputState, board); pacMan.update(delta, inputState, board);
if (timeSinceDeath.count() != 0) {
timeSinceDeath += delta;
}
if (timeSinceDeath.count() > 1000) {
std::apply([&](auto &... ghost) {
(ghost.reset(), ...);
},
ghosts);
pacMan.reset(board);
timeSinceDeath = std::chrono::milliseconds(0);
}
if (timeSinceDeath.count())
return;
if (!pacMan.onTheMove())
return;
std::apply([&](auto &... ghost) { std::apply([&](auto &... ghost) {
(ghost.update(delta, board),...); (ghost.update(delta, board), ...);
}, ghosts); (checkCollision(ghost), ...);
},
ghosts);
eatPellets(); eatPellets();
} }
void Game::checkCollision(Ghost & g) {
if (timeSinceDeath.count() || g.isEyes())
return;
if (g.positionInGrid() != pacMan.positionInGrid())
return;
if (g.isFrightened()) {
g.eat();
score.points += GHOST_POINTS;
} else {
pacMan.eat();
score.lives--;
timeSinceDeath = std::chrono::milliseconds(1);
}
}
void Game::eatPellets() { void Game::eatPellets() {
const auto pos = pacMan.positionInGrid(); const auto pos = pacMan.positionInGrid();
if (pellets.eatPelletAtPosition(pos)) { if (pellets.eatPelletAtPosition(pos)) {
@ -67,6 +105,10 @@ void Game::eatPellets() {
if (superPellets.eatPelletAtPosition(pos)) { if (superPellets.eatPelletAtPosition(pos)) {
score.eatenPellets++; score.eatenPellets++;
score.points += POWER_PELLET_POINTS; score.points += POWER_PELLET_POINTS;
std::apply([&](auto &... ghost) {
(ghost.frighten(), ...);
},
ghosts);
} }
} }

View file

@ -25,10 +25,12 @@ private:
SuperPellets superPellets; SuperPellets superPellets;
std::tuple<Blinky, Speedy, Inky, Clyde> ghosts; std::tuple<Blinky, Speedy, Inky, Clyde> ghosts;
Score score; Score score;
std::chrono::milliseconds timeSinceDeath;
void step(std::chrono::milliseconds delta, InputState inputState); void step(std::chrono::milliseconds delta, InputState inputState);
void eatPellets(); void eatPellets();
void processEvents(InputState & inputState); void processEvents(InputState & inputState);
void checkCollision(Ghost & g);
[[nodiscard]] static auto now(); [[nodiscard]] static auto now();
}; };

View file

@ -8,8 +8,50 @@ Ghost::Ghost(Atlas::Ghost spritesSet, Position startingPosition, Position scatte
scatterTarget(scatterTarget) { scatterTarget(scatterTarget) {
} }
void Ghost::frighten() {
if (state > State::Scatter)
return;
direction = oppositeDirection(direction);
state = State::Frightened;
timeFrighten = 0;
}
bool Ghost::isFrightened() const {
return state == State::Frightened;
}
bool Ghost::isEyes() const {
return state == State::Eyes;
}
void Ghost::eat() {
if (state == State::Eyes)
return;
direction = oppositeDirection(direction);
state = State::Eyes;
timeFrighten = 0;
timeChase = 0;
}
void Ghost::reset() {
pos = startingPosition;
}
[[nodiscard]] PositionInt Ghost::currentSprite() const { [[nodiscard]] PositionInt Ghost::currentSprite() const {
return Atlas::ghostSprite(spritesSet, direction, alternate_animation); switch (state) {
default:
return Atlas::ghostSprite(spritesSet, direction, (animationIndex % 2) == 0);
case State::Frightened:
if (timeFrighten < 3500)
return (animationIndex % 2) == 0 ? Atlas::ghost_frightened2 : Atlas::ghost_frightened1;
return std::array{ Atlas::ghost_frightened1,
Atlas::ghost_frightened2,
Atlas::ghost_frightened3,
Atlas::ghost_frightened4 }[animationIndex];
case State::Eyes:
return Atlas::eyeSprite(direction);
}
return {};
} }
Position Ghost::position() const { Position Ghost::position() const {
@ -17,89 +59,107 @@ Position Ghost::position() const {
} }
Position Ghost::positionInGrid() const { Position Ghost::positionInGrid() const {
return { std::round(pos.x), std::round(pos.y) }; return { std::round(pos.x), std::round(pos.y) };
} }
void Ghost::update(std::chrono::milliseconds time_delta, const Board & board) { void Ghost::update(std::chrono::milliseconds time_delta, const Board & board) {
updateAnimation(time_delta); if (state == State::Eyes && board.isInPen(positionInGrid()))
updatePosition(time_delta, board); state = State::Scatter;
if (state == State::Frightened) {
timeFrighten += time_delta.count();
if (timeFrighten > 6000)
state = State::Scatter;
}
updateAnimation(time_delta);
updatePosition(time_delta, board);
} }
void Ghost::updatePosition(std::chrono::milliseconds time_delta, const Board & board) { void Ghost::updatePosition(std::chrono::milliseconds time_delta, const Board & board) {
updateDirection(board); updateDirection(board);
float position_delta = (0.004 * time_delta.count()) * 0.75; float position_delta = (0.004 * time_delta.count()) * speed();
switch (direction) { switch (direction) {
case Direction::NONE: case Direction::NONE:
break; break;
case Direction::LEFT: case Direction::LEFT:
pos.x -= position_delta; pos.x -= position_delta;
pos.y = round(pos.y); pos.y = round(pos.y);
break; break;
case Direction::RIGHT: case Direction::RIGHT:
pos.x += position_delta; pos.x += position_delta;
pos.y = round(pos.y); pos.y = round(pos.y);
break; break;
case Direction::UP: case Direction::UP:
pos.x = round(pos.x); pos.x = round(pos.x);
pos.y -= position_delta; pos.y -= position_delta;
break; break;
case Direction::DOWN: case Direction::DOWN:
pos.x = round(pos.x); pos.x = round(pos.x);
pos.y += position_delta; pos.y += position_delta;
break; break;
} }
}
double Ghost::speed() const {
if (state == State::Eyes)
return 2;
if (state == State::Frightened)
return 0.5;
return 0.75;
} }
void Ghost::updateDirection(const Board & board) { void Ghost::updateDirection(const Board & board) {
auto cell = positionInGrid(); auto cell = positionInGrid();
if(cell == lastIntersection) if (cell == lastIntersection)
return; return;
struct NewDirection { struct NewDirection {
Direction direction; Direction direction;
Position position; Position position;
double distance; double distance;
}; };
auto [x , y] = cell; auto [x, y] = cell;
std::array directions = { std::array directions = {
NewDirection{Direction::UP, {x, y-1}, 0}, NewDirection{ Direction::UP, { x, y - 1 }, 0 },
NewDirection{Direction::LEFT, {x-1, y}, 0}, NewDirection{ Direction::LEFT, { x - 1, y }, 0 },
NewDirection{Direction::DOWN, {x, y+1}, 0}, NewDirection{ Direction::DOWN, { x, y + 1 }, 0 },
NewDirection{Direction::RIGHT, {x+1, y}, 0} NewDirection{ Direction::RIGHT, { x + 1, y }, 0 }
}; };
const Position target = this->target(board); const Position target = this->target(board);
for(auto && d : directions) { for (auto && d : directions) {
d.distance = (d.direction != oppositeDirection(direction) && board.isWalkableForGost(d.position, cell)) ? d.distance = (d.direction != oppositeDirection(direction) && board.isWalkableForGost(d.position, cell, state == State::Eyes)) ? std::hypot(d.position.x - target.x, d.position.y - target.y)
std::hypot(d.position.x - target.x, d.position.y - target.y) : std::numeric_limits<double>::infinity();
: std::numeric_limits<double>::infinity(); }
}
auto it = std::min_element(directions.begin(), directions.end(), [](const auto & a, const auto &b) { auto it = std::min_element(directions.begin(), directions.end(), [](const auto & a, const auto & b) {
return a.distance < b.distance; return a.distance < b.distance;
}); });
lastIntersection = cell; lastIntersection = cell;
direction = it->direction; direction = it->direction;
} }
Position Ghost::target(const Board & board) const { Position Ghost::target(const Board & board) const {
if(board.isInPen(positionInGrid())) if (state == State::Eyes)
return board.penDoorPosition(); return startingPosition;
return scatterTarget; if (board.isInPen(positionInGrid()))
return board.penDoorPosition();
return scatterTarget;
} }
void Ghost::updateAnimation(std::chrono::milliseconds time_delta) void Ghost::updateAnimation(std::chrono::milliseconds time_delta) {
{ timeForAnimation += time_delta.count();
time += time_delta.count(); if (timeForAnimation >= 250) {
if (time >= 250) { timeForAnimation = 0;
time = 0; animationIndex = (animationIndex + 1) % 4;
alternate_animation = !alternate_animation; }
}
} }
Blinky::Blinky(const Board & board) Blinky::Blinky(const Board & board)

View file

@ -11,7 +11,7 @@ public:
enum class State { enum class State {
Chase, Chase,
Scatter, Scatter,
Freightened, Frightened,
Eyes, Eyes,
}; };
@ -24,8 +24,14 @@ public:
[[nodiscard]] Position positionInGrid() const; [[nodiscard]] Position positionInGrid() const;
void update(std::chrono::milliseconds time_delta, const Board & board); void update(std::chrono::milliseconds time_delta, const Board & board);
void frighten();
void eat();
bool isFrightened() const;
bool isEyes() const;
void reset();
private: private:
double speed() const;
void updateAnimation(std::chrono::milliseconds time_delta); void updateAnimation(std::chrono::milliseconds time_delta);
void updatePosition(std::chrono::milliseconds time_delta, const Board & board); void updatePosition(std::chrono::milliseconds time_delta, const Board & board);
void updateDirection(const Board & board); void updateDirection(const Board & board);
@ -34,13 +40,15 @@ private:
protected: protected:
Atlas::Ghost spritesSet; Atlas::Ghost spritesSet;
Direction direction = Direction::NONE; Direction direction = Direction::NONE;
double time = 0; double timeForAnimation = 0;
bool alternate_animation = false; int animationIndex = 0;
State state = State::Chase; State state = State::Chase;
int timeFrighten = 0;
int timeChase = 0;
Position pos; Position pos;
Position startingPosition; Position startingPosition;
Position scatterTarget; Position scatterTarget;
Position lastIntersection = {-1, -1}; Position lastIntersection = { -1, -1 };
}; };
class Blinky : public Ghost { class Blinky : public Ghost {

View file

@ -5,7 +5,7 @@ PacMan::PacMan(const Board & board)
: pos(Board::initialPacManPosition()) {} : pos(Board::initialPacManPosition()) {}
PositionInt PacMan::currentSprite() const { PositionInt PacMan::currentSprite() const {
return pacManAnimation.animationFrame(direction); return eaten ? pacManAnimation.deathAnimationFrame(direction) : pacManAnimation.animationFrame(direction);
} }
Position PacMan::position() const { Position PacMan::position() const {
@ -16,11 +16,27 @@ Position PacMan::positionInGrid() const {
return { std::round(pos.x), std::round(pos.y) }; return { std::round(pos.x), std::round(pos.y) };
} }
void PacMan::update(std::chrono::milliseconds time_delta, InputState state, const Board & board) { void PacMan::eat() {
setDirection(state); if (eaten)
const auto old = pos; return;
updateMazePosition(time_delta, board); eaten = true;
direction = Direction::NONE;
}
void PacMan::reset(const Board & b) {
eaten = false;
direction = Direction::NONE;
pos = b.initialPacManPosition();
}
void PacMan::update(std::chrono::milliseconds time_delta, InputState state, const Board & board) {
if (eaten) {
updateAnimationPosition(time_delta, false);
return;
}
const auto old = pos;
setDirection(state);
updateMazePosition(time_delta, board);
const bool paused = pos == old; const bool paused = pos == old;
updateAnimationPosition(time_delta, paused); updateAnimationPosition(time_delta, paused);
} }
@ -40,7 +56,7 @@ void PacMan::updateAnimationPosition(std::chrono::milliseconds time_delta, bool
if (paused) { if (paused) {
pacManAnimation.pause(); pacManAnimation.pause();
} else { } else {
pacManAnimation.updateAnimationPosition(time_delta); pacManAnimation.updateAnimationPosition(time_delta, eaten);
} }
} }

View file

@ -21,11 +21,18 @@ public:
void update(std::chrono::milliseconds time_delta, InputState state, const Board & board); void update(std::chrono::milliseconds time_delta, InputState state, const Board & board);
void eat();
void reset(const Board & b);
bool onTheMove() const {
return direction != Direction::NONE;
}
private: private:
Direction direction = Direction::NONE; Direction direction = Direction::NONE;
Direction desired_direction = Direction::NONE; Direction desired_direction = Direction::NONE;
Position pos; Position pos;
PacManAnimation pacManAnimation; PacManAnimation pacManAnimation;
bool eaten = false;
void setDirection(const InputState & state); void setDirection(const InputState & state);

View file

@ -16,9 +16,20 @@ PositionInt PacManAnimation::animationFrame(Direction direction) const {
} }
} }
void PacManAnimation::updateAnimationPosition(std::chrono::milliseconds time_delta) { [[nodiscard]] PositionInt PacManAnimation::deathAnimationFrame(Direction direction) const {
return PositionInt{ animation_position, 1 };
}
void PacManAnimation::updateAnimationPosition(std::chrono::milliseconds time_delta, bool dead) {
if (dead && animation_position >= 11)
return;
animation_position_delta += (0.02) * float(time_delta.count()); animation_position_delta += (0.02) * float(time_delta.count());
animation_position = int(animation_position + animation_position_delta) % 4; animation_position = int(animation_position + animation_position_delta);
if (!dead)
animation_position = animation_position % 4;
animation_position_delta = (animation_position_delta < 1) ? animation_position_delta : (animation_position_delta - 1); animation_position_delta = (animation_position_delta < 1) ? animation_position_delta : (animation_position_delta - 1);
} }

View file

@ -11,8 +11,9 @@
class PacManAnimation { class PacManAnimation {
public: public:
[[nodiscard]] PositionInt animationFrame(Direction direction) const; [[nodiscard]] PositionInt animationFrame(Direction direction) const;
[[nodiscard]] PositionInt deathAnimationFrame(Direction direction) const;
void updateAnimationPosition(std::chrono::milliseconds time_delta); void updateAnimationPosition(std::chrono::milliseconds time_delta, bool dead);
void pause(); void pause();
private: private: