diff --git a/README.md b/README.md index 73fcdd57..7a4d214e 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,17 @@ In this scene,

-We will shortly add a [tutorial](https://github.com/KIT-MRT/arbitration_graphs/pull/51) based on this demo – stay tuned! +## Tutorial + +Follow our [Tutorial](./docs/Tutorial.md) and learn how to use the Arbitration Graphs library! +It's based on this demo and guides you through all important concepts: + +0. [Introduction – start here!](./docs/Tutorial.md) +1. [Implement your first behavior component](./docs/tasks/1_implement_behavior_component.md) +2. [Extend the arbitration graph with that behavior](./docs/tasks/2_extend_arbitration_graph.md) +3. [Learn about nested arbitration graphs](./docs/tasks/3_nested_arbitrators.md) +4. [Arbitrate based on predicted utility](./docs/tasks/4_cost_arbitration.md) +5. [Verify commands and add a fallback strategy](./docs/tasks/5_verification.md) ## Installation diff --git a/_config.yml b/_config.yml index 100338af..b881b7a5 100644 --- a/_config.yml +++ b/_config.yml @@ -17,6 +17,8 @@ layouts_dir: docs/_layouts sass: sass_dir: docs/assets/_sass +# This will be used as default HTML (sub)title +tagline: decision-making for robotics # Support collapsible details/summary sections markdown: CommonMarkGhPages diff --git a/demo/.devcontainer/.bashrc b/demo/.devcontainer/.bashrc new file mode 100644 index 00000000..b8752c22 --- /dev/null +++ b/demo/.devcontainer/.bashrc @@ -0,0 +1,4 @@ +# start Starship prompt +eval "$(starship init bash)" + +source /home/blinky/.motd \ No newline at end of file diff --git a/demo/.devcontainer/.zshrc b/demo/.devcontainer/.zshrc new file mode 100644 index 00000000..5875b839 --- /dev/null +++ b/demo/.devcontainer/.zshrc @@ -0,0 +1,89 @@ +# >>> reference: https://carlosneto.dev/blog/2024/2024-02-08-starship-zsh/ + +# list files with details +alias ll="ls -larht" + + +# set the locale of the shell +export LANG="en_US.UTF-8" + +# define VSCode as the default text editor +export EDITOR="code -w" + +# specify characters considered as word boundaries for command line navigation +export WORDCHARS="" + +# set the location and filename of the history file +export HISTFILE="$HOME/.zsh_history" + +# set the maximum number of lines to be saved in the history file +export HISTSIZE="100000" +export SAVEHIST="$HISTSIZE" + +# disable CTRL + S and CTRL + Q +stty -ixon + +# enable comments "#" expressions in the prompt shell +setopt INTERACTIVE_COMMENTS + +# append new history entries to the history file +setopt APPEND_HISTORY + +# save each command to the history file as soon as it is executed +setopt INC_APPEND_HISTORY + +# ignore recording duplicate consecutive commands in the history +setopt HIST_IGNORE_DUPS + +# ignore commands that start with a space in the history +setopt HIST_IGNORE_SPACE + +# >>> bindkey tip: to discovery the code of your keys, execute "$ cat -v" and press the key, the code will be printed in your shell. + +# use the ZLE (zsh line editor) in emacs mode. Useful to move the cursor in large commands +bindkey -e + +# navigate words using Ctrl + arrow keys +# >>> CRTL + right arrow | CRTL + left arrow +bindkey "^[[1;5C" forward-word +bindkey "^[[1;5D" backward-word + +# macosx override +if [[ "$OSTYPE" == "darwin"* ]]; then + # >>> OPT + right arrow | OPT + left arrow + bindkey "^[^[[C" forward-word + bindkey "^[^[[D" backward-word +fi + +# search history using Up and Down keys +# >>> up arrow | down arrow +bindkey "^[[A" history-beginning-search-backward +bindkey "^[[B" history-beginning-search-forward + +# jump to the start and end of the command line +# >>> CTRL + A | CTRL + E +bindkey "^A" beginning-of-line +bindkey "^E" end-of-line +# >>> Home | End +bindkey "^[[H" beginning-of-line +bindkey "^[[F" end-of-line + +# navigate menu for command output +zstyle ':completion:*:*:*:*:*' menu select +bindkey '^[[Z' reverse-menu-complete + +# delete characters using the "delete" key +bindkey "^[[3~" delete-char + + +# >>> load ZSH plugin + +# enable kubectl plugin autocompletion +autoload -Uz compinit +compinit + + +# start Starship prompt +eval "$(starship init zsh)" + +source /home/blinky/.motd \ No newline at end of file diff --git a/demo/.devcontainer/Dockerfile b/demo/.devcontainer/Dockerfile new file mode 100644 index 00000000..13eef83e --- /dev/null +++ b/demo/.devcontainer/Dockerfile @@ -0,0 +1,26 @@ +ARG VERSION=latest + +FROM ghcr.io/kit-mrt/arbitration_graphs_pacman_tutorial:$VERSION + +USER root + +# Install clangd for the VSCode extension to work out of the box +# Install zsh and tig as modern dev tools +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + clangd \ + curl \ + tig \ + zsh && \ + apt-get clean + +# Install and use starship terminal prompt +RUN curl https://starship.rs/install.sh > /tmp/starship_install.sh && \ + chmod +x /tmp/starship_install.sh && \ + /tmp/starship_install.sh -y && \ + rm /tmp/starship_install.sh + +COPY .devcontainer/.bashrc /home/blinky/.bashrc +COPY .devcontainer/.zshrc /home/blinky/.zshrc + +USER blinky diff --git a/demo/.devcontainer/devcontainer.json b/demo/.devcontainer/devcontainer.json new file mode 100644 index 00000000..d83ac6f0 --- /dev/null +++ b/demo/.devcontainer/devcontainer.json @@ -0,0 +1,44 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose +{ + "name": "Arbitration Graphs Tutorial", + + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": [ + "../docker-compose.tutorial.yaml", + "docker-compose.yml" + ], + + // The 'service' property is the name of the service for the container that VS Code should + // use. Update this value and .devcontainer/docker-compose.yml to the real service name. + "service": "tutorial", + + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "customizations": { + "vscode": { + // Install some useful VSCode C++ extensions + "extensions": [ + "llvm-vs-code-extensions.vscode-clangd", + "vadimcn.vscode-lldb", + "matepek.vscode-catch2-test-adapter", + "twxs.cmake" + ], + "settings": { + // Use zsh as default terminal + "terminal.integrated.profiles.linux": { + "zsh": { + "path": "/bin/zsh", + "args": ["-l", "-i"] + } + }, + "terminal.integrated.defaultProfile.linux": "zsh", + + // Use system installation of clangd + "clangd.path": "clangd" + } + } + } +} diff --git a/demo/.devcontainer/docker-compose.yml b/demo/.devcontainer/docker-compose.yml new file mode 100644 index 00000000..77fba341 --- /dev/null +++ b/demo/.devcontainer/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.8' +services: + tutorial: + build: + context: . + dockerfile: .devcontainer/Dockerfile + + volumes: + - ..:/workspaces:cached + + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1 @@ +build diff --git a/demo/.vscode/launch.json b/demo/.vscode/launch.json new file mode 100644 index 00000000..df67280a --- /dev/null +++ b/demo/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug Pacman Demo", + "program": "${workspaceFolder}/build/arbitration_graphs_pacman_demo", + "args": [], + "cwd": "${workspaceFolder}", + "initCommands":["settings set target.disable-aslr false"], + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/demo/.vscode/tasks.json b/demo/.vscode/tasks.json new file mode 100644 index 00000000..d1bb4811 --- /dev/null +++ b/demo/.vscode/tasks.json @@ -0,0 +1,39 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Create build folder", + "type": "shell", + "command": "mkdir -p ${workspaceFolder}/build" + }, + { + "label": "Configure debug build", + "type": "shell", + "command": "cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTS=true -S ${workspaceFolder} -B ${workspaceFolder}/build" + }, + { + "label": "CMake build for debug", + "dependsOn": ["Create build folder", "Configure debug build"], + "type": "shell", + "command": "cmake --build ${workspaceFolder}/build -j9", + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "Configure release build", + "dependsOn": ["Create build folder"], + "type": "shell", + "command": "cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=true -S ${workspaceFolder} -B ${workspaceFolder}/build" + }, + { + "label": "CMake build for release", + "dependsOn": ["Create build folder", "Configure release build"], + "type": "shell", + "command": "cmake --build ${workspaceFolder}/build -j9" + } + ] +} \ No newline at end of file diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index bbdc0040..87c3100d 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -59,23 +59,25 @@ find_package(Yaml-cpp REQUIRED) ## Build ## ########### -add_library(${PROJECT_NAME} SHARED +add_library(${PROJECT_NAME}_lib SHARED src/astar.cpp src/avoid_ghost_behavior.cpp src/change_dot_cluster_behavior.cpp src/chase_ghost_behavior.cpp src/cost_estimator.cpp src/cluster.cpp + src/eat_closest_dot_behavior.cpp src/entities.cpp src/environment_model.cpp src/move_randomly_behavior.cpp + src/stay_in_place_behavior.cpp src/utils.cpp ) -target_include_directories(${PROJECT_NAME} PRIVATE +target_include_directories(${PROJECT_NAME}_lib PRIVATE include ${SDL2_INCLUDE_DIR} ) -target_link_libraries(${PROJECT_NAME} PUBLIC +target_link_libraries(${PROJECT_NAME}_lib PUBLIC arbitration_graphs glog::glog @@ -86,16 +88,16 @@ target_link_libraries(${PROJECT_NAME} PUBLIC ${SDL2_LIBRARY} ) -add_executable(${PROJECT_NAME}_exe +add_executable(${PROJECT_NAME} src/main.cpp src/pacman_wrapper.cpp ) -target_include_directories(${PROJECT_NAME}_exe PRIVATE +target_include_directories(${PROJECT_NAME} PRIVATE include ${SDL2_INCLUDE_DIR} ) -target_link_libraries(${PROJECT_NAME}_exe PRIVATE - ${PROJECT_NAME} +target_link_libraries(${PROJECT_NAME} PRIVATE + ${PROJECT_NAME}_lib ) @@ -133,7 +135,7 @@ endif() ## Install ## ############# -install(TARGETS ${PROJECT_NAME} ${PROJECT_NAME}_exe +install(TARGETS ${PROJECT_NAME}_lib ${PROJECT_NAME} EXPORT ${PROJECT_NAME}Targets COMPONENT demo LIBRARY DESTINATION lib COMPONENT Runtime diff --git a/demo/Dockerfile b/demo/Dockerfile index 2f9e68ba..89018980 100644 --- a/demo/Dockerfile +++ b/demo/Dockerfile @@ -61,5 +61,5 @@ WORKDIR /home/blinky/demo/build RUN cmake -DCMAKE_BUILD_TYPE=Release .. && \ cmake --build . -j8 -CMD ["bash", "-c", "/home/blinky/.motd && /home/blinky/demo/build/arbitration_graphs_pacman_demo_exe"] +CMD ["bash", "-c", "/home/blinky/.motd && /home/blinky/demo/build/arbitration_graphs_pacman_demo"] diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 00000000..ff88224c --- /dev/null +++ b/demo/README.md @@ -0,0 +1,31 @@ +# Arbitration Graphs Demo and Tutorial + +This is a demo of the [arbitration_graphs](https://github.com/KIT-MRT/arbitration_graphs) library using Pac-Man as an example application. +The arbitration graph controls Pac-Man on its journey to collect tasty dots đŸŦ + +Run the demo with: + +```bash +git clone https://github.com/KIT-MRT/arbitration_graphs.git +cd arbitration_graphs/demo +docker compose up +``` + +Open the GUI with your favorite browser: +[http://0.0.0.0:8080](http://0.0.0.0:8080) + + +## Tutorial + +If you're here for the tutorial, follow the instructions on our [Tutorial GitHub Page](https://kit-mrt.github.io/arbitration_graphs/docs/Tutorial.md). + +For a smooth out-of-the-box experience, we recommend using [Visual Studio Code](https://code.visualstudio.com/) with our DevContainer setup. + +- Open this folder in VSCode +- Build and open the Dev Container by running this [command](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) (use `Ctrl+Shift+P`): + `Dev Containers: Reopen in Container` +- Enjoy a full-blown IDE with code-completion, code-navigation etc. + - Compile via `Ctrl+Shift+B` + - View, run and debug unit tests via [Testing](https://code.visualstudio.com/docs/editor/testing) sidebar + - Debug the Pac-Man Demo via [Run and Debug](https://code.visualstudio.com/docs/editor/debugging) sidebar + - Debug with breakpoints etc. \ No newline at end of file diff --git a/demo/docker-compose.tutorial.yaml b/demo/docker-compose.tutorial.yaml new file mode 100644 index 00000000..8bf2b966 --- /dev/null +++ b/demo/docker-compose.tutorial.yaml @@ -0,0 +1,13 @@ +services: + tutorial: + image: ghcr.io/kit-mrt/arbitration_graphs_pacman_tutorial:$VERSION + env_file: .env + ports: + - "8080:8080" + volumes: + - .:/home/blinky/demo + - $HOME/.Xauthority:/home/blinky/.Xauthority + - /tmp/.X11-unix:/tmp/.X11-unix + environment: + - DISPLAY=$DISPLAY + diff --git a/demo/docker-compose.yaml b/demo/docker-compose.yaml index f18bde66..f7193356 100644 --- a/demo/docker-compose.yaml +++ b/demo/docker-compose.yaml @@ -1,15 +1,8 @@ services: tutorial: - image: ghcr.io/kit-mrt/arbitration_graphs_pacman_tutorial:$VERSION - env_file: .env - ports: - - "8080:8080" - volumes: - - .:/home/blinky/demo - - $HOME/.Xauthority:/home/blinky/.Xauthority - - /tmp/.X11-unix:/tmp/.X11-unix - environment: - - DISPLAY=$DISPLAY + extends: + file: docker-compose.tutorial.yaml + service: tutorial # # This makes sure `docker compose up` only runs the demo service # Use `docker compose run --rm --service-ports tutorial` to run the tutorial diff --git a/demo/include/demo/avoid_ghost_behavior.hpp b/demo/include/demo/avoid_ghost_behavior.hpp index 23c68548..0d083af5 100644 --- a/demo/include/demo/avoid_ghost_behavior.hpp +++ b/demo/include/demo/avoid_ghost_behavior.hpp @@ -24,9 +24,9 @@ class AvoidGhostBehavior : public arbitration_graphs::Behavior { double commitmentMinDistance{7}; }; - AvoidGhostBehavior(EnvironmentModel::Ptr environmentModel, - const Parameters& parameters, - const std::string& name = "AvoidGhost") + explicit AvoidGhostBehavior(EnvironmentModel::Ptr environmentModel, + const Parameters& parameters, + const std::string& name = "AvoidGhost") : Behavior(name), environmentModel_{std::move(environmentModel)}, parameters_{parameters} { } diff --git a/demo/include/demo/chase_ghost_behavior.hpp b/demo/include/demo/chase_ghost_behavior.hpp index f1fde928..ce11321e 100644 --- a/demo/include/demo/chase_ghost_behavior.hpp +++ b/demo/include/demo/chase_ghost_behavior.hpp @@ -25,9 +25,9 @@ class ChaseGhostBehavior : public arbitration_graphs::Behavior { int minScaredTicksLeft{5}; }; - ChaseGhostBehavior(EnvironmentModel::Ptr environmentModel, - const Parameters& parameters, - const std::string& name = "ChaseGhost") + explicit ChaseGhostBehavior(EnvironmentModel::Ptr environmentModel, + const Parameters& parameters, + const std::string& name = "ChaseGhost") : Behavior(name), environmentModel_{std::move(environmentModel)}, parameters_{parameters} { } diff --git a/demo/include/demo/eat_closest_dot_behavior.hpp b/demo/include/demo/eat_closest_dot_behavior.hpp index 41884012..6d8a5ee1 100644 --- a/demo/include/demo/eat_closest_dot_behavior.hpp +++ b/demo/include/demo/eat_closest_dot_behavior.hpp @@ -19,27 +19,10 @@ class EatClosestDotBehavior : public arbitration_graphs::Behavior { : Behavior(name), environmentModel_{std::move(environmentModel)} { } - Command getCommand(const Time& /*time*/) override { - auto pacmanPosition = environmentModel_->pacmanPosition(); - std::optional pathToClosestDot = environmentModel_->pathToClosestDot(pacmanPosition); + Command getCommand(const Time& /*time*/) override; - if (!pathToClosestDot) { - throw std::runtime_error("Failed to compute path to closest dot. Can not provide a sensible command."); - } - - return Command{pathToClosestDot.value()}; - } - - bool checkInvocationCondition(const Time& /*time*/) const override { - // This behavior is only applicable if there is at least one dot. We should check for the presence of a dot - // here, but since the game is won when all dots are collected, we assume at least one dot exists when this - // behavior is invoked. - return true; - } - - bool checkCommitmentCondition(const Time& /*time*/) const override { - return false; - } + bool checkInvocationCondition(const Time& /*time*/) const override; + bool checkCommitmentCondition(const Time& /*time*/) const override; private: EnvironmentModel::Ptr environmentModel_; diff --git a/demo/include/demo/move_randomly_behavior.hpp b/demo/include/demo/move_randomly_behavior.hpp index 7510de6c..a8e40eb4 100644 --- a/demo/include/demo/move_randomly_behavior.hpp +++ b/demo/include/demo/move_randomly_behavior.hpp @@ -29,24 +29,13 @@ class MoveRandomlyBehavior : public arbitration_graphs::Behavior { Command getCommand(const Time& time) override; - bool checkInvocationCondition(const Time& time) const override { - return true; - } - bool checkCommitmentCondition(const Time& time) const override { - return false; - } - - void gainControl(const Time& time) override { - gainedControlAt_ = time; - } - void loseControl(const Time& time) override { - } + bool checkInvocationCondition(const Time& /*time*/) const override; + bool checkCommitmentCondition(const Time& /*time*/) const override; private: Direction selectRandomDirection(); util_caching::Cache directionCache_; - Time gainedControlAt_; Parameters parameters_; std::random_device randomDevice_; diff --git a/demo/include/demo/stay_in_place_behavior.hpp b/demo/include/demo/stay_in_place_behavior.hpp index b0e492e6..8d66d9b1 100644 --- a/demo/include/demo/stay_in_place_behavior.hpp +++ b/demo/include/demo/stay_in_place_behavior.hpp @@ -24,22 +24,10 @@ class StayInPlaceBehavior : public arbitration_graphs::Behavior { : Behavior(name), environmentModel_{std::move(environmentModel)} { } - Command getCommand(const Time& time) override { - Direction currentDirection = environmentModel_->pacmanDirection(); - return Command{oppositeDirection(currentDirection)}; - } - - bool checkInvocationCondition(const Time& time) const override { - return true; - } - bool checkCommitmentCondition(const Time& time) const override { - return false; - } + Command getCommand(const Time& /*time*/) override; - void gainControl(const Time& time) override { - } - void loseControl(const Time& time) override { - } + bool checkInvocationCondition(const Time& /*time*/) const override; + bool checkCommitmentCondition(const Time& /*time*/) const override; private: EnvironmentModel::Ptr environmentModel_; diff --git a/demo/src/eat_closest_dot_behavior.cpp b/demo/src/eat_closest_dot_behavior.cpp new file mode 100644 index 00000000..861977e3 --- /dev/null +++ b/demo/src/eat_closest_dot_behavior.cpp @@ -0,0 +1,27 @@ +#include "demo/eat_closest_dot_behavior.hpp" + +namespace demo { + +Command EatClosestDotBehavior::getCommand(const Time& /*time*/) { + auto pacmanPosition = environmentModel_->pacmanPosition(); + std::optional pathToClosestDot = environmentModel_->pathToClosestDot(pacmanPosition); + + if (!pathToClosestDot) { + throw std::runtime_error("Failed to compute path to closest dot. Can not provide a sensible command."); + } + + return Command{pathToClosestDot.value()}; +} + +bool EatClosestDotBehavior::checkInvocationCondition(const Time& /*time*/) const { + // This behavior is only applicable if there is at least one dot. We should check for the presence of a dot + // here, but since the game is won when all dots are collected, we assume at least one dot exists when this + // behavior is invoked. + return true; +} + +bool EatClosestDotBehavior::checkCommitmentCondition(const Time& /*time*/) const { + return false; +} + +} // namespace demo \ No newline at end of file diff --git a/demo/src/move_randomly_behavior.cpp b/demo/src/move_randomly_behavior.cpp index 25dfb08f..ffd89c24 100644 --- a/demo/src/move_randomly_behavior.cpp +++ b/demo/src/move_randomly_behavior.cpp @@ -22,4 +22,11 @@ Direction MoveRandomlyBehavior::selectRandomDirection() { return randomMove.direction; } +bool MoveRandomlyBehavior::checkInvocationCondition(const Time& /*time*/) const { + return true; +} +bool MoveRandomlyBehavior::checkCommitmentCondition(const Time& /*time*/) const { + return false; +} + } // namespace demo diff --git a/demo/src/pacman_wrapper.cpp b/demo/src/pacman_wrapper.cpp index 7ff832e0..3428afce 100644 --- a/demo/src/pacman_wrapper.cpp +++ b/demo/src/pacman_wrapper.cpp @@ -119,6 +119,8 @@ void PacmanWrapper::printKeybindings() { << " \033[1;32mSpace\033[0m - Pause the demo\n" << " \033[1;32mP\033[0m - Toggle path visualization\n" << "\033[1;36m=====================================\033[0m\n" + << " \033[1;32mGUI\033[0m - Open http://localhost:8080\n" + << "\033[1;36m=====================================\033[0m\n" << std::endl; } diff --git a/demo/src/stay_in_place_behavior.cpp b/demo/src/stay_in_place_behavior.cpp new file mode 100644 index 00000000..4693b5b8 --- /dev/null +++ b/demo/src/stay_in_place_behavior.cpp @@ -0,0 +1,17 @@ +#include "demo/stay_in_place_behavior.hpp" + +namespace demo { + +Command StayInPlaceBehavior::getCommand(const Time& /*time*/) { + Direction currentDirection = environmentModel_->pacmanDirection(); + return Command{oppositeDirection(currentDirection)}; +} + +bool StayInPlaceBehavior::checkInvocationCondition(const Time& /*time*/) const { + return true; +} +bool StayInPlaceBehavior::checkCommitmentCondition(const Time& /*time*/) const { + return false; +} + +} // namespace demo \ No newline at end of file diff --git a/demo/test/CMakeLists.txt b/demo/test/CMakeLists.txt index 14e4cece..a3412783 100644 --- a/demo/test/CMakeLists.txt +++ b/demo/test/CMakeLists.txt @@ -71,7 +71,7 @@ if(GTEST_FOUND) get_filename_component(_test_name ${_test} NAME_WE) # make sure we add only one -test to the target string(REGEX REPLACE "-test" "" TEST_TARGET_NAME ${_test_name}) - set(TEST_TARGET_NAME arbitration_graphs_pacman_demo-gtest-${TEST_TARGET_NAME}) + set(TEST_TARGET_NAME test_${TEST_TARGET_NAME}) message(STATUS "Adding gtest unittest \"${TEST_TARGET_NAME}\" with working dir ${CMAKE_CURRENT_SOURCE_DIR}/${TEST_FOLDER} \n _test: ${_test}" @@ -88,7 +88,7 @@ if(GTEST_FOUND) target_link_libraries(${TEST_TARGET_NAME} PUBLIC ${GTEST_BOTH_LIBRARIES} pthread - arbitration_graphs_pacman_demo + arbitration_graphs_pacman_demo_lib arbitration_graphs EnTT_Pacman util_caching diff --git a/demo/test/chase_ghost_behavior.cpp b/demo/test/chase_ghost_behavior.cpp index ab67a60d..d4891f2a 100644 --- a/demo/test/chase_ghost_behavior.cpp +++ b/demo/test/chase_ghost_behavior.cpp @@ -39,6 +39,7 @@ TEST_F(ChaseGhostBehaviorTest, checkInvocationConditionFalse) { // We don't want to chase ghosts when they are far away environmentModel_->setGhostMode(GhostMode::SCARED); + environmentModel_->setScaredCountdown(40); environmentModel_->setPacmanPosition({1, 1}); environmentModel_->setGhostPositions({8, 8}); ASSERT_FALSE(chaseGhostBehavior_.checkInvocationCondition(time)); @@ -67,6 +68,7 @@ TEST_F(ChaseGhostBehaviorTest, checkCommitmentConditionFalse) { // We don't want to chase ghosts when they are far away environmentModel_->setGhostMode(GhostMode::SCARED); + environmentModel_->setScaredCountdown(40); environmentModel_->setPacmanPosition({1, 1}); environmentModel_->setGhostPositions({8, 8}); ASSERT_FALSE(chaseGhostBehavior_.checkInvocationCondition(time)); diff --git a/docs/Tutorial.md b/docs/Tutorial.md new file mode 100644 index 00000000..31f93278 --- /dev/null +++ b/docs/Tutorial.md @@ -0,0 +1,140 @@ +--- +tagline: "decision-making for Pac-Man" +--- + +# Arbitration Graphs Tutorial + +Let's write an agent for the famous Pac-Man game using Arbitration Graphs 🕹ī¸ + +**TL;DR:** Find links to the individual tasks at the bottom of this page. + +## Introduction + + +### Goal + +The goal of this tutorial is to help you understand how to use the Arbitration Graphs library. +To keep things interesting, we will re-implement some parts of our Pac-Man demo. + +We'll start by looking into the implementation of a single behavior component + and then learn how to integrate it into an arbitration graph using a simple priority arbitrator. + +Next, we'll start adding more and more behavior components to the graph and learn about other aspects of the library + such as cost arbitrators, nested structures and verification. + +The tutorial is structured into several tasks that are meant to be completed in order. + + +### Development Environment + +The easiest way to get started is to use the provided [Visual Studio Code](https://code.visualstudio.com/) [Dev Container](https://code.visualstudio.com/docs/devcontainers/containers). +Using the docker setup via terminal is great as well and works with any IDE. + +Start by checking out the `tutorial` branch where we have removed some parts + of the demo implementation for the purpose of this tutorial. +```bash +git clone --branch tutorial https://github.com/KIT-MRT/arbitration_graphs.git +``` + +
+Go here for the VSCode Dev Container + +Open the `demo` folder of your fresh `arbitration_graphs` clone in VSCode, e.g. via terminal: +```bash +cd arbitration_graphs/demo +code . +``` + +Build and open the Dev Container by running this [command](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) (use `Ctrl+Shift+P`): +`Dev Containers: Reopen in Container` (this might take a while) + +Enjoy a full-blown IDE with code-completion, code-navigation etc. +- Explore the repo in the Explorer sidebar +- Compile via `Ctrl+Shift+B` +- View, run and debug unit tests via [Testing](https://code.visualstudio.com/docs/editor/testing) sidebar +- Debug the Pac-Man Demo via [Run and Debug](https://code.visualstudio.com/docs/editor/debugging) sidebar +- Debug with breakpoints etc. + +
+ + +
+Unfold this for the Docker setup via terminal + +To start an interactive shell in the docker container with all required dependencies installed + and the current directory mounted, run +```bash +cd arbitration_graphs/demo +docker compose run --rm tutorial +``` + +You can then create a build directory and run CMake to build the project. +You should enable the `BUILD_TESTS` option to build the unit tests as well. + +```bash +cd /home/blinky/demo +mkdir build +cd build +cmake -DBUILD_TESTS=true .. +cmake --build . -j9 +``` + +You can then run the demo with +```bash +./arbitration_graphs_pacman_demo +``` + +You'll also find the individual unit executables in this directory. +To execute them all at once, run +```bash +cmake --build . --target test +``` + +We'll leave the setup of your favorite IDE up to you + though most modern IDEs should support attaching to a running docker container. + +
+ + +### What to find where + +Let's take a look at the structure and content of the `arbitration_graphs/demo/` directory. + +``` +demo +├── include +├── src +├── test +└── â€Ļ +``` + +All header files can be found in the `include/` directory with corresponding implementation files in the `src/` directory. +The entire demo is thoroughly tested using the unit tests you'll find in the `test/` directory. + +Each behavior component is implemented in a separate `_behavior.hpp` file as a class inheriting from the abstract `Behavior` class. + +Next, there is `environment_model.hpp`. +You guessed it, it contains the environment model for the arbitration graph. +In it, we store things like current positions of Pac-Man and the ghosts, the maze, several utility functions + and other things required by the behavior components. + +The `cost_estimator.hpp` file will be relevant for a later task when we cover [cost arbitrators](./tasks/4_cost_arbitration.md). + +Similarly, the `verifier.hpp` file will be used to add a verification layer to the arbitration graph [near the end](./tasks/5_verification.md) of the tutorial. + +Finally, in `pacman_agent.hpp`, the behavior components are assembled into an arbitration graph. +It's also where you'll spend most of your time during this tutorial. + +If you are interested in how the demo works in detail, + you'll find additional code not directly relevant to the tutorial in the `include/utils/` directory + + +## Tasks + +With the basics out of the way, let's work through the tasks. + +1. [Implement your first behavior component](./tasks/1_implement_behavior_component.md) +2. [Extend the arbitration graph with that behavior](./tasks/2_extend_arbitration_graph.md) +3. [Learn about nested arbitration graphs](./tasks/3_nested_arbitrators.md) +4. [Arbitrate based on predicted utility](./tasks/4_cost_arbitration.md) +5. [Verify commands and add a fallback strategy](./tasks/5_verification.md) diff --git a/docs/_includes/head-custom.html b/docs/_includes/head-custom.html index 4a2e9169..7f8b0918 100644 --- a/docs/_includes/head-custom.html +++ b/docs/_includes/head-custom.html @@ -1,4 +1,3 @@ - \ No newline at end of file diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index fcd768c7..6704facb 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -3,7 +3,12 @@ -{% seo %} + {% capture htmltitle %}{{ page.title }} | {{ page.tagline | default: site.tagline | default: site.github.repository_name }}{% endcapture %} + {{ htmltitle }} + + + {% seo title=false %} + @@ -32,6 +37,40 @@

{{ page.description | default: site.description | de
+ + + {{ content }} diff --git a/docs/assets/_sass/fonts.scss b/docs/assets/_sass/fonts.scss index 57d04fb5..f05e8f4f 100644 --- a/docs/assets/_sass/fonts.scss +++ b/docs/assets/_sass/fonts.scss @@ -28,6 +28,7 @@ body { code { font-family: "Source Code Pro", Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 0.85rem; } pre { diff --git a/docs/assets/_sass/toc.scss b/docs/assets/_sass/toc.scss index 0eb87b26..1c6a0bda 100644 --- a/docs/assets/_sass/toc.scss +++ b/docs/assets/_sass/toc.scss @@ -4,7 +4,7 @@ html { } // Hide navbar on small/medium screens -#page-container>nav { +#page-container nav { display: none; } @@ -13,6 +13,7 @@ html { // Style table of content /** page layout **/ #page-container { + background-color: $section-bg-color; display: grid; grid-template-columns: 1fr 15em; max-width: 100em; @@ -20,14 +21,18 @@ html { margin: 0 auto; } + #page-container .main-content { + padding-top: 0; + } + // Make nav sticky - #page-container>nav { + #page-container nav { display: block; position: sticky; - top: 2rem; - margin-top: 2rem; + top: 0; + padding-bottom: .4rem; align-self: start; - + background-color: $section-bg-color; ul, ol { @@ -41,28 +46,50 @@ html { } } + .pages-nav { + padding: .4rem 0; + } + // ScrollSpy active styles (see toc.js tab for activation) - .section-nav li.active>a { - color: #333; + nav li.active>a { + color: $nav-text-active-color; font-weight: 500; } - .section-nav { + #page-container .pages-nav > ul { + display: flex; + width: 100%; + } + + #page-container .pages-nav li { + margin-left: 1.5rem; + text-wrap: nowrap; + } + + #page-container .pages-nav li:first-of-type { + margin-left: .5rem; + } + + #page-container .section-nav > ul { + margin-top: 4rem; + } + + #page-container .section-nav>ul { padding-left: 0; - border-left: 1px solid #efefef; + border-left: 1px solid $nav-border-color; } - .section-nav a { + nav a { text-decoration: none; display: block; padding: .125rem 0; - color: #ccc; + color: $nav-text-color; transition: all 50ms ease-in-out; /* 💡 This small transition makes setting of the active state smooth */ } - .section-nav a:hover, - .section-nav a:focus { - color: #666; + nav a:hover, + nav a:focus { + color: $nav-text-focus-color; } } \ No newline at end of file diff --git a/docs/assets/_sass/variables.scss b/docs/assets/_sass/variables.scss index 1f12ce6b..8d8da1c4 100644 --- a/docs/assets/_sass/variables.scss +++ b/docs/assets/_sass/variables.scss @@ -2,6 +2,11 @@ $large-breakpoint: 64em !default; $medium-breakpoint: 42em !default; +// Navigation +$nav-text-color: #ccc !default; +$nav-text-active-color: #333 !default; +$nav-text-focus-color: #666 !default; + // Headers $header-heading-color: #fff !default; $header-bg-color: #008383 !default; @@ -9,6 +14,7 @@ $header-bg-color-secondary: #00001D !default; // Text $section-headings-color: #257180 !default; +$section-bg-color: #fff !default; $body-text-color: #606c71 !default; $body-link-color: #008383 !default; $blockquote-text-color: #819198 !default; @@ -21,3 +27,4 @@ $code-text-color: #f8f8f2 !default; $border-color: #dce6f0 !default; $table-border-color: #e9ebec !default; $hr-border-color: #eff0f1 !default; +$nav-border-color: #efefef !default; diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss index b5e8a4b7..46d302c0 100644 --- a/docs/assets/css/style.scss +++ b/docs/assets/css/style.scss @@ -14,6 +14,10 @@ // Add table of contents navbar @import 'toc'; +#page-container { + background-color: $section-bg-color; +} + // Switch to light theme logo img.github-logo { content: url({{ "/docs/assets/img/github-mark.svg" | relative_url }}); diff --git a/docs/sitemap.xml b/docs/sitemap.xml new file mode 100644 index 00000000..82c99f38 --- /dev/null +++ b/docs/sitemap.xml @@ -0,0 +1,59 @@ +--- +layout: null +sitemap: + exclude: 'yes' +permalink: /sitemap.xml +--- + + + {% for post in site.posts %} + {% assign file_ext = post.url | split: '.' | last %} + {% if post.published != false and file_ext != 'css' %} + + {{ site.url }}{{ post.url }} + {% if post.sitemap.lastmod %} + {{ post.sitemap.lastmod | date: "%Y-%m-%d" }} + {% elsif post.date %} + {{ post.date | date_to_xmlschema }} + {% else %} + {{ site.time | date_to_xmlschema }} + {% endif %} + {% if post.sitemap.changefreq %} + {{ post.sitemap.changefreq }} + {% else %} + monthly + {% endif %} + {% if post.sitemap.priority %} + {{ post.sitemap.priority }} + {% else %} + 0.5 + {% endif %} + + {% endif %} + {% endfor %} + {% for page in site.pages %} + {% assign file_ext = page.url | split: '.' | last %} + {% if page.sitemap.exclude != "yes" and file_ext != 'css' %} + + {{ site.url }}{{ page.url | remove: "index.html" }} + {% if page.sitemap.lastmod %} + {{ page.sitemap.lastmod | date: "%Y-%m-%d" }} + {% elsif page.date %} + {{ page.date | date_to_xmlschema }} + {% else %} + {{ site.time | date_to_xmlschema }} + {% endif %} + {% if page.sitemap.changefreq %} + {{ page.sitemap.changefreq }} + {% else %} + monthly + {% endif %} + {% if page.sitemap.priority %} + {{ page.sitemap.priority }} + {% else %} + 0.3 + {% endif %} + + {% endif %} + {% endfor %} + \ No newline at end of file diff --git a/docs/tasks/1_implement_behavior_component.md b/docs/tasks/1_implement_behavior_component.md new file mode 100644 index 00000000..0341802a --- /dev/null +++ b/docs/tasks/1_implement_behavior_component.md @@ -0,0 +1,106 @@ +--- +title: "Arbitration Graphs Tutorial" +menu_title: "First Behavior" +tagline: "Task 1: Implement a Behavior Component" +--- + +# Task 1: Implement a Behavior Component + +Implement your first `checkInvocationCondition()` and `getCommand()` function, such that the ChaseGhost behavior component passes its unit tests. + +## Context + +Before we start building our arbitration graph, we want to take a closer look into behavior components. +Don't worry, most of the behavior components are already implemented for you + but we want to make sure you have an idea of how they work. + +With the current state of the arbitration graph, Pac-Man will just move around and eat dots until a ghost gets too close. +That's great and all but if we ate a power pellet, we want to take advantage and chase the ghosts to eat them for extra points! + +To do this, we need to implement the `ChaseGhost` behavior component. +It essentially does the exact opposite of the `AvoidGhost` behavior component + but is only applicable when Pac-Man ate a power pellet. +We can ensure that this is always the case by using the behavior's invocation condition. + +But wait - the current implementation of the invocation condition is not complete. +It should only be applicable if there is one of these tasty ghosts close by. + +Once that's out of the way, we'll take a closer look at the `getCommand()` function which is missing some core logic right now. + +## Goal + +Finish the implementation of the `checkInvocationCondition()` and `getCommand()` functions + of the `ChaseGhost` behavior component such that it passes its unit tests. + +## Instructions + +- Build and run the game, take a look at the arbitration graph and observe how Pac-Man behaves. +- Run the unit tests and note that the `ChaseGhost`'s `checkInvocationConditionFalse` test is failing +- Open the implementation of the `ChaseGhost` behavior component in `src/chase_ghost_behavior.cpp`. +- The `checkInvocationCondition()` function is already implemented but does not check for the presence of a ghost. +- Implement the missing piece. Take a look at the implementation of `AvoidGhostBehavior::checkInvocationCondition()` if you need inspiration. +- The `getCommand()` function is partially implemented but the core logic is missing. +- 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 piece 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) +| +[Next task →](2_extend_arbitration_graph.md) diff --git a/docs/tasks/2_extend_arbitration_graph.md b/docs/tasks/2_extend_arbitration_graph.md new file mode 100644 index 00000000..83d0ac63 --- /dev/null +++ b/docs/tasks/2_extend_arbitration_graph.md @@ -0,0 +1,86 @@ +--- +title: "Arbitration Graphs Tutorial" +menu_title: "Extending the Graph" +tagline: "Task 2: Extend the Arbitration Graph" +--- + +# Task 2: Extend the Arbitration Graph + +Extend the arbitration graph with the `ChaseGhost` behavior component. + +## Context + +With our next behavior component ready to go, we need to think about integrating it into our arbitration graph. + +For this purpose, we need to modify the `PacmanAgent` class to include the `ChaseGhost` behavior component we implemented in the [previous task](1_implement_behavior_component.md). +Integrating a new behavior component into the arbitration graph is as simple as instantiating it and adding it as a new option to one of the arbitrators. +Since right now there is just one arbitrator – a priority arbitrator – the choice is simple. +We just need to worry about the order in which the options are assigned to the arbitrator. +Should chasing a ghost have a higher priority than avoiding a ghost or vice versa? + +## Goal + +Integrate the `ChaseGhost` behavior component into the arbitration graph defined in the `PacmanAgent` class. + +## Instructions + +- Take a look at how the other behavior components are defined in `include/demo/pacman_agent.hpp`. +- Add the `ChaseGhost` behavior component as a new member of the `PacmanAgent` class and initialize it in the constructor. +- Extend the `PacmanAgent` parameter struct to include the parameters for the `ChaseGhost` behavior component. +- Add a new option to the priority arbitrator. +- Run the game, take a look at the new arbitration graph and observe how Pac-Man 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_; +``` + +Extend the `PacmanAgent` parameter struct to include the parameters for the `ChaseGhost` behavior component: +```cpp +struct Parameters { + AvoidGhostBehavior::Parameters avoidGhostBehavior; + // Add the parameters for the ChaseGhost behavior component + ChaseGhostBehavior::Parameters chaseGhostBehavior; + MoveRandomlyBehavior::Parameters moveRandomlyBehavior; +}; +``` + +In the constructor of the `PacmanAgent` class, initialize the `ChaseGhost` behavior component and add it to the priority arbitrator. + +The invocation condition of `ChaseGhost` is a subset of the `AvoidGhost` invocation condition. Therefore, it only makes sense to add `ChaseGhost` with higher priority than (i.e. before) the `AvoidGhost` behavior component: +```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) +| +[Tutorial Home](../Tutorial.md) +| +[Next task →](3_nested_arbitrators.md) diff --git a/docs/tasks/3_nested_arbitrators.md b/docs/tasks/3_nested_arbitrators.md new file mode 100644 index 00000000..48b92d28 --- /dev/null +++ b/docs/tasks/3_nested_arbitrators.md @@ -0,0 +1,98 @@ +--- +title: "Arbitration Graphs Tutorial" +menu_title: "Nesting" +tagline: "Task 3: Nested arbitration graphs" +--- + +# Task 3: Nested arbitration graphs + +Integrate a long-term behavior and add another layer to the arbitration graph. + +## Context + +We have now implemented a behavior component and integrated it into the arbitration graph. +So far, all behavior components are children of the root arbitrator. + +Let's make things a bit more interesting by adding a long-term behavior component that's also about eating dots. +The `ChangeDotCluster` behavior will move Pac-man to an area in the maze where there is a higher density of dots. + +For now, we'll just decide between the two dot eating strategies using chance. +We can achieve that by adding them to a random arbitrator which is then added as an option to the root arbitrator. + +There are more sophisticated ways to decide between behavior components, we'll cover those in the [next task](4_cost_arbitration.md). + +## Goal + +Add the `EatClosestDot` and `ChangeDotCluster` behavior components to a random arbitrator nested within the root arbitrator. + +## Instructions + +- Add the `ChangeDotCluster` behavior component as a new member of the `PacmanAgent` class and initialize it in the constructor. +- Add a random arbitrator as a new member of the `PacmanAgent` class, analogous to the priority arbitrator. +- Add the `EatClosestDot` and `ChangeDotCluster` behavior components as options to the random arbitrator. +- Add the random arbitrator as an option to the root arbitrator. +- Run the game and observe how Pac-Man 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 better code readability, 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](2_extend_arbitration_graph.md) +| +[Tutorial Home](../Tutorial.md) +| +[Next task →](4_cost_arbitration.md) diff --git a/docs/tasks/4_cost_arbitration.md b/docs/tasks/4_cost_arbitration.md new file mode 100644 index 00000000..b49113cc --- /dev/null +++ b/docs/tasks/4_cost_arbitration.md @@ -0,0 +1,148 @@ +--- +title: "Arbitration Graphs Tutorial" +menu_title: "Cost Arbitrator" +tagline: "Task 4: Arbitrate based on predicted utility" +--- + +# Task 4: Arbitrate based on predicted utility + +Learn how the cost arbitrator can help you to arbitrate between behaviors based on their expected cost/utility. + +## Context + +The `EatDot` arbitrator we added in the [previous task](3_nested_arbitrators.md) decides between the two dot eating strategies randomly. +That's obviously not the greatest idea. +There must be a better way. + +Turns out, there is! +There is another type of arbitrator that might be more suitable for this task: the cost arbitrator. + +As the name suggests, the cost arbitrator computes a cost for each command received from its children and selects the one with the lowest cost. +We need some kind of cost function for this, which we will implement in the `CostEstimator` class. + +The idea is to reward a planned path that contains lots of dots while also moving into an area with a high dot density. +We prepared the general structure of the `CostEstimator` class for you, you just need to fill in the blanks. + +Let's get started! + +## Goal + +Finish the implementation of the `CostEstimator` and replace the random arbitrator with a cost arbitrator. + +## Instructions + +- Run the unit tests and note that some of the `CostEstimator` tests are failing +- In `cost_estimator.cpp`, fill in the blanks to compute `nDots` and `nCells`. +- Compile and run the unit tests for the `CostEstimator` to verify that your implementation is correct. +- Add an instance of the `CostEstimator` to the `PacmanAgent` class and initialize it in the constructor. + Don't forget to include the necessary headers and extend the parameter struct with the parameters for the `CostEstimator`. +- 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 = environmentModel_->toAbsolutePath(command.path); + + // 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" +``` + +To keep things tidy and consistent, add an alias definition analogous to the existing ones: + +```cpp +using CostArbitrator = arbitration_graphs::CostArbitrator; +``` + +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_; +``` + +Extend the `Parameters` struct to contain the parameters for the `CostEstimator`: +```cpp +struct Parameters { + AvoidGhostBehavior::Parameters avoidGhostBehavior; + ChaseGhostBehavior::Parameters chaseGhostBehavior; + MoveRandomlyBehavior::Parameters moveRandomlyBehavior; + + // Add the parameters for the CostEstimator + CostEstimator::Parameters 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](3_nested_arbitrators.md) +| +[Tutorial Home](../Tutorial.md) +| +[Next task →](5_verification.md) diff --git a/docs/tasks/5_verification.md b/docs/tasks/5_verification.md new file mode 100644 index 00000000..b540069c --- /dev/null +++ b/docs/tasks/5_verification.md @@ -0,0 +1,147 @@ +--- +title: "Arbitration Graphs Tutorial" +menu_title: "Verification and Fallbacks" +tagline: "Task 5: Better safe than sorry" +--- + +# 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 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 + +- In `verifier.hpp`, finish the implementation of the `Verifier::analyze()` method. +- Compile and run the unit tests for the `Verifier` to verify that your implementation is correct. +- Add an instance of the `Verifier` to the `PacmanAgent` class and initialize it in the constructor. +- Pass the `Verifier` instance to the constructors of the arbitrators. + (Hint: You'll need to adjust the template parameters of the arbitrators.) +- Add the `StayInPlace` behavior component analogously to the other behavior components. +- Mark the `StayInPlace` 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 + 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`. +```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](4_cost_arbitration.md) +| +[Tutorial Home](../Tutorial.md)