Introduction
Setting Up
This page describes how to set up Dies for development.
Requirements
To compile the project all you need a recent version of stable Rust. We recommend using rustup to manage your Rust installation.
For development, you should make sure that rustfmt
and clippy
are installed. You can install them with rustup component add rustfmt clippy
.
To use the ER-Force simulator, you will also need to install a recent version of Docker and ensure that the Docker daemon is running.
Building and Running
Dies uses cargo workspaces to manage its crates. The dies-cli
crate contains the Dies CLI, which can be used to run matches.
To run tests for all crates, run cargo test
in the root directory. You can run specific tests by specifying the crate name, eg. cargo test -p dies-core
.
To run the CLI from the workspace root, use cargo run
.
TODO: describe web UI.
Developer Guide
This document provides some guidelines for developing Dies.
Using Git
The main branch should remain stable at all times. All new features and fixes should be developed in feature branches, and merged into main only after they are complete and have been tested. Feature branches should be named <name of author>/<feature name>
, eg. balint/fix-ersim-env-win
.
The CI will run tests and formatting checks on all branches, but you should run these locally before pushing your changes.
Commit names should adhere to a lite version of the Conventional Commits spec, ie. they should follow the format: <commit type>: <short description>
. Scopes are not needed. The commit type should be one of the following:
feat
: new featurefix
: bug fixrefactor
: refactoringdocs
: documentationtest
: adding testschore
: other changes (eg. CI, formatting, etc.)wip
: work in progressmisc
: other changes
Code Style
Dies uses the rustfmt formatter. It is recommended to use the rust-analyzer extension for VS Code, and configure it to automatically format the code on save.
We also use clippy for linting. The VS Code extension should also automatically run clippy on save.
In general, the Rust code should be idiomatic, and should attempt to follow the Rust API Guidelines.
Generics should be used with consideration. They are powerful, but they make the code more complex and significantly increase compile times. As a rule of thumb, remember YAGNI (you ain't gonna need it) -- don't overengineer, you can always add generics later if you need them.
Crate Structure
Dies is split into multiple crates. We use cargo workspaces to manage these. Having a lot of crates is not necessarily a bad thing -- crates are the unit of compilation in Rust, so more granular crates help caching and parallel compilation.
In general, the dependency graph between our crates should resemble a diamond:
+- feature crates -+
/ \
dies-core ------------> dies-cli
\ /
+- feature crates -+
There should be a few root crates (dies-core
) which will rarely change, a number of specialized feature crates (dies-basestation-client
, dies-executor
, etc.), and a few small leaf crates which just glue things together (dies-cli
).
For an up-to-date list of crates, see the README.
Error Handling
Errors should be handled according to Rust idioms. In general, we should avoid panicking, and instead propagate Result
to the very top, where it can be handled by the executor.
Throughout the codebase, the anyhow
crate should be used for error handling. Anwhow provides a convenient dynamic wrapper type for errors, so we do not have to define our own error types. anyhow::Result
should be the return type of all functions which can fail.
Logging
Logging is really important for Dies, as it is the only way to debug issues that occured during a match. Logs should contain all the information needed to replay matches, including all the data we receive from the vision system, the world state, and the commands we send to the robots.
Dies uses the log
crate for logging. The following log levels should be used:
error
: should be used only for fatal errors, which cause the executor to stop, ie. errors should only be logged in the executorwarn
: should be used for unexpected events or recoverable errors, which do not cause the executor to stop and do not get propagated upinfo
: should be used rarely, only for information that the user needs to know while running Dies - for example when long operations start and finish, or when error-prone operations succeeddebug
: should be used most of the time: everything from the data we receive from the vision system, commands we send to the robots, to all minor events should be logged at this level
We do not use trace
.
println!
and all other forms of direct output should be avoided, as they are not captured by the logger.
Testing
Reliability is very important for Dies, so we aim for high test coverage. All new features should come with corresponding unit tests. Unit tests should test the code located in the module they are in, and should avoid relying on other modules.
Property-based testing should be explored for testing the core logic of Dies. See the proptest crate for more details on this technique.
Documentation
This section is WIP.
Dies Architecture
Dies is Delft Mercurians' open source framework for building AIs, specifically the central team controller, for RoboCup Small Size League.
Dies runs on a central server, and communicates with the tournament software (ssl-vision/simulator, game controller), and with the robots on the field. It is responsible for running the AI, and for sending commands to the robots.
graph TD GameController[/Game Controller/] <--> Dies SSLVision[/SSL Vision/] --> Dies Robots[/Robots/] <--> Dies
Framework Component Relationships
Legend
- A `*--` B: Ownership (A can only exist as part of B)
- A `o..` B: Aggregation (A owns B, but B can exist independently)
- A `<..` B: Dependency (A does not store B, but makes use of it)
classDiagram %% Cli is the main entry point Cli o.. WebUi Cli o.. TestMainLoop %% WebUi owns the main loops WebUi o.. InteractiveMainLoop WebUi o.. PlaybackMainLoop %% Main components owned by TestMainLoop TestMainLoop <.. Simulator TestMainLoop <.. TeamMap %% Main components owned by InteractiveMainLoop InteractiveMainLoop <.. Simulator InteractiveMainLoop <.. SslClient InteractiveMainLoop <.. BsClient InteractiveMainLoop <.. TeamMap PlaybackMainLoop *-- Logplayer %% TeamMap owns Teams TeamMap *-- Team %% Team owns core components Team *-- Tracker Team *-- TeamController Team *-- StrategyServer %% Tracker hierarchy Tracker *-- PlayerTracker Tracker *-- BallTracker Tracker *-- GameStateTracker %% Controller hierarchy TeamController *-- PlayerController PlayerController *-- RobotController PlayerController *-- Skills %% Skills collection Skills o-- GoTo Skills o-- Face Skills o-- FetchBall Skills o-- OrbitBall Skills o-- Pass Skills o-- Receive %% Strategy client/server relationship StrategyServer ..> StrategyApi : IPC StrategyApi <-- StrategyClient class Cli { main() } class Skills { <<collection>> } class StrategyServer { <<IPC Server>> } class StrategyApi { }
Data Flow
Interactive Mode (simulation or live)
flowchart TB %% Define all components first UI[Web UI] WS[Web Server] ML[Main Loop] TR[Tracker] CT[Controller] SS[Strategy Server] SA[Strategy API] ST[Strategy Process] SIM[Simulator] SSL[SSL/BS Client] TM[Team] CLI[CLI] %% Group the UI layer subgraph UserInterface[User Interface] UI WS CLI end %% Group the core system components subgraph CoreSystem[Core] ML SIM SSL end %% Group the team components subgraph TeamSystem[Team] TM TR CT SS end %% Group the strategy components subgraph StrategySystem[Strategy] SA ST end %% Now define all the connections CLI -->|Config| WS ML -->|Debug data & Status| WS WS -->|Player Overrides| TM WS <-->|Start/Stop| ML UI <-->|UiMessages & UiData| WS ML -->|Simulator Cmds| SIM SSL -->|Vision/GC Updates| TR SIM -->|Vision/GC Updates| TR TR -->|Tracker Data| TM TM -->|Tracker Data| CT TM -->|Tracker Data| SS SS -->|Team Cmds| CT SS <--->|Team Cmds & Tracker Data| SA SA <-->|IPC| ST SSL <-->|Robot Cmds| CT TR -->|World Frame| WS
Test Mode (automated)
flowchart TB %% Main components - note the reduced set TML[Test Main Loop] TR[Tracker] CT[Controller] SS[Strategy Server] SA[Strategy API] ST[Strategy Process] SIM[Simulator] TM[Team] %% Test configuration and control TML -->|Test Scenario Commands| SIM %% Simulated vision flow SIM -->|Simulated Vision/GC Updates| TR %% Tracker data flow - through team TR -->|Tracker Data| TM TM -->|Tracker Data| CT TM -->|Tracker Data| SS %% Strategy flow remains the same SS -->|Team Cmds| CT SS <-->|Team Cmds & Tracker Data| SA SA <-->|IPC| ST %% Robot control now goes directly to simulator CT -->|Robot Commands| SIM %% Add subgraph for test components subgraph TestFramework[Test Framework] TML end %% Add subgraph for team components subgraph Team TM TR CT SS end
Introduction to Behavior Trees
A Behavior Tree (BT) is a mathematical model of plan execution used in computer science, robotics, control systems and video games. They are a way of describing the "brain" of an AI-controlled character or agent.
Core Concepts
A BT is a tree of nodes that controls the flow of decision-making. Each node, when "ticked" (or executed), returns a status:
Success
: The node has completed its task successfully.Failure
: The node has failed to complete its task.Running
: The node is still working on its task and needs more time.
There are several types of nodes:
1. Action Nodes (or Leaf Nodes)
These are the leaves of the tree and represent actual actions the agent can perform. In our system, these are called Skills, like Kick
, FetchBall
, or GoToPosition
. An Action Node will typically return Running
while the action is in progress, and Success
or Failure
upon completion.
2. Composite Nodes
These nodes have one or more children and control how their children are executed. The most common types are:
-
Sequence: Executes its children one by one in order. It returns
Failure
as soon as one of its children fails. If a child returnsRunning
, the Sequence node also returnsRunning
and will resume from that child on the next tick. It returnsSuccess
only if all children succeed. A Sequence is like a logical AND. -
Select (or Fallback): Executes its children one by one in order. It returns
Success
as soon as one of its children succeeds. If a child returnsRunning
, the Select node also returnsRunning
and will resume from that child on the next tick. It returnsFailure
only if all children fail. A Select is like a logical OR. -
ScoringSelect: A more advanced version of
Select
. It evaluates a score for each child and picks the one with the highest score to execute. It includes hysteresis to prevent rapid switching between behaviors.
3. Decorator Nodes
These nodes have a single child and modify its behavior or its return status. Common examples include:
-
Guard (or Condition): Checks a condition. If the condition is true, it ticks its child node and returns the child's status. If the condition is false, it returns
Failure
without executing the child. -
Semaphore: Limits the number of agents that can run a certain part of the tree simultaneously. This is crucial for team coordination, preventing all robots from trying to do the same thing at once (e.g., everyone chasing the ball).
Behavior Trees in Dies
In the Dies framework, a behavior tree is constructed for each robot on every tick (if it doesn't have one already). This tree dictates the robot's actions for that tick. The tree is built and executed based on the current state of the game, which is provided to the BT as a RobotSituation
object. This object contains all the information a robot needs to make decisions, such as ball position, player positions, and game state.
Scripting with Rhai
To enable dynamic and flexible behavior definition, the Dies project uses the Rhai scripting language. This page provides an overview of the most relevant Rhai language features for writing behavior tree scripts.
What is Rhai?
Rhai is a tiny, fast, and easy-to-use embedded scripting language for Rust. It features a syntax similar to a combination of JavaScript and Rust, and is designed for deep integration with a host Rust application.
While Rhai is a rich language, you only need to know a subset of its features to be productive in the Dies system.
Core Language Features
Here are the most important language features you will use when writing BT scripts.
Variables
Variables are declared with the let
keyword. They are dynamically typed.
#![allow(unused)] fn main() { let x = 42; let name = "droste"; let is_active = true; }
Functions
You can define your own functions using the fn
keyword. These are essential for creating conditions for Guard
nodes and scorers for ScoringSelect
nodes.
#![allow(unused)] fn main() { fn my_function(a, b) { return a + b > 10; } // Functions can be called as you'd expect let result = my_function(5, 6); }
The last expression in a function is implicitly returned, so you can often omit the return
keyword:
#![allow(unused)] fn main() { fn my_function(a, b) { a + b > 10 } }
Data Types
You'll primarily work with these data types:
- Integers (
-1
,0
,42
) - Floats (
3.14
,-0.5
) - Booleans (
true
,false
) - Strings (
"hello"
,"a description"
) - Arrays: A list of items, enclosed in
[]
.#![allow(unused)] fn main() { let my_array = [1, 2, "three", true]; }
- Maps: A collection of key-value pairs, enclosed in
#{}
. These are used foroptions
parameters in many skills and for defining scorers inScoringSelect
.#![allow(unused)] fn main() { let my_map = #{ heading: 3.14, with_ball: false }; }
Control Flow
Standard if/else
statements are supported for conditional logic within your functions.
#![allow(unused)] fn main() { fn my_scorer(s) { let score = 100.0; if s.has_ball { score += 50.0; } else { score -= 20.0; } return score; } }
Comments
Use //
for single-line comments and /* ... */
for block comments.
#![allow(unused)] fn main() { // This is a single line comment. /* This is a multi-line comment. */ }
How Rhai is Used in Dies
In Dies, we use Rhai to declaratively construct behavior trees. Instead of building the tree structure in Rust, we write a Rhai script that defines the tree. This script is then loaded and executed by the system to generate the behavior tree for each robot.
The core of this system is a Rhai script, typically located at crates/dies-executor/src/bt_scripts/standard_player_tree.rhai
. This script must contain an entry-point function:
#![allow(unused)] fn main() { fn build_player_bt(player_id) { // ... script logic to build and return a BehaviorNode ... } }
This function is called for each robot, and it is expected to return a BehaviorNode
which serves as the root of that robot's behavior tree.
Rust vs. Rhai: Where to Write Logic?
A key design principle of the Dies behavior system is the separation of concerns between Rust and Rhai. Here's a guideline for what code belongs where:
What to write in Rust (Skills)
Rust is used to implement the fundamental "building blocks" of robot behavior. These are called Skills. A skill should be:
- Atomic: It should represent a single, clear action (e.g.,
Kick
,FetchBall
,GoToPosition
). - Reusable: It should be generic enough to be used in many different contexts within the behavior tree.
- Self-contained: It should manage its own state (e.g., tracking whether a
GoToPosition
action is complete). - Parameterized: It should be configurable via arguments (e.g., the target for
GoToPosition
).
Examples of good skills implemented in Rust are Kick
, FetchBall
, and GoToPosition
. They are exposed to Rhai as functions that create Action Nodes.
What to write in Rhai (Strategy)
Rhai is used to compose these building blocks into complex, high-level strategies. The Rhai script defines the "brain" of the robot by wiring skills together using behavior tree nodes like Sequence
, Select
, and Guard
. Your Rhai script should focus on:
- Decision-making: Using
Select
andGuard
nodes to choose the right action based on the game state (RobotSituation
). - Orchestration: Using
Sequence
to define a series of actions to achieve a goal. - Team Coordination: Using
Semaphore
to coordinate behavior between multiple robots. - Dynamic Behavior: Using callbacks to dynamically provide arguments to skills based on real-time world data.
The standard_player_tree.rhai
script is a perfect example of this. It doesn't implement the how of moving or kicking, but it defines the when and why: when to be an attacker, when to support, what defines those roles, and how to transition between them.
The Core Idea
Simple, reusable skills go into Rust. Complex, high-level behavior goes into Rhai.
This separation allows strategists to rapidly iterate on high-level tactics in Rhai without needing to recompile the entire Rust application. Meanwhile, the core skills can be implemented and optimized in performant, reliable Rust code.
Getting Started: Your First Behavior Tree
This guide will walk you through creating a simple behavior tree script.
The Entry Point
All behavior tree scripts must define an entry point function called build_player_bt
. This function takes a player_id
as an argument and must return a BehaviorNode
.
#![allow(unused)] fn main() { // In standard_player_tree.rhai fn build_player_bt(player_id) { // Return a BehaviorNode } }
Example 1: The Simplest Action
Let's start with the most basic tree: a single action. We'll make the robot fetch the ball.
#![allow(unused)] fn main() { // standard_player_tree.rhai fn build_player_bt(player_id) { // This tree has only one node: an ActionNode that executes the "FetchBall" skill. return FetchBall(); } }
With this script, every robot will simply try to fetch the ball, no matter the situation.
Example 2: A Sequence of Actions
A more useful behavior might involve a sequence of actions. For example, fetch the ball, then turn towards the opponent's goal, and then kick. We can achieve this with a Sequence
node.
#![allow(unused)] fn main() { // standard_player_tree.rhai fn build_player_bt(player_id) { return Sequence([ FetchBall(), FaceTowardsPosition(6000.0, 0.0), // Assuming opponent goal is at (6000, 0) Kick() ]); } }
A Sequence
node executes its children in order. It will only proceed to the next child if the previous one returns Success
. If any child Fail
s, the sequence stops and fails. If a child is Running
, the sequence is also Running
.
Example 3: Conditional Behavior with Guards
Now, let's make the behavior conditional. We only want to kick if we actually have the ball. We can use a Guard
node for this. A Guard
takes a condition function and a child node. It only executes the child if the condition is true.
#![allow(unused)] fn main() { // standard_player_tree.rhai // A condition function. It receives the 'RobotSituation' object. fn i_have_ball(s) { // The 's' object gives access to the robot's state. // The 'has_ball()' method checks if the robot's breakbeam sensor detects the ball. // NOTE: The exact API of the situation object is subject to change. // For now, we assume 'has_ball()' is available. return s.has_ball(); } fn build_player_bt(player_id) { return Select([ // This branch is for when we have the ball Sequence([ // This Guard ensures the rest of the sequence only runs if we have the ball. Guard(i_have_ball, "Do I have the ball?"), FaceTowardsPosition(6000.0, 0.0, "Face opponent goal"), Kick("Kick!"), ]), // This is the fallback branch if the first one fails (i.e., we don't have the ball) FetchBall("Get the ball"), ]); } }
This tree uses a Select
node. A Select
node tries its children in order until one succeeds.
- It first tries the
Sequence
. - The
Sequence
starts with aGuard
. Ifi_have_ball
returnsfalse
, theGuard
fails, which makes theSequence
fail. - The
Select
node then moves to its next child, which isFetchBall()
. - If
i_have_ball
returnstrue
, theGuard
succeeds, and theSequence
continues to face the goal and kick. If theSequence
succeeds, theSelect
node also succeeds and stops.
This creates a simple but effective offensive logic. You can now build upon these concepts to create more complex and intelligent behaviors.
API Reference Overview
This section provides a detailed reference for all the functions and nodes available in the Dies Rhai scripting environment.
The API can be categorized as follows:
- Behavior Nodes: These are the building blocks of your behavior tree. They control the flow of execution.
- Composite Nodes:
Select
,Sequence
,ScoringSelect
. - Decorator Nodes:
Guard
,Semaphore
.
- Composite Nodes:
- Skills (Action Nodes): These are the leaf nodes of the tree that perform actual actions, like moving or kicking.
- Helpers: Utility functions, for example for creating vectors or working with player ID's.
- The Situation Object: An object passed to condition and scorer functions, providing access to the current world state.
Each part of the API is detailed in the subsequent pages. When writing scripts, you will be combining these components to create complex and intelligent behaviors for the robots.
Behavior Nodes
Behavior nodes are the fundamental building blocks for constructing the logic of a behavior tree. They control the flow of execution.
Composite Nodes
Composite nodes have multiple children and execute them in a specific order.
Select
A Select
node, also known as a Fallback or Priority node, executes its children sequentially until one of them returns Success
or Running
.
- Returns
Success
if any child returnsSuccess
. - Returns
Running
if any child returnsRunning
. - Returns
Failure
only if all children returnFailure
.
This is equivalent to a logical OR.
Syntax:
#![allow(unused)] fn main() { Select(children: Array, [description: String]) -> BehaviorNode }
Parameters:
children
: An array ofBehaviorNode
s.description
(optional): A string description for debugging.
Example:
#![allow(unused)] fn main() { Select([ // Try to score if possible TryToScoreAGoal(), // Otherwise, pass to a teammate PassToTeammate(), // If all else fails, just hold the ball HoldBall(), ]) }
Sequence
A Sequence
node executes its children sequentially.
- Returns
Failure
if any child returnsFailure
. - Returns
Running
if any child returnsRunning
. - Returns
Success
only if all children returnSuccess
.
This is equivalent to a logical AND.
Syntax:
#![allow(unused)] fn main() { Sequence(children: Array, [description: String]) -> BehaviorNode }
Parameters:
children
: An array ofBehaviorNode
s.description
(optional): A string description for debugging.
Example:
#![allow(unused)] fn main() { Sequence([ FetchBall(), FaceTowardsPosition(6000.0, 0.0), Kick() ]) }
ScoringSelect
A ScoringSelect
node evaluates a score for each of its children on every tick and executes the child with the highest score. This is useful for dynamic decision-making where multiple options are viable and need to be weighed against each other. It includes a hysteresis margin to prevent rapid, oscillating switching between behaviors.
Syntax:
#![allow(unused)] fn main() { ScoringSelect(children_scorers: Array, hysteresis_margin: Float, [description: String]) -> BehaviorNode }
Parameters:
children_scorers
: An array of maps, where each map has two keys:node
: TheBehaviorNode
child.scorer
: A function pointer to a scorer function. The scorer function receives theRobotSituation
object and must return a floating-point score.
hysteresis_margin
: A float value. A new child will only be chosen if its score exceeds the current best score by this margin. This prevents flip-flopping.description
(optional): A string description for debugging.
Example:
#![allow(unused)] fn main() { fn score_attack(s) { /* ... returns a score ... */ } fn score_defend(s) { /* ... returns a score ... */ } ScoringSelect( [ #{ node: AttackBehavior(), scorer: score_attack }, #{ node: DefendBehavior(), scorer: score_defend } ], 0.1, // Hysteresis margin of 0.1 "Choose between attacking and defending" ) }
Decorator Nodes
Decorator nodes have a single child and modify its behavior.
Guard
A Guard
node, or condition node, checks a condition before executing its child. If the condition is true, it ticks the child. If the condition is false, it returns Failure
immediately without ticking the child.
Syntax:
#![allow(unused)] fn main() { Guard(condition_fn: FnPtr, child: BehaviorNode, cond_description: String) -> BehaviorNode }
Parameters:
condition_fn
: A function pointer to a condition function. The condition function receives theRobotSituation
object and must returntrue
orfalse
.child
: TheBehaviorNode
to execute if the condition is true.cond_description
: A string description of the condition for debugging.
Example:
#![allow(unused)] fn main() { fn we_have_the_ball(s) { return s.has_ball(); } // Only execute the 'ShootGoal' action if we have the ball. Guard( we_have_the_ball, ShootGoal(), "Check if we have the ball" ) }
Semaphore
A Semaphore
node is used for team-level coordination. It limits the number of robots that can execute its child node at the same time. Each semaphore is identified by a unique string ID.
For example, you can use a semaphore to ensure only one robot tries to be the primary attacker.
Syntax:
#![allow(unused)] fn main() { Semaphore(child: BehaviorNode, id: String, max_count: Int, [description: String]) -> BehaviorNode }
Parameters:
child
: TheBehaviorNode
to execute.id
: A unique string identifier for the semaphore.max_count
: The maximum number of robots that can acquire this semaphore.description
(optional): A string description for debugging.
Example:
#![allow(unused)] fn main() { // Only one player can be the attacker at a time. Semaphore( AttackerBehavior(), "attacker_semaphore", 1 ) }
Skills (Action Nodes)
Skills are the leaf nodes of a behavior tree that perform concrete actions. In our system, calling a skill function in Rhai creates an ActionNode
, which is a type of BehaviorNode
.
Dynamic Arguments
Many skills accept dynamic arguments. This means that instead of providing a fixed, static value, you can provide a callback function. This function will be executed by the behavior tree on each tick to determine the value for that argument dynamically.
A callback function always receives the RobotSituation
object (usually named s
) as its only parameter, giving you access to the full world state to make decisions.
Example:
#![allow(unused)] fn main() { // Static argument GoToPosition(vec2(100.0, 200.0)) // Dynamic argument using a callback fn get_ball_pos(s) { return s.world.ball.position; } GoToPosition(get_ball_pos) }
This allows for creating highly reactive and flexible behaviors.
GoToPosition
Moves the robot to a target position.
Syntax:
GoToPosition(target: Vec2 | FnPtr, [options: Map], [description: String]) -> BehaviorNode
Parameters:
target
: The target position. Can be a staticVec2
(created withvec2(x, y)
) or a callback function that returns aVec2
.options
(optional): A map with optional parameters. Any of these can also be dynamic callbacks.heading
(Float | FnPtr): The final heading of the robot in radians.with_ball
(Bool | FnPtr): Iftrue
, the robot will try to keep the ball while moving.avoid_ball
(Bool | FnPtr): Iftrue
, the robot will actively try to avoid the ball while moving.
description
(optional): A string description for debugging.
Example (Static):
#![allow(unused)] fn main() { // Go to a fixed position with a fixed heading. GoToPosition( vec2(0.0, 0.0), #{ heading: 1.57, with_ball: true }, "Go to center with ball" ) }
Example (Dynamic):
#![allow(unused)] fn main() { fn get_ball_pos(s) { return s.world.ball.position; } fn get_heading_towards_goal(s) { let goal_pos = vec2(6000.0, 0.0); // Assumes a 'angle_to' helper exists return s.player.position.angle_to(goal_pos); } // Go towards the ball, while always facing the opponent's goal. GoToPosition( get_ball_pos, #{ heading: get_heading_towards_goal, avoid_ball: false }, "Follow ball while facing goal" ) }
FaceAngle
Rotates the robot to face a specific angle.
Syntax:
FaceAngle(angle: Float | FnPtr, [options: Map], [description: String]) -> BehaviorNode
Parameters:
angle
: The target angle in radians. Can be a staticFloat
or a callback function that returns aFloat
.options
(optional): A map with the following keys:with_ball
(Bool | FnPtr): Iftrue
, the robot will try to keep the ball while turning.
description
(optional): A string description for debugging.
FaceTowardsPosition
Rotates the robot to face a specific world position.
Syntax:
FaceTowardsPosition(target: Vec2 | FnPtr, [options: Map], [description: String]) -> BehaviorNode
Parameters:
target
: The target position to face. Can be a staticVec2
or a callback function that returns aVec2
.options
(optional): A map withwith_ball
(Bool | FnPtr).description
(optional): A string description for debugging.
FaceTowardsOwnPlayer
Rotates the robot to face a teammate.
Syntax:
FaceTowardsOwnPlayer(player_id: Int, [options: Map], [description: String]) -> BehaviorNode
Parameters:
player_id
: The ID of the teammate to face.options
(optional): A map withwith_ball
(Bool).description
(optional): A string description for debugging.
Kick
Kicks the ball. This skill assumes the robot has the ball.
Syntax:
Kick([description: String]) -> BehaviorNode
Parameters:
description
(optional): A string description for debugging.
Wait
Makes the robot wait for a specified duration.
Syntax:
Wait(duration_secs: Float, [description: String]) -> BehaviorNode
Parameters:
duration_secs
: The duration to wait in seconds.description
(optional): A string description for debugging.
FetchBall
Moves the robot to the ball to capture it.
Syntax:
FetchBall([description: String]) -> BehaviorNode
Parameters:
description
(optional): A string description for debugging.
InterceptBall
Moves the robot to intercept a moving ball.
Syntax:
InterceptBall([description: String]) -> BehaviorNode
Parameters:
description
(optional): A string description for debugging.
ApproachBall
Moves the robot close to the ball without necessarily capturing it.
Syntax:
ApproachBall([description: String]) -> BehaviorNode
Parameters:
description
(optional): A string description for debugging.
FetchBallWithHeadingAngle
Moves to the ball and aligns the robot to a specific angle after capturing it.
Syntax:
FetchBallWithHeadingAngle(angle_rad: Float, [description: String]) -> BehaviorNode
Parameters:
angle_rad
: The target angle in radians after fetching the ball.description
(optional): A string description for debugging.
FetchBallWithHeadingPosition
Moves to the ball and aligns the robot to face a specific position after capturing it.
Syntax:
FetchBallWithHeadingPosition(x: Float, y: Float, [description: String]) -> BehaviorNode
Parameters:
x
,y
: The coordinates of the target position to face.description
(optional): A string description for debugging.
FetchBallWithHeadingPlayer
Moves to the ball and aligns the robot to face a teammate after capturing it.
Syntax:
FetchBallWithHeadingPlayer(player_id: Int, [description: String]) -> BehaviorNode
Parameters:
player_id
: The ID of the teammate to face.description
(optional): A string description for debugging.
Helpers & The Situation Object
This section covers utility functions and the RobotSituation
object that provides world context to scripts.
Helper Functions
vec2
Creates a 2D vector. This is primarily for internal use but can be useful for clarity.
Syntax:
vec2(x: Float, y: Float) -> Vec2
Player ID Helpers
These functions help in working with PlayerId
's.
to_string
Converts a PlayerId
to its string representation.
Syntax:
to_string(id: PlayerId) -> String
hash_float
Returns a float between 0.0 and 1.0 based on the player's ID. This is a deterministic hash, meaning the same ID will always produce the same float. This is useful for inducing different behaviors for different players in a predictable way (e.g., distributing players on the field).
Syntax:
hash_float(id: PlayerId) -> Float
The RobotSituation
Object
When you define a condition function for a Guard
or a scorer function for a ScoringSelect
, it receives a single argument. This argument is an object that holds the current state of the world from the robot's perspective. It is a snapshot of the RobotSituation
struct from Rust.
Signature of callbacks:
#![allow(unused)] fn main() { fn my_condition(situation) -> bool { ... } fn my_scorer(situation) -> Float { ... } }
Accessing Data
You can access data from the situation
object to make decisions.
Note on Implementation Discrepancy: The original design for the scripting system specified that a simplified "view" of the
RobotSituation
would be created as a RhaiMap
for scripts to use. However, the current implementation passes the RustRobotSituation
struct directly. For fields and methods of this struct to be accessible from Rhai, they need to be registered with the Rhai engine. This registration does not appear to be implemented yet.Therefore, the API described below is based on the intended design and may not be fully functional until the necessary getters are registered in
rhai_plugin.rs
.
Here are the key properties you can expect to access from the situation
object:
situation.player_id
: ThePlayerId
of the current robot.situation.has_ball
: A boolean (true
orfalse
) indicating if the robot's breakbeam sensor detects the ball.situation.world
: An object containing the world data.situation.world.ball
: Information about the ball (e.g.,position
,velocity
).situation.world.own_players
: An array of data for your teammates.situation.world.opp_players
: An array of data for opponents.
situation.player
: Data for the current robot (e.g.,position
,velocity
).
Example Usage in a Condition:
#![allow(unused)] fn main() { fn is_ball_in_our_half(s) { // Access ball position from the world state if s.world.ball.position.x < 0.0 { return true; } else { return false; } } fn is_near_ball(s) { // Calculate distance between player and ball let dx = s.player.position.x - s.world.ball.position.x; let dy = s.player.position.y - s.world.ball.position.y; let dist_sq = dx*dx + dy*dy; return dist_sq < 500.0 * 500.0; // Is the robot within 500mm of the ball? } }
This powerful object allows you to create highly context-aware and dynamic behaviors.