Merge pull request #32 from mod-cpp/olafurw/ai_exercise
Merging AI exercise code into the repository.
This commit is contained in:
commit
b9a5169eab
15 changed files with 203 additions and 34 deletions
|
@ -6,14 +6,14 @@ const std::size_t ROWS = 31;
|
|||
const std::size_t COLUMNS = 28;
|
||||
|
||||
enum class Cell {
|
||||
wall = 0,
|
||||
pellet = 1,
|
||||
// nothing = 2,
|
||||
// missing 3,
|
||||
power_pellet = 4,
|
||||
pen = 5,
|
||||
left_portal = 6,
|
||||
right_portal = 7
|
||||
wall = 0,
|
||||
pellet = 1,
|
||||
// nothing = 2,
|
||||
// missing 3,
|
||||
power_pellet = 4,
|
||||
pen = 5,
|
||||
left_portal = 6,
|
||||
right_portal = 7
|
||||
};
|
||||
|
||||
// clang-format off
|
||||
|
@ -124,4 +124,26 @@ std::vector<GridPosition> initialSuperPelletPositions() {
|
|||
return positions;
|
||||
}
|
||||
|
||||
// AI
|
||||
|
||||
bool isIntersection(GridPosition point) {
|
||||
if (!isWalkableForPacMan(point) || cellAtPosition(point) == Cell::left_portal || cellAtPosition(point) == Cell::right_portal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const GridPosition right{ point.x + 1, point.y };
|
||||
const bool rightWalkable = isWalkableForPacMan(right);
|
||||
|
||||
const GridPosition left{ point.x - 1, point.y };
|
||||
const bool leftWalkable = isWalkableForPacMan(left);
|
||||
|
||||
const GridPosition top{ point.x, point.y - 1 };
|
||||
const bool topWalkable = isWalkableForPacMan(top);
|
||||
|
||||
const GridPosition bottom{ point.x, point.y + 1 };
|
||||
const bool bottomWalkable = isWalkableForPacMan(bottom);
|
||||
|
||||
return (topWalkable && rightWalkable) || (rightWalkable && bottomWalkable) || (bottomWalkable && leftWalkable) || (leftWalkable && topWalkable);
|
||||
}
|
||||
|
||||
} // namespace pacman
|
||||
|
|
|
@ -28,10 +28,10 @@ Position Clyde::target(const GameState & gameState) const {
|
|||
if (state == State::Scatter)
|
||||
return targetPosition;
|
||||
|
||||
const auto pacManPosition = gameState.pacMan.positionInGrid();
|
||||
auto distanceFomPacMan = std::hypot(pos.x - double(pacManPosition.x), pos.y - double(pacManPosition.y));
|
||||
const auto pacManPosition = gameState.pacMan.position();
|
||||
const auto distanceFomPacMan = std::hypot(pos.x - double(pacManPosition.x), pos.y - double(pacManPosition.y));
|
||||
if (distanceFomPacMan > 8)
|
||||
targetPosition = gridPositionToPosition(pacManPosition);
|
||||
targetPosition = pacManPosition;
|
||||
|
||||
return targetPosition;
|
||||
}
|
||||
|
@ -44,4 +44,4 @@ Position Clyde::scatterTarget() const {
|
|||
return { 0, 30 };
|
||||
}
|
||||
|
||||
}
|
||||
} // namespace pacman
|
||||
|
|
|
@ -7,7 +7,8 @@ constexpr int NORMAL_PELLET_POINTS = 10;
|
|||
constexpr int POWER_PELLET_POINTS = 50;
|
||||
|
||||
void GameState::step(std::chrono::milliseconds delta) {
|
||||
pacMan.update(delta, inputState.direction());
|
||||
pacManAI.update(pacMan, pellets);
|
||||
pacMan.update(delta, pacManAI.suggestedDirection());
|
||||
|
||||
if (isPacManDying()) {
|
||||
handleDeathAnimation(delta);
|
||||
|
@ -18,7 +19,7 @@ void GameState::step(std::chrono::milliseconds delta) {
|
|||
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
|
||||
pinky.update(delta, *this); // ghosts know what they want, which is usually pacman's location
|
||||
inky.update(delta, *this);
|
||||
clyde.update(delta, *this);
|
||||
fruit.update(delta, *this);
|
||||
|
@ -80,14 +81,14 @@ void GameState::eatPellets() {
|
|||
}
|
||||
|
||||
void GameState::eatFruit() {
|
||||
const auto pos = pacMan.positionInGrid();
|
||||
const auto fruitpos = positionToGridPosition(fruit.position());
|
||||
const auto pos = pacMan.positionInGrid();
|
||||
const auto fruitpos = positionToGridPosition(fruit.position());
|
||||
|
||||
// TODO: hitboxes based collision
|
||||
if(fruit.isVisible() && pos == fruitpos) {
|
||||
score.points += fruit.eat();
|
||||
score.eatenFruits++;
|
||||
}
|
||||
// TODO: hitboxes based collision
|
||||
if (fruit.isVisible() && pos == fruitpos) {
|
||||
score.points += fruit.eat();
|
||||
score.eatenFruits++;
|
||||
}
|
||||
}
|
||||
|
||||
void GameState::killPacMan() {
|
||||
|
@ -100,4 +101,4 @@ bool GameState::isPacManDying() const {
|
|||
return timeSinceDeath.count() != 0;
|
||||
}
|
||||
|
||||
}
|
||||
} // namespace pacman
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
#include "Ghost.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <numeric>
|
||||
#include <algorithm>
|
||||
|
||||
namespace pacman {
|
||||
|
||||
|
@ -13,6 +13,7 @@ Ghost::Ghost(Atlas::Ghost spriteSet)
|
|||
void Ghost::frighten() {
|
||||
if (state > State::Scatter)
|
||||
return;
|
||||
|
||||
direction = oppositeDirection(direction);
|
||||
state = State::Frightened;
|
||||
timeFrighten = {};
|
||||
|
@ -29,6 +30,7 @@ bool Ghost::isEyes() const {
|
|||
void Ghost::die() {
|
||||
if (state == State::Eyes)
|
||||
return;
|
||||
|
||||
direction = oppositeDirection(direction);
|
||||
state = State::Eyes;
|
||||
timeFrighten = {};
|
||||
|
@ -42,7 +44,7 @@ void Ghost::reset() {
|
|||
timeChase = {};
|
||||
}
|
||||
|
||||
GridPosition Ghost::currentSprite() const {
|
||||
GridPosition Ghost::currentSprite() const {
|
||||
switch (state) {
|
||||
default:
|
||||
return Atlas::ghostSprite(spriteSet, direction, (animationIndex % 2) == 0);
|
||||
|
@ -64,6 +66,10 @@ GridPosition Ghost::positionInGrid() const {
|
|||
return positionToGridPosition(pos);
|
||||
}
|
||||
|
||||
Direction Ghost::currentDirection() const {
|
||||
return direction;
|
||||
}
|
||||
|
||||
void Ghost::update(std::chrono::milliseconds time_delta, const GameState & gameState) {
|
||||
if (state == State::Eyes && isInPen())
|
||||
state = State::Scatter;
|
||||
|
@ -122,8 +128,7 @@ 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())) {
|
||||
} else if (!isWalkableForGhost(positionInGrid(), old_grid_position, isEyes())) {
|
||||
pos = old_position;
|
||||
direction = oppositeDirection(direction);
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ Position Inky::target(const GameState & gameState) const {
|
|||
|
||||
// Then it calculates the distance between Blinky and this position
|
||||
const auto & blinkyPosition = gameState.blinky.positionInGrid();
|
||||
double distanceBetweenBlinkyAndTarget = std::hypot(blinkyPosition.x - targetPosition.x, blinkyPosition.y - targetPosition.y);
|
||||
const double distanceBetweenBlinkyAndTarget = std::hypot(blinkyPosition.x - targetPosition.x, blinkyPosition.y - targetPosition.y);
|
||||
|
||||
// And selects a point on the line crossing blinky and this position that is at twice that distance
|
||||
// away from blinky
|
||||
|
@ -67,4 +67,4 @@ Position Inky::scatterTarget() const {
|
|||
return { 27, 30 };
|
||||
}
|
||||
|
||||
}
|
||||
} // namespace pacman
|
||||
|
|
80
lib/PacManAI.cpp
Normal file
80
lib/PacManAI.cpp
Normal file
|
@ -0,0 +1,80 @@
|
|||
#include "PacManAI.hpp"
|
||||
|
||||
#include "Board.hpp"
|
||||
#include <fmt/format.h>
|
||||
|
||||
namespace pacman {
|
||||
Direction PacManAI::suggestedDirection() const {
|
||||
return direction;
|
||||
}
|
||||
|
||||
void PacManAI::update(const PacMan & pacMan, const Pellets & pellets) {
|
||||
const GridPosition pacManGridPos = pacMan.positionInGrid();
|
||||
const GridPosition currentGridPos = positionToGridPosition(pos);
|
||||
if (currentGridPos == pacManGridPos) {
|
||||
return;
|
||||
}
|
||||
|
||||
pos = gridPositionToPosition(pacManGridPos);
|
||||
|
||||
if (!isIntersection(pacManGridPos)) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto pelletPositions = pellets.currentPositions();
|
||||
if (pelletPositions.empty()) {
|
||||
return;
|
||||
}
|
||||
auto pelletSort = [&pacManGridPos](GridPosition pelletA, GridPosition pelletB) {
|
||||
double distanceA = positionDistance(pacManGridPos, pelletA);
|
||||
double distanceB = positionDistance(pacManGridPos, pelletB);
|
||||
return distanceA < distanceB;
|
||||
};
|
||||
std::sort(pelletPositions.begin(), pelletPositions.end(), pelletSort);
|
||||
|
||||
const auto & target = pelletPositions[0];
|
||||
const Position targetPos{ double(target.x), double(target.y) };
|
||||
|
||||
struct Move {
|
||||
Direction direction = Direction::NONE;
|
||||
Position position;
|
||||
double distanceToTarget = std::numeric_limits<double>::infinity();
|
||||
};
|
||||
|
||||
const Position currentPosition = { double(pacManGridPos.x), double(pacManGridPos.y) };
|
||||
const auto [x, y] = currentPosition;
|
||||
std::array<Move, 4> possibleMoves = {
|
||||
Move{ Direction::UP, { x, y - 1 } },
|
||||
Move{ Direction::LEFT, { x - 1, y } },
|
||||
Move{ Direction::DOWN, { x, y + 1 } },
|
||||
Move{ Direction::RIGHT, { x + 1, y } }
|
||||
};
|
||||
|
||||
for (auto & move : possibleMoves) {
|
||||
const bool invalidPosition = (move.position.x < 0 || move.position.y < 0);
|
||||
if (invalidPosition) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool isOpposite = (move.direction == oppositeDirection(direction));
|
||||
if (isOpposite) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const GridPosition gridMove = { size_t(move.position.x), size_t(move.position.y) };
|
||||
const bool canWalk = isWalkableForPacMan(gridMove);
|
||||
if (!canWalk) {
|
||||
continue;
|
||||
}
|
||||
|
||||
move.distanceToTarget = positionDistance(move.position, targetPos);
|
||||
}
|
||||
|
||||
const auto optimalMove = std::min_element(possibleMoves.begin(), possibleMoves.end(), [](const auto & a, const auto & b) {
|
||||
return a.distanceToTarget < b.distanceToTarget;
|
||||
});
|
||||
|
||||
const auto & move = *optimalMove;
|
||||
direction = move.direction;
|
||||
}
|
||||
} // namespace pacman
|
|
@ -6,6 +6,14 @@ namespace pacman {
|
|||
Pellets::Pellets()
|
||||
: positions(initialPelletPositions()) {}
|
||||
|
||||
bool Pellets::isPellet(GridPosition p) const {
|
||||
auto match = [&p](GridPosition pellet) {
|
||||
return p.x == pellet.x && p.y == pellet.y;
|
||||
};
|
||||
|
||||
return std::any_of(positions.begin(), positions.end(), match);
|
||||
}
|
||||
|
||||
bool Pellets::eatPelletAtPosition(GridPosition p) {
|
||||
auto it = std::find(positions.begin(), positions.end(), p);
|
||||
if (it == positions.end())
|
||||
|
|
|
@ -6,6 +6,14 @@ namespace pacman {
|
|||
SuperPellets::SuperPellets()
|
||||
: positions(initialSuperPelletPositions()) {}
|
||||
|
||||
bool SuperPellets::isPellet(GridPosition p) const {
|
||||
auto match = [&p](GridPosition pellet) {
|
||||
return p.x == pellet.x && p.y == pellet.y;
|
||||
};
|
||||
|
||||
return std::any_of(positions.begin(), positions.end(), match);
|
||||
}
|
||||
|
||||
bool SuperPellets::eatPelletAtPosition(GridPosition p) {
|
||||
auto it = std::find(positions.begin(), positions.end(), p);
|
||||
if (it == positions.end())
|
||||
|
|
|
@ -18,7 +18,13 @@ std::vector<GridPosition> initialPelletPositions();
|
|||
|
||||
std::vector<GridPosition> 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 };
|
||||
}
|
||||
|
||||
bool isIntersection(GridPosition point);
|
||||
|
||||
} // namespace pacman
|
||||
|
|
|
@ -2,15 +2,16 @@
|
|||
|
||||
#include "Blinky.hpp"
|
||||
#include "Clyde.hpp"
|
||||
#include "Fruits.hpp"
|
||||
#include "Ghost.hpp"
|
||||
#include "Inky.hpp"
|
||||
#include "InputState.hpp"
|
||||
#include "PacMan.hpp"
|
||||
#include "PacManAI.hpp"
|
||||
#include "Pellets.hpp"
|
||||
#include "Pinky.hpp"
|
||||
#include "Score.hpp"
|
||||
#include "SuperPellets.hpp"
|
||||
#include "Fruits.hpp"
|
||||
#include "InputState.hpp"
|
||||
|
||||
namespace pacman {
|
||||
|
||||
|
@ -23,6 +24,7 @@ struct GameState {
|
|||
Clyde clyde;
|
||||
|
||||
PacMan pacMan;
|
||||
PacManAI pacManAI;
|
||||
InputState inputState;
|
||||
Pellets pellets;
|
||||
SuperPellets superPellets;
|
||||
|
|
|
@ -25,6 +25,7 @@ public:
|
|||
GridPosition currentSprite() const;
|
||||
Position position() const;
|
||||
GridPosition positionInGrid() const;
|
||||
Direction currentDirection() const;
|
||||
|
||||
void update(std::chrono::milliseconds time_delta, const GameState & gameState);
|
||||
void frighten();
|
||||
|
|
25
lib/include/PacManAI.hpp
Normal file
25
lib/include/PacManAI.hpp
Normal file
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include "Blinky.hpp"
|
||||
#include "Clyde.hpp"
|
||||
#include "Direction.hpp"
|
||||
#include "Inky.hpp"
|
||||
#include "PacMan.hpp"
|
||||
#include "Pellets.hpp"
|
||||
#include "Pinky.hpp"
|
||||
#include "Position.hpp"
|
||||
#include "SuperPellets.hpp"
|
||||
|
||||
namespace pacman {
|
||||
|
||||
class PacManAI {
|
||||
public:
|
||||
void update(const PacMan & pacMan, const Pellets & pellets);
|
||||
Direction suggestedDirection() const;
|
||||
|
||||
private:
|
||||
Position pos;
|
||||
Direction direction = Direction::RIGHT;
|
||||
};
|
||||
|
||||
} // namespace pacman
|
|
@ -17,6 +17,7 @@ public:
|
|||
return positions;
|
||||
}
|
||||
|
||||
bool isPellet(GridPosition p) const;
|
||||
bool eatPelletAtPosition(GridPosition p);
|
||||
|
||||
private:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace pacman {
|
||||
|
@ -14,7 +14,9 @@ struct Position {
|
|||
struct GridPosition {
|
||||
size_t x;
|
||||
size_t y;
|
||||
constexpr GridPosition(size_t x, size_t y) : x(x), y(y) {}
|
||||
constexpr GridPosition(size_t x, size_t y)
|
||||
: x(x),
|
||||
y(y) {}
|
||||
};
|
||||
|
||||
inline GridPosition positionToGridPosition(Position pos) {
|
||||
|
@ -34,6 +36,13 @@ constexpr bool operator!=(const GridPosition & a, const GridPosition & b) {
|
|||
return !(a == b);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline double positionDistance(const T & a, const T & b) {
|
||||
const double first = double(a.x) - double(b.x);
|
||||
const double second = double(a.y) - double(b.y);
|
||||
return std::sqrt((first * first) + (second * second));
|
||||
}
|
||||
|
||||
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<double>::epsilon();
|
||||
|
|
|
@ -17,6 +17,7 @@ public:
|
|||
return positions;
|
||||
}
|
||||
|
||||
bool isPellet(GridPosition p) const;
|
||||
bool eatPelletAtPosition(GridPosition p);
|
||||
|
||||
private:
|
||||
|
|
Loading…
Reference in a new issue