pacman/lib/Ghost.cpp

231 lines
7 KiB
C++
Raw Normal View History

2021-06-24 11:32:52 +00:00
#include "Ghost.hpp"
2021-06-28 12:07:28 +00:00
#include <array>
2021-07-01 12:53:52 +00:00
#include <cmath>
2021-07-29 09:16:08 +00:00
#include <numeric>
2021-09-10 09:35:26 +00:00
#include <algorithm>
2021-06-24 11:32:52 +00:00
2021-07-05 12:10:01 +00:00
namespace pacman {
2021-07-28 17:06:35 +00:00
Ghost::Ghost(Atlas::Ghost spriteSet)
: spriteSet(spriteSet) {
2021-06-24 11:32:52 +00:00
}
2021-06-28 10:42:21 +00:00
void Ghost::frighten() {
if (state > State::Scatter)
return;
direction = oppositeDirection(direction);
state = State::Frightened;
2021-07-29 09:16:08 +00:00
timeFrighten = {};
2021-06-28 10:42:21 +00:00
}
bool Ghost::isFrightened() const {
return state == State::Frightened;
}
bool Ghost::isEyes() const {
return state == State::Eyes;
}
2021-07-13 12:26:57 +00:00
void Ghost::die() {
2021-06-28 10:42:21 +00:00
if (state == State::Eyes)
return;
direction = oppositeDirection(direction);
state = State::Eyes;
2021-07-29 09:16:08 +00:00
timeFrighten = {};
timeChase = {};
2021-06-28 10:42:21 +00:00
}
void Ghost::reset() {
pos = initialPosition();
state = State::Scatter;
2021-07-29 09:16:08 +00:00
timeFrighten = {};
timeChase = {};
2021-06-28 10:42:21 +00:00
}
2021-08-12 08:18:51 +00:00
GridPosition Ghost::currentSprite() const {
2021-06-28 10:42:21 +00:00
switch (state) {
default:
2021-07-28 17:06:35 +00:00
return Atlas::ghostSprite(spriteSet, direction, (animationIndex % 2) == 0);
2021-06-28 10:42:21 +00:00
case State::Eyes:
return Atlas::eyeSprite(direction);
2021-07-01 12:53:52 +00:00
case State::Frightened:
2021-07-29 09:16:08 +00:00
if (timeFrighten.count() < 3500)
2021-07-01 12:53:52 +00:00
return Atlas::initialFrightened(animationIndex);
else
return Atlas::endingFrightened(animationIndex);
2021-06-28 10:42:21 +00:00
}
2021-06-24 11:32:52 +00:00
}
Position Ghost::position() const {
return pos;
}
2021-07-05 11:54:54 +00:00
GridPosition Ghost::positionInGrid() const {
2021-07-06 10:35:23 +00:00
return positionToGridPosition(pos);
2021-06-24 11:32:52 +00:00
}
2021-07-28 13:28:36 +00:00
void Ghost::update(std::chrono::milliseconds time_delta, const GameState & gameState) {
2021-07-07 09:39:09 +00:00
if (state == State::Eyes && isInPen())
2021-06-28 10:42:21 +00:00
state = State::Scatter;
if (state == State::Frightened) {
2021-07-29 09:16:08 +00:00
timeFrighten += time_delta;
if (timeFrighten.count() > 6000)
2021-06-28 10:42:21 +00:00
state = State::Scatter;
}
2021-07-29 09:16:08 +00:00
if (state == State::Scatter || state == State::Chase) {
timeChase += time_delta;
const auto newState = defaultStateAtDuration(std::chrono::duration_cast<std::chrono::seconds>(timeChase));
2021-07-29 09:16:08 +00:00
if (newState != state) {
direction = oppositeDirection(direction);
state = newState;
}
}
2021-06-28 10:42:21 +00:00
updateAnimation(time_delta);
2021-07-28 13:28:36 +00:00
updatePosition(time_delta, gameState);
}
2021-07-07 09:39:09 +00:00
bool Ghost::isInPen() const {
2021-07-08 15:42:24 +00:00
return pacman::isInPen(positionInGrid());
2021-07-01 12:53:52 +00:00
}
2021-07-28 13:28:36 +00:00
void Ghost::updatePosition(std::chrono::milliseconds time_delta, const GameState & gameState) {
updateDirection(gameState);
2021-06-28 10:42:21 +00:00
2021-09-10 12:44:40 +00:00
double position_delta = (0.004 * double(time_delta.count())) * speed(gameState);
2021-06-28 10:42:21 +00:00
const auto old_position = pos;
const GridPosition old_grid_position = positionToGridPosition(old_position);
2021-06-28 10:42:21 +00:00
switch (direction) {
case Direction::NONE:
break;
case Direction::LEFT:
pos.x -= position_delta;
pos.y = round(pos.y);
break;
case Direction::RIGHT:
pos.x += position_delta;
pos.y = round(pos.y);
break;
case Direction::UP:
pos.x = round(pos.x);
pos.y -= position_delta;
break;
case Direction::DOWN:
pos.x = round(pos.x);
pos.y += position_delta;
break;
}
2021-07-29 09:16:08 +00:00
if (isPortal(positionInGrid(), direction)) {
pos = gridPositionToPosition(teleport(positionInGrid()));
}
else if (!isWalkableForGhost(positionInGrid(), old_grid_position, isEyes())) {
pos = old_position;
direction = oppositeDirection(direction);
}
2021-06-28 10:42:21 +00:00
}
2021-07-07 08:38:52 +00:00
/*
* Each time a ghost finds itself at an intersection,
* it picks a target position - the specific target depends on the state
* of the ghost and the specific ghost.
*
* For each 4 cells around the current ghost position the straight-line distance
* to the target is calculated (this ignores all obstacles, including walls)
*
* The ghost then selects among these 4 cells the one with the shortest euclidean distance to the target.
* If a cell is a wall or would cause a ghost to move in the opposite direction, the distance to the target
* from that cell is considered infinite (due to the shape of the maze, there is always one direction
* a ghost can take).
*
2021-07-07 09:24:12 +00:00
* In the scatter state, each ghost tries to reach an unreachable position outside of the map.
2021-07-07 08:38:52 +00:00
* This makes ghosts run in circle around the island at each of the 4 map corner.
*/
2021-07-28 13:28:36 +00:00
void Ghost::updateDirection(const GameState & gameState) {
2021-07-06 10:35:23 +00:00
const auto current_grid_position = positionInGrid();
if (current_grid_position == last_grid_position)
2021-06-28 10:42:21 +00:00
return;
2021-07-06 10:35:23 +00:00
struct Move {
2021-09-10 14:19:24 +00:00
Direction direction = Direction::NONE;
2021-07-06 10:35:23 +00:00
Position position;
double distance_to_target = std::numeric_limits<double>::infinity();
2021-06-28 10:42:21 +00:00
};
2021-07-06 10:35:23 +00:00
const Position current_position = { double(current_grid_position.x), double(current_grid_position.y) };
const auto [x, y] = current_position;
std::array<Move, 4> possible_moves = {
Move{ Direction::UP, { x, y - 1 } },
Move{ Direction::LEFT, { x - 1, y } },
Move{ Direction::DOWN, { x, y + 1 } },
Move{ Direction::RIGHT, { x + 1, y } }
};
2021-06-28 10:42:21 +00:00
2021-07-28 13:28:36 +00:00
const Position target_position = target(gameState);
2021-07-06 10:35:23 +00:00
for (auto & move : possible_moves) {
2021-07-29 09:16:08 +00:00
if (isPortal(current_grid_position, move.direction))
move.position = gridPositionToPosition(teleport(current_grid_position));
2021-07-06 10:35:23 +00:00
const bool invalid_position = (move.position.x < 0 || move.position.y < 0);
if (invalid_position)
continue;
const bool opposite_direction = (move.direction == oppositeDirection(direction));
if (opposite_direction)
continue;
2021-09-10 09:02:37 +00:00
const GridPosition grid_position = { size_t(move.position.x), size_t(move.position.y) };
2021-07-28 13:41:32 +00:00
const bool can_walk = isWalkableForGhost(grid_position, current_grid_position, isEyes());
2021-07-06 10:35:23 +00:00
if (!can_walk)
continue;
move.distance_to_target = std::hypot(move.position.x - target_position.x, move.position.y - target_position.y);
2021-06-28 10:42:21 +00:00
}
2021-07-06 10:35:23 +00:00
const auto optimal_move = std::min_element(possible_moves.begin(), possible_moves.end(), [](const auto & a, const auto & b) {
return a.distance_to_target < b.distance_to_target;
2021-06-28 10:42:21 +00:00
});
const auto & move = *optimal_move;
2021-07-06 15:09:42 +00:00
direction = move.direction;
2021-07-06 10:35:23 +00:00
last_grid_position = current_grid_position;
}
2021-06-28 10:42:21 +00:00
void Ghost::updateAnimation(std::chrono::milliseconds time_delta) {
2021-09-10 12:44:40 +00:00
timeForAnimation += double(time_delta.count());
2021-06-28 10:42:21 +00:00
if (timeForAnimation >= 250) {
timeForAnimation = 0;
animationIndex = (animationIndex + 1) % 4;
}
2021-06-24 11:32:52 +00:00
}
2021-07-29 09:16:08 +00:00
/*
* Ghosts alternate between the scatter and chase states at
* specific intervals
*/
2021-08-02 12:09:03 +00:00
Ghost::State Ghost::defaultStateAtDuration(std::chrono::seconds seconds) {
2021-07-29 09:16:08 +00:00
// This array denotes the duration of each state, alternating between scatter and chase
std::array changes = { /*scatter*/ 7, 20, 7, 20, 5, 20, 5 };
2021-07-29 09:16:08 +00:00
// 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));
2021-08-02 12:09:03 +00:00
// 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());
2021-07-29 09:16:08 +00:00
// We get the position of that iterator in the array
auto count = std::distance(std::begin(changes), it);
2021-09-10 14:19:38 +00:00
// Because the first position is scatter, all the even positions will be "scatter"
// all the odd positions will be "chase"
2021-07-29 09:16:08 +00:00
return count % 2 == 0 ? State::Scatter : State::Chase;
}
2021-07-05 12:10:01 +00:00
} // namespace pacman