From 675a92dd0e2f8b3f8e878cf828467fa4cbefc704 Mon Sep 17 00:00:00 2001 From: Nick Le Large Date: Thu, 24 Oct 2024 15:46:00 +0200 Subject: [PATCH] Add solutions to all tasks --- docs/tasks/1_implement_behavior_component.md | 56 +++++++++++++ docs/tasks/2_extend_arbitration_graph.md | 35 +++++++++ docs/tasks/3_add_more_behaviors.md | 37 +++++++++ docs/tasks/4_nested_arbitrators.md | 56 +++++++++++++ docs/tasks/5_cost_arbitration.md | 83 ++++++++++++++++++++ docs/tasks/6_verification.md | 80 +++++++++++++++++++ 6 files changed, 347 insertions(+) diff --git a/docs/tasks/1_implement_behavior_component.md b/docs/tasks/1_implement_behavior_component.md index 82ea1e34..6ba2c9e0 100644 --- a/docs/tasks/1_implement_behavior_component.md +++ b/docs/tasks/1_implement_behavior_component.md @@ -38,6 +38,62 @@ Finish the implementation of the `checkInvocationCondition` and `getCommand` fun - Implement the missing piece. Take a look at the implementation of `AvoidGhostBehavior::getCommand` if you need inspiration. - Compile and run the unit tests for the `ChaseGhost` behavior component to verify that your implementation is correct. +## Solution + +
+Click here to expand the solution + +Fix the invocation condition in `src/chase_ghost_behavior.cpp`: +```cpp +bool ChaseGhostBehavior::checkInvocationCondition(const Time& time) const { + return environmentModel_->closestScaredGhost(time).has_value() && + environmentModel_->closestScaredGhost(time)->ghost.scaredCountdown > parameters_.minScaredTicksLeft && + environmentModel_->closestScaredGhost(time)->distance < parameters_.invocationMinDistance; // Only applicable if a ghost is close by +} +``` + +Add the missing pice of the getCommand function in `src/chase_ghost_behavior.cpp`: +```cpp +Command ChaseGhostBehavior::getCommand(const Time& time) { + auto pacmanPosition = environmentModel_->pacmanPosition(); + + auto closestScaredGhost = environmentModel_->closestScaredGhost(time); + if (!closestScaredGhost) { + throw std::runtime_error("Can not compute command to chase ghost because there are no scared ghosts."); + } + + auto ghostPosition = closestScaredGhost->ghost.position; + + std::optional direction; + + // Add this part: + // Chose the direction moving pacman towards the closest scared ghost + double minDistance = std::numeric_limits::max(); + for (const auto& move : Move::possibleMoves()) { + auto nextPosition = environmentModel_->positionConsideringTunnel(pacmanPosition + move.deltaPosition); + + if (environmentModel_->isWall(nextPosition)) { + continue; + } + + // Chose the direction moving pacman towards the closest scared ghost (considering ghost movement) + auto nextDistance = environmentModel_->mazeDistance(nextPosition, ghostPosition); + if (nextDistance < minDistance) { + direction = move.direction; + minDistance = nextDistance; + } + } + + if (!direction) { + throw std::runtime_error("Failed to compute direction to chase the closest ghost."); + } + + return Command{direction.value()}; +} + +``` +
+ --- [Tutorial Home](../Tutorial.md) diff --git a/docs/tasks/2_extend_arbitration_graph.md b/docs/tasks/2_extend_arbitration_graph.md index 84b09546..99c28ff8 100644 --- a/docs/tasks/2_extend_arbitration_graph.md +++ b/docs/tasks/2_extend_arbitration_graph.md @@ -26,6 +26,41 @@ Integrate the `ChaseGhost` behavior component into the arbitration graph defined - Add a new option to the priority arbitrator. - Run the game, take a look at the new arbitration graph and observe how PacMan behaves. +## Solution + +
+Click here to expand the solution + +Include the header of the `ChaseGhost` behavior component in `include/demo/pacman_agent.hpp`: +```cpp +#include "chase_ghost_behavior.hpp" +``` + +Add the `ChaseGhost` behavior component as a new member of the `PacmanAgent` class: +```cpp +private: + ChaseGhostBehavior::Ptr chaseGhostBehavior_; +``` + +In the constructor of the `PacmanAgent` class, initialize the `ChaseGhost` behavior component and add it to the priority arbitrator: +```cpp +explicit PacmanAgent(const entt::Game& game) + : parameters_{}, environmentModel_{std::make_shared(game)} { + + avoidGhostBehavior_ = std::make_shared(environmentModel_, parameters_.avoidGhostBehavior); + // Initialize the ChaseGhost behavior component + chaseGhostBehavior_ = std::make_shared(environmentModel_, parameters_.chaseGhostBehavior); + moveRandomlyBehavior_ = std::make_shared(parameters_.moveRandomlyBehavior); + + rootArbitrator_ = std::make_shared("Pacman"); + // Add the ChaseGhost behavior component to the priority arbitrator (before the AvoidGhost behavior component!) + rootArbitrator_->addOption(chaseGhostBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + rootArbitrator_->addOption(avoidGhostBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + rootArbitrator_->addOption(moveRandomlyBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); +} +``` +
+ --- [← Previous task](1_implement_behavior_component.md) diff --git a/docs/tasks/3_add_more_behaviors.md b/docs/tasks/3_add_more_behaviors.md index 1ed6508d..2beb9300 100644 --- a/docs/tasks/3_add_more_behaviors.md +++ b/docs/tasks/3_add_more_behaviors.md @@ -25,6 +25,43 @@ Integrate the `EatClosestDot` behavior component into the arbitration graph defi - Integrate the new component just like you did in the last task. - Start the game and see how PacMan stop wandering around aimlessly and starts eating dots. +## Solution + +
+Click here to expand the solution + +Include the header of the `EatClosestDot` behavior component in `include/demo/pacman_agent.hpp`: +```cpp +#include "eat_closest_dot_behavior.hpp" +``` + +Add the `ChaseGhost` behavior component as a new member of the `PacmanAgent` class: +```cpp +private: + EatClosestDotBehavior::Ptr eatClosestDotBehavior_; +``` + +In the constructor of the `PacmanAgent` class, initialize the `ChaseGhost` behavior component and add it to the priority arbitrator: +```cpp +explicit PacmanAgent(const entt::Game& game) + : parameters_{}, environmentModel_{std::make_shared(game)} { + + avoidGhostBehavior_ = std::make_shared(environmentModel_, parameters_.avoidGhostBehavior); + chaseGhostBehavior_ = std::make_shared(environmentModel_, parameters_.chaseGhostBehavior); + // Initialize the EatClosestDot behavior component + eatClosestDotBehavior_ = std::make_shared(environmentModel_); + moveRandomlyBehavior_ = std::make_shared(parameters_.moveRandomlyBehavior); + + rootArbitrator_ = std::make_shared("Pacman"); + rootArbitrator_->addOption(chaseGhostBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + rootArbitrator_->addOption(avoidGhostBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + // Add the EatClosestDot behavior component to the priority arbitrator (after the ghost behavior components!) + rootArbitrator_->addOption(eatClosestDotBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + rootArbitrator_->addOption(moveRandomlyBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); +} +``` +
+ --- diff --git a/docs/tasks/4_nested_arbitrators.md b/docs/tasks/4_nested_arbitrators.md index b6e2eb5e..61fda45c 100644 --- a/docs/tasks/4_nested_arbitrators.md +++ b/docs/tasks/4_nested_arbitrators.md @@ -30,6 +30,62 @@ Add the `EatClosestDot` and `ChangeDotCluster` behavior components to a random a - Add the random arbitrator as an option to the root arbitrator. - Run the game and observe how PacMan behaves. +## Solution + +
+Click here to expand the solution + +Include the header of the `ChangeDotCluster` behavior component and the random arbitrator in `include/demo/pacman_agent.hpp`: +```cpp +#include + +#include "change_dot_cluster_behavior.hpp" +``` + +For easier to read code, add the following alias near the top of the class definition: +```cpp +using RandomArbitrator = arbitration_graphs::RandomArbitrator; +``` + +Add the `ChangeDotCluster` behavior component and the `RandomArbitrator` as a new members of the `PacmanAgent` class: +```cpp +private: + ChangeDotClusterBehavior::Ptr changeDotClusterBehavior_; + + RandomArbitrator::Ptr eatDotsArbitrator_; +``` + +In the constructor of the `PacmanAgent` class, initialize the `ChangeDotCluster` behavior component and the `RandomArbitrator`: +Add the `EatClosestDot` and `ChangeDotCluster` behavior components as options to the random arbitrator. +Finally, add the random arbitrator as an option to the root arbitrator: +```cpp +explicit PacmanAgent(const entt::Game& game) + : parameters_{}, environmentModel_{std::make_shared(game)} { + + avoidGhostBehavior_ = std::make_shared(environmentModel_, parameters_.avoidGhostBehavior); + // Initialize the ChangeDotCluster behavior component + changeDotClusterBehavior_ = std::make_shared(environmentModel_); + chaseGhostBehavior_ = std::make_shared(environmentModel_, parameters_.chaseGhostBehavior); + eatClosestDotBehavior_ = std::make_shared(environmentModel_); + moveRandomlyBehavior_ = std::make_shared(parameters_.moveRandomlyBehavior); + + // Initialize the random arbitrator and add the EatClosestDot and ChangeDotCluster behavior components as options + eatDotsArbitrator_ = std::make_shared("EatDots"); + eatDotsArbitrator_->addOption( changeDotClusterBehavior_, RandomArbitrator::Option::Flags::INTERRUPTABLE); + eatDotsArbitrator_->addOption( eatClosestDotBehavior_, RandomArbitrator::Option::Flags::INTERRUPTABLE); + + rootArbitrator_ = std::make_shared("Pacman"); + rootArbitrator_->addOption(chaseGhostBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + rootArbitrator_->addOption(avoidGhostBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + // The EatDot arbitrator is itself an option of the root arbitrator + rootArbitrator_->addOption(eatDotsArbitrator_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + rootArbitrator_->addOption(moveRandomlyBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); +} +``` + + +
+ --- [← Previous task](3_add_more_behaviors.md) diff --git a/docs/tasks/5_cost_arbitration.md b/docs/tasks/5_cost_arbitration.md index 90ed6fe0..d520312d 100644 --- a/docs/tasks/5_cost_arbitration.md +++ b/docs/tasks/5_cost_arbitration.md @@ -32,6 +32,89 @@ Finish the implementation of the `CostEstimator` and replace the random arbitrat - Add an instance of the `CostEstimator` to the `PacmanAgent` class and initialize it in the constructor. - Replace the random arbitrator with a cost arbitrator in the `PacmanAgent` class. Pass the `CostEstimator` instance to the `addOption` method. +## Solution + +
+Click here to expand the solution + +Finish the implementation of the `CostEstimator` class in `cost_estimator.cpp`: +```cpp +double CostEstimator::estimateCost(const Command& command, bool /*isActive*/) { + Positions absolutePath = utils::toAbsolutePath(command.path, environmentModel_); + + // Compute the number of dots along the path and in the neighborhood of the path end using helper functions + const int nDotsAlongPath = utils::dotsAlongPath(absolutePath, environmentModel_); + const int nDotsInRadius = + utils::dotsInRadius(absolutePath.back(), environmentModel_, parameters_.pathEndNeighborhoodRadius); + const int nDots = nDotsAlongPath + nDotsInRadius; + + if (nDots == 0) { + return std::numeric_limits::max(); + } + + // Compute the size of the path and the neighborhood of the path end + const int pathLength = static_cast(absolutePath.size()); + const int neighborhoodSize = static_cast(std::pow(2 * parameters_.pathEndNeighborhoodRadius + 1, 2)); + const int nCells = pathLength + neighborhoodSize; + + // We can define a cost as the inverse of a benefit. + // Our benefit is a dot density (number of dots / number of examined cells) + return static_cast(nCells) / nDots; +} +``` + +Replace the include of the random arbitrator with the cost arbitrator in `include/demo/pacman_agent.hpp`. +Also, include `cost_estimator.hpp`: +```cpp +#include + +#include "cost_estimator.hpp" +``` + +Change the type of the `eatDotsArbitrator_` member in the `PacmanAgent` class to `CostArbitrator` and add an instance of the `CostEstimator`: +```cpp +private: + CostArbitrator::Ptr eatDotsArbitrator_; + + CostEstimator::Ptr costEstimator_; +``` + +As always, the magic happens in the constructor of the `PacmanAgent` class. +Instantiate the cost estimator and pass it in the `addOption` calls: +```cpp +explicit PacmanAgent(const entt::Game& game) + : parameters_{}, environmentModel_{std::make_shared(game)} { + + avoidGhostBehavior_ = std::make_shared(environmentModel_, parameters_.avoidGhostBehavior); + changeDotClusterBehavior_ = std::make_shared(environmentModel_); + chaseGhostBehavior_ = std::make_shared(environmentModel_, parameters_.chaseGhostBehavior); + eatClosestDotBehavior_ = std::make_shared(environmentModel_); + moveRandomlyBehavior_ = std::make_shared(parameters_.moveRandomlyBehavior); + + // This is now a cost arbitrator + eatDotsArbitrator_ = std::make_shared("EatDots"); + // Construct the cost estimator + costEstimator_ = std::make_shared(environmentModel_, parameters_.costEstimator); + // Add the ChangeDotCluster and EatClosestDot behavior components as options to the + // cost arbitrator while also passing the cost estimator + eatDotsArbitrator_->addOption( + changeDotClusterBehavior_, CostArbitrator::Option::Flags::INTERRUPTABLE, costEstimator_); + eatDotsArbitrator_->addOption( + eatClosestDotBehavior_, CostArbitrator::Option::Flags::INTERRUPTABLE, costEstimator_); + + rootArbitrator_ = std::make_shared("Pacman"); + rootArbitrator_->addOption(chaseGhostBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + rootArbitrator_->addOption(avoidGhostBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + rootArbitrator_->addOption(eatDotsArbitrator_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + rootArbitrator_->addOption(moveRandomlyBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + rootArbitrator_->addOption(stayInPlaceBehavior_, + PriorityArbitrator::Option::Flags::INTERRUPTABLE | + PriorityArbitrator::Option::FALLBACK); +} +``` + +
+ --- [← Previous task](4_nested_arbitrators.md) diff --git a/docs/tasks/6_verification.md b/docs/tasks/6_verification.md index 5a45c55d..ecfcb236 100644 --- a/docs/tasks/6_verification.md +++ b/docs/tasks/6_verification.md @@ -55,6 +55,86 @@ Add the `MoveRandomly` behavior component as a last resort fallback layer. - Try breaking a behavior component on purpose and see how the system reacts. (Try throwing an exception in the `getCommand` method of a behavior component or returning a command that will lead to a collision with a wall.) +## Solution + +
+Click here to expand the solution + +In the `Verifier::analyze` method (in `include/demo/verifier.hpp`), we simply check if the command would lead to an invalid position: +```cpp +VerificationResult analyze(const Time /*time*/, const Command& command) const { + Move nextMove = Move{command.path.front()}; + Position nextPosition = environmentModel_->pacmanPosition() + nextMove.deltaPosition; + + // The command is considered safe if the next position is in bounds and not a wall + if (environmentModel_->isPassableCell(nextPosition)) { + return VerificationResult{true}; + } + + return VerificationResult{false}; +} +``` + +Include the verifier header you just implemented, in `include/demo/pacman_agent.hpp`. +Also, include `stay_in_place_behavior.hpp`. +```cpp +#include "stay_in_place_behavior.hpp" +#include "verifier.hpp" +``` + +Adjust the template parameters in the alias definitions to contain the verifier types: +```cpp +public: + using CostArbitrator = arbitration_graphs::CostArbitrator; + using PriorityArbitrator = arbitration_graphs::PriorityArbitrator; +``` + +Add the verifier and the fallback behavior component as members of the `PacmanAgent` class: +```cpp +private: + StayInPlaceBehavior::Ptr stayInPlaceBehavior_; + + Verifier verifier_; +``` + +In the constructor of the `PacmanAgent` class, initialize the verifier and the `StayInPlace` behavior component. +Make sure to also pass the verifier to the arbitrator constructors: +```cpp + explicit PacmanAgent(const entt::Game& game) + : parameters_{}, + environmentModel_{std::make_shared(game)}, + verifier_{environmentModel_} // We can initialize the verifier in the member initializer list { + + avoidGhostBehavior_ = std::make_shared(environmentModel_, parameters_.avoidGhostBehavior); + changeDotClusterBehavior_ = std::make_shared(environmentModel_); + chaseGhostBehavior_ = std::make_shared(environmentModel_, parameters_.chaseGhostBehavior); + eatClosestDotBehavior_ = std::make_shared(environmentModel_); + moveRandomlyBehavior_ = std::make_shared(parameters_.moveRandomlyBehavior); + // Initialize the StayInPlace behavior component + stayInPlaceBehavior_ = std::make_shared(environmentModel_); + + // Pass the verifier instance to the cost arbitrator + eatDotsArbitrator_ = std::make_shared("EatDots", verifier_); + costEstimator_ = std::make_shared(environmentModel_, parameters_.costEstimator); + eatDotsArbitrator_->addOption( + changeDotClusterBehavior_, CostArbitrator::Option::Flags::INTERRUPTABLE, costEstimator_); + eatDotsArbitrator_->addOption( + eatClosestDotBehavior_, CostArbitrator::Option::Flags::INTERRUPTABLE, costEstimator_); + + // Pass the verifier instance to the priority arbitrator + rootArbitrator_ = std::make_shared("Pacman", verifier_); + rootArbitrator_->addOption(chaseGhostBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + rootArbitrator_->addOption(avoidGhostBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + rootArbitrator_->addOption(eatDotsArbitrator_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + rootArbitrator_->addOption(moveRandomlyBehavior_, PriorityArbitrator::Option::Flags::INTERRUPTABLE); + // Add the StayInPlace behavior component. Mark it as a last resort fallback layer using the FALLBACK flag. + rootArbitrator_->addOption(stayInPlaceBehavior_, + PriorityArbitrator::Option::Flags::INTERRUPTABLE | + PriorityArbitrator::Option::FALLBACK); + } +``` +
+ --- [← Previous task](5_cost_arbitration.md)