├── .formatter.exs ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile-integration ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── actions │ └── sample.ex ├── bot_army_starter.ex ├── run.ex └── trees │ ├── run_load.sh │ ├── sample_load.ex │ └── sample_load_bt.json ├── mix.exs ├── mix.lock └── test ├── bot_army_starter_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ### Expected Behaviour 5 | 6 | ### Actual Behaviour 7 | 8 | ### Reproduce Scenario (including but not limited to) 9 | 10 | #### Steps to Reproduce 11 | 12 | #### Platform and Version 13 | 14 | #### Sample Code that illustrates the problem 15 | 16 | #### Logs taken while reproducing problem 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Related Issue 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Motivation and Context 15 | 16 | 17 | 18 | ## How Has This Been Tested? 19 | 20 | 21 | 22 | 23 | 24 | ## Screenshots (if appropriate): 25 | 26 | ## Types of changes 27 | 28 | 29 | 30 | - [ ] Bug fix (non-breaking change which fixes an issue) 31 | - [ ] New feature (non-breaking change which adds functionality) 32 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 33 | 34 | ## Checklist: 35 | 36 | 37 | 38 | 39 | - [ ] I have signed the [Adobe Open Source CLA](https://opensource.adobe.com/cla.html). 40 | - [ ] My code follows the code style of this project. 41 | - [ ] My change requires a change to the documentation. 42 | - [ ] I have updated the documentation accordingly. 43 | - [ ] I have read the **CONTRIBUTING** document. 44 | - [ ] I have added tests to cover my changes. 45 | - [ ] All new and existing tests passed. 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | bot_demo-*.tar 24 | 25 | bot_run.log 26 | .elixir_ls 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Adobe Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language. 18 | * Being respectful of differing viewpoints and experiences. 19 | * Gracefully accepting constructive criticism. 20 | * Focusing on what is best for the community. 21 | * Showing empathy towards other community members. 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances. 27 | * Trolling, insulting/derogatory comments, and personal or political attacks. 28 | * Public or private harassment. 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission. 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting. 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Grp-opensourceoffice@adobe.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version]. 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for choosing to contribute! 4 | 5 | The following are a set of guidelines to follow when contributing to this project. 6 | 7 | ## Code Of Conduct 8 | 9 | This project adheres to the Adobe [code of conduct](../CODE_OF_CONDUCT.md). By participating, 10 | you are expected to uphold this code. Please report unacceptable behavior to 11 | [Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com). 12 | 13 | ## Have A Question? 14 | 15 | Start by filing an issue. The existing committers on this project work to reach 16 | consensus around project direction and issue solutions within issue threads 17 | (when appropriate). 18 | 19 | ## Contributor License Agreement 20 | 21 | All third-party contributions to this project must be accompanied by a signed contributor 22 | license agreement. This gives Adobe permission to redistribute your contributions 23 | as part of the project. [Sign our CLA](https://opensource.adobe.com/cla.html). You 24 | only need to submit an Adobe CLA one time, so if you have submitted one previously, 25 | you are good to go! 26 | 27 | ## Code Reviews 28 | 29 | All submissions should come in the form of pull requests and need to be reviewed 30 | by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/) 31 | for more information on sending pull requests. 32 | 33 | Lastly, please follow the [pull request template](PULL_REQUEST_TEMPLATE.md) when 34 | submitting a pull request! 35 | 36 | ## From Contributor To Committer 37 | 38 | We love contributions from our community! If you'd like to go a step beyond contributor 39 | and become a committer with full write access and a say in the project, you must 40 | be invited to the project. The existing committers employ an internal nomination 41 | process that must reach lazy consensus (silence is approval) before invitations 42 | are issued. If you feel you are qualified and want to get more deeply involved, 43 | feel free to reach out to existing committers to have a conversation about that. 44 | 45 | ## Security Issues 46 | 47 | Security issues shouldn't be reported on this issue tracker. Instead, [file an issue to our security experts](https://helpx.adobe.com/security/alertus.html). 48 | -------------------------------------------------------------------------------- /Dockerfile-integration: -------------------------------------------------------------------------------- 1 | FROM elixir:1.8-alpine AS elixir-builder 2 | 3 | MAINTAINER Dave Persing 4 | 5 | WORKDIR /opt/app 6 | 7 | ENV MIX_ENV=dev 8 | 9 | RUN sed -i 's/http\:\/\/dl-cdn.alpinelinux.org/https\:\/\/alpine.global.ssl.fastly.net/g' /etc/apk/repositories \ 10 | && apk add --update --no-cache bash openssl git \ 11 | && rm -rf /var/cache/apk/* \ 12 | && mix local.hex --force \ 13 | && mix local.rebar --force 14 | 15 | COPY . . 16 | 17 | RUN mix do deps.get, compile 18 | 19 | CMD ["mix", "test"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | © Copyright 2020 Adobe. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bot Army Starting Template 2 | 3 | A starting point for setting up a new bot army. This includes samples for both load 4 | testing and integration/functional testing. 5 | 6 | See [Bot Army Docs](https://hexdocs.pm/bot_army/1.0.0/readme.html) on how to use the 7 | bot army. 8 | 9 | The [Bot Army Cookbook](https://opensource.adobe.com/bot_army_cookbook/) is also 10 | useful. 11 | 12 | ## Set up 13 | 14 | You will need to have Elixir and Erlang installed on your computer/container 15 | ([asdf](https://github.com/asdf-vm/asdf-elixir) works well for this). 16 | 17 | Fetch and compile deps with `mix do deps.get, deps.compile`. 18 | 19 | If you want to write your tests in the same repo as the project they are testing, you 20 | can `mv bot_army_starter your_project/bot_test`. 21 | 22 | ## Using 23 | 24 | The behavior trees are located in `/lib/trees` and the actions are in `/lib/actions`. 25 | 26 | The sample load tree imports tree data from a json file created with the [Behavior 27 | Tree Visual Editor](https://github.com/adobe/behavior_tree_editor). 28 | Download the editor from there and open `./lib/trees/sample_load_bt.json` to view and 29 | edit the tree. 30 | 31 | Follow the examples and the docs to build out your trees and actions. You may also 32 | need to add a `/lib/bot.ex` module if you need to [customize the 33 | bot](https://hexdocs.pm/bot_army/1.0.0/BotArmy.Bot.html#module-extending-the-bot) 34 | (to add websocket syncing for example). 35 | 36 | Keep your actions atomic, and use the structure of the trees to describe the logic of 37 | your behaviors. 38 | 39 | ## Running locally 40 | 41 | You can run the bots locally using the two included mix tasks: 42 | 43 | ### Load test runner 44 | 45 | `mix bots.load_test --tree BotArmyStarter.Trees.SampleLoad --n 5 --custom '[magic_number: 7]'` 46 | 47 | You will see some output. Press "q" then "enter" to quit the bots and see a summary 48 | of their actions. You can view the full logs at `bot_run.log`. 49 | 50 | For convenience, the above command can also be run with `sh lib/trees/run_load.sh` 51 | (edit that file to change parameters). 52 | 53 | ### Integration test runner 54 | 55 | `mix test` 56 | 57 | This will run all of the integration tests in `test/` via ExUnit. 58 | 59 | ## Deploying 60 | 61 | You can run the bots in a docker container. A sample docker file is provided, but 62 | you will need to adjust it (particularly the `CMD` section). 63 | 64 | You can also build a "release" which can be run as an executable. _Be aware the 65 | release can only run on the same OS it was built on!_ 66 | 67 | Just run `mix release`, which will build it to 68 | `_build/prod/rel/bot_army_starter/bin/bot_army_starter`. 69 | 70 | To run it `cd _build/prod/rel/bot_army_starter/` and then run `bin/bot_test eval 'BotArmyStarter.Run.run(30)'` (See `lib/run.ex`). 71 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # third-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :bot_demo, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:bot_demo, :key) 18 | # 19 | # You can also configure a third-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /lib/actions/sample.ex: -------------------------------------------------------------------------------- 1 | defmodule BotArmyStarter.Actions.Sample do 2 | @moduledoc """ 3 | Contrived examples of actions for guessing a number. 4 | 5 | """ 6 | 7 | require Logger 8 | 9 | @doc """ 10 | Sets `context.guess_count` to 0 and clears any `context.guess`. 11 | """ 12 | def init_guesses_count(_context) do 13 | {:succeed, guess_count: 0, guess: nil} 14 | end 15 | 16 | @doc """ 17 | Makes sure the provided number is between 0 and 10 (inclusive) 18 | """ 19 | def validate_number(_context, n) do 20 | if n >= 0 and n <= 10 do 21 | Logger.info("Target number is #{n}") 22 | :succeed 23 | else 24 | Logger.error("Target number must be between 0 and 10, received #{n}") 25 | :fail 26 | end 27 | end 28 | 29 | @doc """ 30 | Chooses a random number between 1 and 10 and saves it to `context.guess`. Also 31 | increments `context.guess_count`. 32 | 33 | (Note this will error and the bot will die if `guess_count` has not been set.) 34 | """ 35 | def make_guess(%{guess_count: guess_count}) when is_integer(guess_count) do 36 | guess = Enum.random(0..10) 37 | Logger.info("Is it... #{guess}?") 38 | {:succeed, guess: guess, guess_count: guess_count + 1} 39 | end 40 | 41 | @doc """ 42 | Succeeds if the guess matches the target, fails otherwise. 43 | """ 44 | def check_guess(%{guess: guess}, target) when guess == target, do: :succeed 45 | def check_guess(_, _), do: :fail 46 | 47 | @doc """ 48 | Fails or succeeds based on if `context.guess_count` is less than `max_guesses`. 49 | """ 50 | def more_guesses_left?(%{guess_count: guess_count}, max_guesses) do 51 | if guess_count < max_guesses do 52 | :succeed 53 | else 54 | {:error, :out_of_guesses} 55 | end 56 | end 57 | 58 | @doc """ 59 | Validates the guess count (used in integration tests). 60 | """ 61 | def validate_guess_count(%{guess_count: guess_count}, expected) 62 | when guess_count == expected, 63 | do: :succeed 64 | 65 | def validate_guess_count(_, _), do: :fail 66 | end 67 | -------------------------------------------------------------------------------- /lib/bot_army_starter.ex: -------------------------------------------------------------------------------- 1 | defmodule BotArmyStarter do 2 | @moduledoc """ 3 | Documentation for BotArmyStarter. 4 | """ 5 | end 6 | -------------------------------------------------------------------------------- /lib/run.ex: -------------------------------------------------------------------------------- 1 | defmodule BotArmyStarter.Run do 2 | @moduledoc """ 3 | An entry point to run the bot army (particularly from a mix release) 4 | 5 | Paramerts include: 6 | 7 | (edit these as needed) 8 | * `magic_number` - a number for the bots to guess 9 | """ 10 | alias BotArmy.LoadTest 11 | 12 | def run(magic_number) do 13 | Application.ensure_all_started(:logger) 14 | Application.ensure_all_started(:bot_army) 15 | Application.ensure_all_started(:bot_army_starter) 16 | 17 | metadata = [ 18 | :bot_id, 19 | :action, 20 | :outcome, 21 | :error, 22 | :duration, 23 | :uptime, 24 | :bot_pid, 25 | :session_id, 26 | :bot_count 27 | ] 28 | 29 | Logger.configure(level: :warn) 30 | 31 | Logger.add_backend({LoggerFileBackend, :bot_log}) 32 | 33 | Logger.configure_backend({LoggerFileBackend, :bot_log}, 34 | path: "bot_run.log", 35 | level: :debug, 36 | metadata: metadata 37 | ) 38 | 39 | # do any set up you need... 40 | 41 | # add custom config to BotArmy.SharedData (this is how the mix tasks do it) 42 | BotArmy.SharedData.put(:magic_number, magic_number) 43 | 44 | LoadTest.run(%{ 45 | # you can parameterize this 46 | n: 1, 47 | tree: BotArmyStarter.Trees.SampleLoad.tree(), 48 | bot: BotArmy.Bot.Default 49 | }) 50 | 51 | # Wait for input before stopping the bots 52 | IO.gets("\n\n\nPress to stop the bots\n\n\n") 53 | 54 | BotArmy.Metrics.SummaryReport.build_report() 55 | BotArmy.LoadTest.stop() 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/trees/run_load.sh: -------------------------------------------------------------------------------- 1 | mix bots.load_test \ 2 | --tree "BotArmyStarter.Trees.SampleLoad" \ 3 | --n 5 \ 4 | --custom '[magic_number: 7]' 5 | -------------------------------------------------------------------------------- /lib/trees/sample_load.ex: -------------------------------------------------------------------------------- 1 | defmodule BotArmyStarter.Trees.SampleLoad do 2 | @moduledoc """ 3 | This is a contrived example. The bots will try up to 5 times to guess the number 4 | provided at rutime. The provided number should be between 1 and 10. 5 | 6 | * `magic_number` - a number for the bots to guess 7 | """ 8 | 9 | @doc """ 10 | Validates the supplied number, then tries to guess it up to 5 times, then sleeps 11 | for a minute. 12 | 13 | This uses the visual behavior tree editor 14 | (https://github.com/adobe/behavior_tree_editor) to define the tree (which strips 15 | the custom action prefix BotArmyStarter.Actions, so it is added back in). 16 | """ 17 | def tree() do 18 | # "custom" runtime config is available from `BotArmy.SharedData` 19 | magic_number = BotArmy.SharedData.get(:magic_number) 20 | 21 | BotArmy.BTParser.parse!( 22 | "lib/trees/sample_load_bt.json", 23 | "Root", 24 | context: %{magic_number: magic_number}, 25 | module_base: BotArmyStarter.Actions 26 | ) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/trees/sample_load_bt.json: -------------------------------------------------------------------------------- 1 | {"data":{"custom_nodes":[{"category":"action","description":"Succeeds if the guess matches the target, fails otherwise.\n","name":"Sample.check_guess(target)","properties":{},"scope":"node","title":"Sample.check_guess(target)","version":"0.3.0"},{"category":"action","description":"Sets `context.guess_count` to 0 and clears any `context.guess`.\n","name":"Sample.init_guesses_count()","properties":{},"scope":"node","title":"Sample.init_guesses_count()","version":"0.3.0"},{"category":"action","description":"Chooses a random number between 1 and 10 and saves it to `context.guess`. Also \nincrements `context.guess_count`.\n\n(Note this will error and the bot will die if `guess_count` has not been set.)\n","name":"Sample.make_guess()","properties":{},"scope":"node","title":"Sample.make_guess()","version":"0.3.0"},{"category":"action","description":"Fails or succeeds based on if `context.guess_count` is less than `max_guesses`.\n","name":"Sample.more_guesses_left?(max_guesses)","properties":{},"scope":"node","title":"Sample.more_guesses_left?(max_guesses)","version":"0.3.0"},{"category":"action","description":"Validates the guess count (used in integration tests).\n","name":"Sample.validate_guess_count(expected)","properties":{},"scope":"node","title":"Sample.validate_guess_count(expected)","version":"0.3.0"},{"category":"action","description":"Makes sure the provided number is between 0 and 10 (inclusive)\n","name":"Sample.validate_number(n)","properties":{},"scope":"node","title":"Sample.validate_number(n)","version":"0.3.0"}],"scope":"project","selectedTree":"a4bab2e0-ce84-4773-8c8a-a632940a521d","trees":[{"description":"The root of this tree. You can set tree-wide properties on this node and use them in an action's \"args\" property with this notation: `{{key_name}}`.","display":{"camera_x":743,"camera_y":371.5,"camera_z":1,"x":-432,"y":0},"id":"a4bab2e0-ce84-4773-8c8a-a632940a521d","nodes":{"47b416aa-8098-4f25-a84a-a84318f86bcd":{"description":"Sets `context.guess_count` to 0 and clears any `context.guess`.\n","display":{"x":12,"y":-60},"id":"47b416aa-8098-4f25-a84a-a84318f86bcd","name":"Sample.init_guesses_count()","properties":{},"title":"Sample.init_guesses_count()"},"7e122337-45ae-4320-8452-75aaf7ec8e6f":{"description":"An instance of the tree. You can add properties to this node and they will be available in action nodes in this tree (using syntax like `{{key}}`). Properties defined on this node will overwrite the same property defined on the tree's root node.","display":{"x":-24,"y":-132},"id":"7e122337-45ae-4320-8452-75aaf7ec8e6f","name":"420330e3-cb7f-495f-8192-d20ee860da3a","properties":{"magic_number":"{{magic_number}}"},"title":"validate_number"},"9fa39aaf-4f18-41a7-8535-a00ca84a325f":{"description":"An instance of the tree. You can add properties to this node and they will be available in action nodes in this tree (using syntax like `{{key}}`). Properties defined on this node will overwrite the same property defined on the tree's root node.","display":{"x":192,"y":36},"id":"9fa39aaf-4f18-41a7-8535-a00ca84a325f","name":"98d2ca88-22c6-42dc-8225-7dfb281d92a4","properties":{"magic_number":"{{magic_number}}"},"title":"try_to_guess_number"},"a2254ccd-6747-4749-9e98-b5dfdf4a9f06":{"children":["7e122337-45ae-4320-8452-75aaf7ec8e6f","47b416aa-8098-4f25-a84a-a84318f86bcd","f82c539c-46c6-4520-938b-b00295711dab","fde12906-19ed-44ab-996c-ec7803008ea3"],"description":"Takes multiple children and runs them from top to bottom (or left to right). If any fail, this node fails, if all succeed, this node succeeds.","display":{"x":-228,"y":0},"id":"a2254ccd-6747-4749-9e98-b5dfdf4a9f06","name":"sequence","properties":{},"title":"Sequence"},"f82c539c-46c6-4520-938b-b00295711dab":{"child":"9fa39aaf-4f18-41a7-8535-a00ca84a325f","description":"Takes one child which it repeats until it succeeds. This node always succeeds.","display":{"x":-24,"y":36},"id":"f82c539c-46c6-4520-938b-b00295711dab","name":"repeat_until_succeed","properties":{},"title":"Repeat until succeed"},"fde12906-19ed-44ab-996c-ec7803008ea3":{"description":"\"Pauses\" the bot for the specified number of seconds. You can specify two numbers (like `wait(1,10)`) to wait a random number of seconds between those numbers.","display":{"x":-24,"y":132},"id":"fde12906-19ed-44ab-996c-ec7803008ea3","name":"wait","properties":{},"title":"wait(1)"}},"properties":{},"root":"a2254ccd-6747-4749-9e98-b5dfdf4a9f06","scope":"tree","title":"Root","version":"0.3.0"},{"description":"The root of this tree. You can set tree-wide properties on this node and use them in an action's \"args\" property with this notation: `{{key_name}}`.","display":{"camera_x":768,"camera_y":517.5,"camera_z":1,"x":-408,"y":-120},"id":"420330e3-cb7f-495f-8192-d20ee860da3a","nodes":{"6f9315ad-0e7b-451d-864d-357e611ee92e":{"description":"Makes sure the provided number is between 0 and 10 (inclusive)\n","display":{"x":-12,"y":-168},"id":"6f9315ad-0e7b-451d-864d-357e611ee92e","name":"Sample.validate_number(n)","properties":{},"title":"Sample.validate_number({{magic_number}})"},"72abd835-096d-4875-8740-6f9f038ea509":{"children":["6f9315ad-0e7b-451d-864d-357e611ee92e","eb56ca37-0011-4510-993d-fd9810fdc1f6"],"description":"Takes multiple children and runs them from top to bottom (or left to right), succeeding when any one succeeds. Fails if all fail.","display":{"x":-288,"y":-120},"id":"72abd835-096d-4875-8740-6f9f038ea509","name":"select","properties":{},"title":"Select"},"eb56ca37-0011-4510-993d-fd9810fdc1f6":{"description":"Raises an error with the supplied message.","display":{"x":0,"y":-84},"id":"eb56ca37-0011-4510-993d-fd9810fdc1f6","name":"error","properties":{},"title":"error(\"The number must be between 1 and 10\")"}},"properties":{},"root":"72abd835-096d-4875-8740-6f9f038ea509","scope":"tree","title":"validate_number","version":"0.3.0"},{"description":"The root of this tree. You can set tree-wide properties on this node and use them in an action's \"args\" property with this notation: `{{key_name}}`.","display":{"camera_x":491,"camera_y":465.5,"camera_z":1,"x":-324,"y":0},"id":"98d2ca88-22c6-42dc-8225-7dfb281d92a4","nodes":{"00c7075f-2c86-4f20-980d-e51e0b613f1f":{"children":["fe593486-dbf7-4625-8167-ba1aca96ebd0","e88e8c7b-0786-46b9-81e9-83717d724113"],"description":"Takes multiple children and runs them from top to bottom (or left to right). If any fail, this node fails, if all succeed, this node succeeds.","display":{"x":-12,"y":-132},"id":"00c7075f-2c86-4f20-980d-e51e0b613f1f","name":"sequence","properties":{},"title":"Sequence"},"0571e4fd-917c-45e0-8704-436b37c56007":{"children":["75df4b55-44cc-4fc6-8eb0-cbae9be09249","8a39453e-2906-471f-b202-7a364464f689","a985d931-ce96-47db-8de1-870b36246670","7edb4cf5-a99a-4442-8bc6-bede16a9e5ad"],"description":"Takes multiple children and runs them from top to bottom (or left to right). If any fail, this node fails, if all succeed, this node succeeds.","display":{"x":-12,"y":132},"id":"0571e4fd-917c-45e0-8704-436b37c56007","name":"sequence","properties":{},"title":"Sequence"},"42f0b96e-685a-4e87-82a1-df329fd742e0":{"children":["00c7075f-2c86-4f20-980d-e51e0b613f1f","0571e4fd-917c-45e0-8704-436b37c56007"],"description":"Takes multiple children and runs them from top to bottom (or left to right), succeeding when any one succeeds. Fails if all fail.","display":{"x":-216,"y":0},"id":"42f0b96e-685a-4e87-82a1-df329fd742e0","name":"select","properties":{},"title":"Select"},"69704e50-cf2e-47d3-9075-329843ab8440":{"description":"Fails or succeeds based on if `context.guess_count` is less than `max_guesses`.\n","display":{"x":456,"y":-180},"id":"69704e50-cf2e-47d3-9075-329843ab8440","name":"Sample.more_guesses_left?(max_guesses)","properties":{},"title":"Sample.more_guesses_left?(5)"},"75df4b55-44cc-4fc6-8eb0-cbae9be09249":{"description":"Chooses a random number between 1 and 10 and saves it to `context.guess`. Also \nincrements `context.guess_count`.\n\n(Note this will error and the bot will die if `guess_count` has not been set.)\n","display":{"x":204,"y":24},"id":"75df4b55-44cc-4fc6-8eb0-cbae9be09249","name":"Sample.make_guess()","properties":{},"title":"Sample.make_guess()"},"7edb4cf5-a99a-4442-8bc6-bede16a9e5ad":{"description":"Logs the specified message.","display":{"x":228,"y":240},"id":"7edb4cf5-a99a-4442-8bc6-bede16a9e5ad","name":"log","properties":{},"title":"log(\"I guessed the number!\")"},"8a39453e-2906-471f-b202-7a364464f689":{"description":"\"Pauses\" the bot for the specified number of seconds. You can specify two numbers (like `wait(1,10)`) to wait a random number of seconds between those numbers.","display":{"x":192,"y":96},"id":"8a39453e-2906-471f-b202-7a364464f689","name":"wait","properties":{},"title":"wait(2)"},"a985d931-ce96-47db-8de1-870b36246670":{"description":"Succeeds if the guess matches the target, fails otherwise.\n","display":{"x":276,"y":168},"id":"a985d931-ce96-47db-8de1-870b36246670","name":"Sample.check_guess(target)","properties":{},"title":"Sample.check_guess({{magic_number}})"},"e88e8c7b-0786-46b9-81e9-83717d724113":{"description":"Logs the specified message.","display":{"x":300,"y":-96},"id":"e88e8c7b-0786-46b9-81e9-83717d724113","name":"log","properties":{},"title":"log(\"Failed to guess the number :(\")"},"fe593486-dbf7-4625-8167-ba1aca96ebd0":{"child":"69704e50-cf2e-47d3-9075-329843ab8440","description":"Takes one child. If that child succeeds, this node fails, and vice versa.","display":{"x":204,"y":-180},"id":"fe593486-dbf7-4625-8167-ba1aca96ebd0","name":"negate","properties":{},"title":"Negate"}},"properties":{},"root":"42f0b96e-685a-4e87-82a1-df329fd742e0","scope":"tree","title":"try_to_guess_number","version":"0.3.0"}],"version":"0.3.0"},"description":"","name":"bot army stater repo sample"} -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule BotArmyStarter.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :bot_army_starter, 7 | version: "0.1.0", 8 | elixir: "~> 1.8", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | {:credo, "~> 1.1", only: [:dev, :test]}, 25 | {:behavior_tree, "~> 0.3.0"}, 26 | {:bot_army, "~> 1.0"} 27 | ] 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "behavior_tree": {:hex, :behavior_tree, "0.3.1", "8c2f556b34e75e923662b0fefe0cc5e794ef2d146524e1d2c084420b97ca27c2", [:mix], [{:ex_zipper, "~> 0.1.3", [hex: :ex_zipper, repo: "hexpm", optional: false]}], "hexpm", "0fe956a856379bc86adedbc2e9877192154a25e1f1ba8ac85cb78b945c482c82"}, 3 | "bot_army": {:hex, :bot_army, "1.0.0", "772de341fc35b43f3e70b0bd9f869981889270b2c400d8667a832fe294d3a823", [:mix], [{:behavior_tree, "~> 0.3.1", [hex: :behavior_tree, repo: "hexpm", optional: false]}, {:con_cache, "~> 0.13.1", [hex: :con_cache, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:logger_file_backend, "~> 0.0.10", [hex: :logger_file_backend, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:poison, "~> 3.1.0", [hex: :poison, repo: "hexpm", optional: false]}, {:timex, "~> 3.0", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "188ed8e3a48a0795521c3b79e71b10e421ce6bc9942af2163d9667f1e9a9df45"}, 4 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 5 | "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"}, 6 | "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, 7 | "con_cache": {:hex, :con_cache, "0.13.1", "047e097ab2a8c6876e12d0c29e29a86d487b592df97b98e3e2abedad574e215d", [:mix], [], "hexpm", "d3c19f04153b691d7bfdb15bcfc3b8fea503b0b51635115393809af7eabe2f37"}, 8 | "cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e5580029080f3f1ad17436fb97b0d5ed2ed4e4815a96bac36b5a992e20f58db6"}, 9 | "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm", "1e1a3d176d52daebbecbbcdfd27c27726076567905c2a9d7398c54da9d225761"}, 10 | "credo": {:hex, :credo, "1.1.4", "c2f3b73c895d81d859cec7fcee7ffdb972c595fd8e85ab6f8c2adbf01cf7c29c", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6e25e83b4f460de47f33743239ac3d8d3f0f28ee467b0392e47c040a66cd3377"}, 11 | "ex_zipper": {:hex, :ex_zipper, "0.1.3", "886452d9e889c94f74048d8467bf386777d7462f270ac0ecefb2d7644a8828e7", [:mix], [{:stream_data, "~> 0.3.0", [hex: :stream_data, repo: "hexpm", optional: false]}], "hexpm", "783b768f313f9be65bf0b3ef31e21a345afd8522e316286957131c13d77df1c0"}, 12 | "gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm", "e0b8598e802676c81e66b061a2148c37c03886b24a3ca86a1f98ed40693b94b3"}, 13 | "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"}, 14 | "httpoison": {:hex, :httpoison, "1.6.1", "2ce5bf6e535cd0ab02e905ba8c276580bab80052c5c549f53ddea52d72e81f33", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "89149056039084024a284cd703b2d1900d584958dba432132cb21ef35aed7487"}, 15 | "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, 16 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"}, 17 | "logger_file_backend": {:hex, :logger_file_backend, "0.0.11", "3bbc5f31d3669e8d09d7a9443e86056fae7fc18e45c6f748c33b8c79a7e147a1", [:mix], [], "hexpm", "62be826f04644c62b0a2bc98a13e2e7ae52c0a4eda020f4c59d7287356d5e445"}, 18 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 19 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, 20 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 21 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, 22 | "plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "164baaeb382d19beee0ec484492aa82a9c8685770aee33b24ec727a0971b34d0"}, 23 | "plug_cowboy": {:hex, :plug_cowboy, "2.1.0", "b75768153c3a8a9e8039d4b25bb9b14efbc58e9c4a6e6a270abff1cd30cbe320", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "6cd8ddd1bd1fbfa54d3fc61d4719c2057dae67615395d58d40437a919a46f132"}, 24 | "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm", "73c1682f0e414cfb5d9b95c8e8cd6ffcfdae699e3b05e1db744e58b7be857759"}, 25 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, 26 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, 27 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"}, 28 | "stream_data": {:hex, :stream_data, "0.3.0", "cbfc8e3212f64683615657ea27804126a42ded634adfdfee258bf087ee605d46", [:mix], [], "hexpm", "ecea01cf7e8bae76df1493919571a778785edfd4934c1909d2be610bbfbf8146"}, 29 | "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"}, 30 | "tzdata": {:hex, :tzdata, "1.0.1", "f6027a331af7d837471248e62733c6ebee86a72e57c613aa071ebb1f750fc71a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cf1345dfbce6acdfd4e23cbb36e96e53d1981bc89181cd0b936f4f398f4c0b78"}, 31 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, 32 | } 33 | -------------------------------------------------------------------------------- /test/bot_army_starter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BotArmyStarterTest do 2 | @moduledoc false 3 | 4 | use BotArmy.IntegrationTest 5 | 6 | alias BotArmyStarter.Actions.Sample 7 | 8 | log_to_file() 9 | use_bot_module(BotArmy.Bot.Default) 10 | 11 | setup do 12 | %{magic_number: 9} 13 | end 14 | 15 | pre_all_tree "pre all" do 16 | Node.sequence([action(BotArmy.Actions, :log, ["Pre all ..."])]) 17 | end 18 | 19 | post_all_tree "post all" do 20 | BehaviorTree.Node.sequence([action(BotArmy.Actions, :log, ["Post all ..."])]) 21 | end 22 | 23 | # these one will only run if you tag a test with `@tag :requires_setup 24 | pre_tree "conditional pre tree", context do 25 | if context[:requires_setup] do 26 | BehaviorTree.Node.sequence([ 27 | action(BotArmy.Actions, :log, ["special set up..."]) 28 | ]) 29 | end 30 | end 31 | 32 | post_tree "conditional post tree", context do 33 | if context[:requires_setup] do 34 | BehaviorTree.Node.sequence([ 35 | action(BotArmy.Actions, :log, ["special set up..."]) 36 | ]) 37 | end 38 | end 39 | 40 | # @tag :verbose 41 | @tag :requires_setup 42 | test_tree "validate target number", context do 43 | Node.select([ 44 | action(Sample, :validate_number, [context.magic_number]), 45 | action(BotArmy.Actions, :error, ["#{context.magic_number} is an invalid number"]) 46 | ]) 47 | end 48 | 49 | # You can skip it if you want, see ExUnit.Case docs 50 | # @tag :skip 51 | test_tree "Bots eventually guess correctly" do 52 | # Warning, this is a recipe for an infinite loop. In this case we are certain 53 | # the loop will exit eventually. 54 | Node.sequence([ 55 | action(Sample, :init_guesses_count), 56 | Node.repeat_until_succeed( 57 | Node.sequence([ 58 | action(Sample, :make_guess), 59 | action(Sample, :check_guess, [9]) 60 | ]) 61 | ) 62 | ]) 63 | end 64 | 65 | test_tree "validate target number using bt.json subtree", %{magic_number: magic_number} do 66 | BotArmy.BTParser.parse!( 67 | "lib/trees/sample_load_bt.json", 68 | "validate_number", 69 | context: %{magic_number: magic_number}, 70 | module_base: BotArmyStarter.Actions 71 | ) 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------