Skip to the content.

Task 5: Better safe than sorry

Execute only safe commands and add a fallback strategy.

Context

The arbitration graph is now complete and Pac-Man is eating dots like a pro. But there is one last topic we want to talk about: safety and robustness.

Depending on your application, you might only want to execute commands that you know meet certain criteria. The specific requirements will depend on your application and could be anything from physical constraints to safety requirements. In our case, we only want to execute commands where Pac-Man does not run into walls.

We can ensure that commands obey these requirements by adding a verifier to the arbitrators. The arbitrator will then run the verification step and only choose commands that pass this step.

The leads us to another issue. What to do if the command we wanted to execute does not pass the verification step?

Glad you asked! The first thing that happens out-of-the-box: the arbitrator will just choose the next best option passing verification. E.g., if the EatClosestDot is not safe, the EatDot arbitrator will just return the ChangeDotCluster command to the root arbitrator in case ChangeDotCluster is both applicable and does itself pass verification.

If that’s not the case though, we can think about adding additional behavior components as fallback layers to enable graceful degradation of the system. The first one is already there: MoveRandomly is something we probably don’t really want to do under normal circumstances. But if we run out of ideas, it is still a valid option. It might also give our main behavior components a chance to recover or to solve deadlock situations.

Finally, it is a good idea to add a last resort fallback layer. This behavior component should be a simple implementation that is always applicable and does not require a lot of context knowledge. If the system is in a failing state, the latter might not be available. We can mark a behavior component as last resort fallback layer in order to exclude it from verification. After all, it’s our last straw and it’s better to execute that than to do nothing.

In our case, we will add a StayInPlace behavior component. Pac-Man is not actually able to stop, so he will just keep moving back and forth. Probably not an ideal strategy to win the game, but we can be sure to have a comprehensible command at all times. Also, Pac-Man will never run into a wall with this behavior component.

Phew, that was a long read. Time to get our hands dirty!

Goal

Finish the implementation of the Verifier class and have the existing arbitrators use it. Add the MoveRandomly behavior component as a last resort fallback layer.

Instructions

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:

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
    return VerificationResult{environmentModel_->isPassableCell(nextPosition)};
}

Include the verifier header you just implemented, in include/demo/pacman_agent.hpp. Also, include stay_in_place_behavior.hpp.

#include "stay_in_place_behavior.hpp"
#include "verifier.hpp"

Adjust the template parameters in the alias definitions to contain the verifier types:

public:
    using CostArbitrator = arbitration_graphs::CostArbitrator<Command, Command, Verifier, VerificationResult>;
    using PriorityArbitrator = arbitration_graphs::PriorityArbitrator<Command, Command, Verifier, VerificationResult>;

Add the verifier and the fallback behavior component as members of the PacmanAgent class:

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:

    explicit PacmanAgent(const entt::Game& game)
            : parameters_{},
              environmentModel_{std::make_shared<EnvironmentModel>(game)},
              verifier_{environmentModel_} { // We can initialize the verifier in the member initializer list

        avoidGhostBehavior_ = std::make_shared<AvoidGhostBehavior>(environmentModel_, parameters_.avoidGhostBehavior);
        changeDotClusterBehavior_ = std::make_shared<ChangeDotClusterBehavior>(environmentModel_);
        chaseGhostBehavior_ = std::make_shared<ChaseGhostBehavior>(environmentModel_, parameters_.chaseGhostBehavior);
        eatClosestDotBehavior_ = std::make_shared<EatClosestDotBehavior>(environmentModel_);
        moveRandomlyBehavior_ = std::make_shared<MoveRandomlyBehavior>(parameters_.moveRandomlyBehavior);
        // Initialize the StayInPlace behavior component
        stayInPlaceBehavior_ = std::make_shared<StayInPlaceBehavior>(environmentModel_);

        // Pass the verifier instance to the cost arbitrator
        eatDotsArbitrator_ = std::make_shared<CostArbitrator>("EatDots", verifier_);
        costEstimator_ = std::make_shared<CostEstimator>(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<PriorityArbitrator>("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 | Tutorial Home