Merge pull request 'des/ndctt2021' (#1) from des/ndctt2021 into main

Reviewed-on: #1
This commit is contained in:
Dag-Erling Smørgrav 2021-10-20 08:00:21 +00:00
commit 334d41441c
15 changed files with 177 additions and 38 deletions

View File

@ -59,13 +59,16 @@ static Cell cellAtPosition(GridPosition point) {
return Cell(board[point.y][point.x]); return Cell(board[point.y][point.x]);
} }
bool isWall(GridPosition point) {
return cellAtPosition(point) == Cell::wall;
}
bool isWalkableForPacMan(GridPosition point) { bool isWalkableForPacMan(GridPosition point) {
return cellAtPosition(point) != Cell::wall && cellAtPosition(point) != Cell::pen; return !isWall(point) && !isInPen(point);
} }
bool isWalkableForGhost(GridPosition target_position, GridPosition current_position, bool isEyes) { bool isWalkableForGhost(GridPosition target_position, GridPosition current_position, bool isEyes) {
const Cell cell = cellAtPosition(target_position); if (isWall(target_position))
if (cell == Cell::wall)
return false; return false;
return isEyes || isInPen(current_position) || !isInPen(target_position); return isEyes || isInPen(current_position) || !isInPen(target_position);
} }

View File

@ -36,9 +36,10 @@ void Canvas::render(const GameState & gameState) {
renderPellets(gameState.pellets); renderPellets(gameState.pellets);
renderSuperPellets(gameState.superPellets); renderSuperPellets(gameState.superPellets);
renderGhost(gameState.blinky); renderGhost(gameState.ghosts.blinky);
renderGhost(gameState.pinky); renderGhost(gameState.ghosts.pinky);
renderGhost(gameState.inky); renderGhost(gameState.ghosts.inky);
renderGhost(gameState.ghosts.dave);
renderScore(gameState.score.points); renderScore(gameState.score.points);
renderLives(gameState.score.lives); renderLives(gameState.score.lives);

38
lib/Dave.cpp Normal file
View File

@ -0,0 +1,38 @@
#include "Dave.hpp"
namespace pacman {
Dave::Dave()
: Ghost(Atlas::Ghost::dave) {
pos = initialPosition();
}
double Dave::speed() const {
if (state == State::Eyes)
return 2;
if (state == State::Frightened)
return 0.5;
return 0.75;
}
void Dave::setTarget(Position pacManPos) {
if (isInPen()) {
target = penDoorPosition();
return;
}
if (positionDistance(pos, pacManPos) > 8) {
target = pacManPos;
} else {
target = scatterTarget();
}
}
Position Dave::initialPosition() const {
return { 15.5, 14 };
}
Position Dave::scatterTarget() const {
return { 0, 30 };
}
} // namespace pacman

View File

@ -40,6 +40,9 @@ void Game::processEvents(InputState & inputState) {
if (sf::Keyboard::isKeyPressed(sf::Keyboard::A)) { if (sf::Keyboard::isKeyPressed(sf::Keyboard::A)) {
inputState.enableAI = !inputState.enableAI; inputState.enableAI = !inputState.enableAI;
} }
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Q)) {
exit(0);
}
inputState.down = sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Down); inputState.down = sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Down);
inputState.up = sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Up); inputState.up = sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Up);

View File

@ -18,18 +18,11 @@ void GameState::step(std::chrono::milliseconds delta) {
if (!pacMan.hasDirection()) if (!pacMan.hasDirection())
return; return;
blinky.setTarget(pacMan.position()); ghosts.setTarget(pacMan.positionInGrid(), pacMan.currentDirection());
blinky.update(delta); ghosts.update(delta);
pinky.setTarget(pacMan.positionInGrid(), pacMan.currentDirection());
pinky.update(delta);
inky.setTarget(pacMan.positionInGrid(), pacMan.currentDirection(), blinky.positionInGrid());
inky.update(delta);
fruit.update(delta, score.eatenPellets); fruit.update(delta, score.eatenPellets);
ghosts.checkCollision(*this);
checkCollision(blinky);
checkCollision(pinky);
checkCollision(inky);
eatPellets(); eatPellets();
eatFruit(); eatFruit();
@ -55,9 +48,7 @@ void GameState::handleDeathAnimation(std::chrono::milliseconds delta) {
timeSinceDeath += delta; timeSinceDeath += delta;
if (timeSinceDeath.count() > 1000) { if (timeSinceDeath.count() > 1000) {
blinky.reset(); ghosts.reset();
pinky.reset();
inky.reset();
pacMan.reset(); pacMan.reset();
pacManAI.reset(); pacManAI.reset();
timeSinceDeath = std::chrono::milliseconds(0); timeSinceDeath = std::chrono::milliseconds(0);
@ -74,10 +65,7 @@ void GameState::eatPellets() {
if (superPellets.eatPelletAtPosition(pos)) { if (superPellets.eatPelletAtPosition(pos)) {
score.eatenPellets++; score.eatenPellets++;
score.points += POWER_PELLET_POINTS; score.points += POWER_PELLET_POINTS;
ghosts.frighten();
blinky.frighten();
pinky.frighten();
inky.frighten();
} }
} }

41
lib/GhostState.cpp Normal file
View File

@ -0,0 +1,41 @@
#include "GhostState.hpp"
#include "GameState.hpp"
namespace pacman {
void GhostState::setTarget(GridPosition pacManPosition, Direction pacManDirection) {
blinky.setTarget(gridPositionToPosition(pacManPosition));
pinky.setTarget(pacManPosition, pacManDirection);
inky.setTarget(pacManPosition, pacManDirection, blinky.positionInGrid());
dave.setTarget(gridPositionToPosition(pacManPosition));
}
void GhostState::update(std::chrono::milliseconds delta) {
blinky.update(delta);
pinky.update(delta);
inky.update(delta);
dave.update(delta);
}
void GhostState::checkCollision(GameState & gameState) {
gameState.checkCollision(blinky);
gameState.checkCollision(pinky);
gameState.checkCollision(inky);
gameState.checkCollision(dave);
}
void GhostState::reset(void) {
blinky.reset();
pinky.reset();
inky.reset();
dave.reset();
}
void GhostState::frighten(void) {
blinky.frighten();
pinky.frighten();
inky.frighten();
dave.frighten();
}
} // namespace pacman

View File

@ -16,22 +16,28 @@ Direction PacManAI::suggestedDirection() const {
// This function is not yet implemented. // This function is not yet implemented.
// You will implement it as part of module 25. // You will implement it as part of module 25.
GridPosition PacManAI::pelletClosestToPacman(GridPosition, GridPosition PacManAI::pelletClosestToPacman(GridPosition position,
std::vector<GridPosition> &) { std::vector<GridPosition> & pellets) {
if (pellets.empty())
return {0, 0}; return { 0, 0 };
return *std::min_element(pellets.begin(), pellets.end(), [position](GridPosition a, GridPosition b) {
return positionDistance(a, position) < positionDistance(b, position);
});
} }
// This function is not yet implemented. // This function is not yet implemented.
// You will implement it as part of module 25. // You will implement it as part of module 25.
bool PacManAI::isValidMove(const Move &) { bool PacManAI::isValidMove(const Move & move) {
return false; return isWalkableForPacMan(move.position) && move.direction != oppositeDirection(direction);
} }
// This function is not yet implemented. // This function is not yet implemented.
// You will implement it as part of module 25. // You will implement it as part of module 25.
Direction PacManAI::optimalDirection(const std::array<Move, 4> &) { Direction PacManAI::optimalDirection(const std::array<Move, 4> & moves) {
return Direction::NONE; auto bestMove = std::min_element(moves.begin(), moves.end(), [](Move a, Move b) {
return a.distanceToTarget < b.distanceToTarget;
});
return bestMove->direction;
} }
void PacManAI::update(const PacMan & pacMan, const Pellets & pellets) { void PacManAI::update(const PacMan & pacMan, const Pellets & pellets) {

View File

@ -12,7 +12,7 @@ enum class Ghost : unsigned int {
blinky = 2, blinky = 2,
pinky = 3, pinky = 3,
inky = 4, inky = 4,
clyde = 5, dave = 5,
}; };
constexpr GridPosition pacman_right_wide = { 0, 0 }; constexpr GridPosition pacman_right_wide = { 0, 0 };
@ -46,7 +46,7 @@ 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::dave && "Invalid Ghost");
auto y = static_cast<size_t>(ghost); auto y = static_cast<size_t>(ghost);
size_t x = 0; size_t x = 0;
switch (direction) { switch (direction) {

View File

@ -8,6 +8,7 @@
namespace pacman { namespace pacman {
bool isWall(GridPosition point);
bool isWalkableForPacMan(GridPosition point); bool isWalkableForPacMan(GridPosition point);
bool isWalkableForGhost(GridPosition target_position, GridPosition current_position, bool isEyes); bool isWalkableForGhost(GridPosition target_position, GridPosition current_position, bool isEyes);
bool isInPen(GridPosition point); bool isInPen(GridPosition point);

20
lib/include/Dave.hpp Normal file
View File

@ -0,0 +1,20 @@
#pragma once
#include "Ghost.hpp"
namespace pacman {
class Dave final : public Ghost {
public:
Dave();
void setTarget(Position pacManPos);
protected:
double speed() const override;
Position initialPosition() const override;
private:
Position scatterTarget() const;
};
} // namespace pacman

View File

@ -1,8 +1,10 @@
#pragma once #pragma once
#include "Blinky.hpp" #include "Blinky.hpp"
#include "Dave.hpp"
#include "Fruits.hpp" #include "Fruits.hpp"
#include "Ghost.hpp" #include "Ghost.hpp"
#include "GhostState.hpp"
#include "Inky.hpp" #include "Inky.hpp"
#include "InputState.hpp" #include "InputState.hpp"
#include "PacMan.hpp" #include "PacMan.hpp"
@ -17,9 +19,7 @@ namespace pacman {
struct GameState { struct GameState {
void step(std::chrono::milliseconds delta); void step(std::chrono::milliseconds delta);
Blinky blinky; GhostState ghosts;
Pinky pinky;
Inky inky;
PacMan pacMan; PacMan pacMan;
PacManAI pacManAI; PacManAI pacManAI;

View File

@ -0,0 +1,26 @@
#pragma once
#include "Blinky.hpp"
#include "Dave.hpp"
#include "Inky.hpp"
#include "Pinky.hpp"
namespace pacman {
class GameState;
class GhostState {
public:
void setTarget(GridPosition pacManPosition, Direction pacManDirection);
void update(std::chrono::milliseconds delta);
void checkCollision(GameState & gameState);
void reset();
void frighten();
Blinky blinky;
Pinky pinky;
Inky inky;
Dave dave;
};
} // namespace pacman

View File

@ -8,4 +8,4 @@ target_link_libraries(pacman_tests Catch2::Catch2 libpacman)
# This setup the tests on CI. We disable the AI tests # This setup the tests on CI. We disable the AI tests
# because you will have to implement them. # because you will have to implement them.
add_test(NAME pacman_tests COMMAND pacman_tests "~[AI]") add_test(NAME pacman_tests COMMAND pacman_tests)

View File

@ -65,4 +65,15 @@ TEST_CASE("Teleport", "[board]") {
const pacman::GridPosition result = pacman::teleport(portalLeft); const pacman::GridPosition result = pacman::teleport(portalLeft);
REQUIRE(result.x == portalRight.x); REQUIRE(result.x == portalRight.x);
} }
} }
TEST_CASE("Is wall", "[board]") {
REQUIRE(pacman::isWall(pacman::GridPosition{ 0, 0 }));
REQUIRE(pacman::isWall(pacman::GridPosition{ 27, 30 }));
REQUIRE(!pacman::isWall(pacman::GridPosition{ 1, 1 }));
REQUIRE(!pacman::isWall(pacman::GridPosition{ 26, 29 }));
REQUIRE(!pacman::isWall(pacman::GridPosition{ 0, 14 }));
REQUIRE(!pacman::isWall(pacman::GridPosition{ 27, 14 }));
REQUIRE(!pacman::isWall(pacman::GridPosition{ 11, 13 }));
REQUIRE(!pacman::isWall(pacman::GridPosition{ 16, 15 }));
}

View File

@ -7,6 +7,7 @@ TEST_CASE("Find pellet closest to PacMan", "[AI]") {
PacManAI AI; PacManAI AI;
using TestData = std::tuple<GridPosition, std::vector<GridPosition>, GridPosition>; using TestData = std::tuple<GridPosition, std::vector<GridPosition>, GridPosition>;
auto data = GENERATE( auto data = GENERATE(
TestData{{5, 5}, {}, {0, 0}},
TestData{{5, 5}, {{5, 6}}, {5, 6}}, TestData{{5, 5}, {{5, 6}}, {5, 6}},
TestData{{5, 5}, {{5, 5}}, {5, 5}}, TestData{{5, 5}, {{5, 5}}, {5, 5}},
TestData{{5, 5}, {{0, 0}, {5, 6}}, {5, 6}}, TestData{{5, 5}, {{0, 0}, {5, 6}}, {5, 6}},