Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 feature
  • fix: bug fix
  • refactor: refactoring
  • docs: documentation
  • test: adding tests
  • chore: other changes (eg. CI, formatting, etc.)
  • wip: work in progress
  • misc: 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 executor
  • warn: should be used for unexpected events or recoverable errors, which do not cause the executor to stop and do not get propagated up
  • info: 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 succeed
  • debug: 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 returns Running, the Sequence node also returns Running and will resume from that child on the next tick. It returns Success 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 returns Running, the Select node also returns Running and will resume from that child on the next tick. It returns Failure 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 for options parameters in many skills and for defining scorers in ScoringSelect.
    #![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 and Guard 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 Fails, 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.

  1. It first tries the Sequence.
  2. The Sequence starts with a Guard. If i_have_ball returns false, the Guard fails, which makes the Sequence fail.
  3. The Select node then moves to its next child, which is FetchBall().
  4. If i_have_ball returns true, the Guard succeeds, and the Sequence continues to face the goal and kick. If the Sequence succeeds, the Select 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.
  • 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 returns Success.
  • Returns Running if any child returns Running.
  • Returns Failure only if all children return Failure.

This is equivalent to a logical OR.

Syntax:

#![allow(unused)]
fn main() {
Select(children: Array, [description: String]) -> BehaviorNode
}

Parameters:

  • children: An array of BehaviorNodes.
  • 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 returns Failure.
  • Returns Running if any child returns Running.
  • Returns Success only if all children return Success.

This is equivalent to a logical AND.

Syntax:

#![allow(unused)]
fn main() {
Sequence(children: Array, [description: String]) -> BehaviorNode
}

Parameters:

  • children: An array of BehaviorNodes.
  • 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: The BehaviorNode child.
    • scorer: A function pointer to a scorer function. The scorer function receives the RobotSituation 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 the RobotSituation object and must return true or false.
  • child: The BehaviorNode 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: The BehaviorNode 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 static Vec2 (created with vec2(x, y)) or a callback function that returns a Vec2.
  • 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): If true, the robot will try to keep the ball while moving.
    • avoid_ball (Bool | FnPtr): If true, 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 static Float or a callback function that returns a Float.
  • options (optional): A map with the following keys:
    • with_ball (Bool | FnPtr): If true, 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 static Vec2 or a callback function that returns a Vec2.
  • options (optional): A map with with_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 with with_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 Rhai Map for scripts to use. However, the current implementation passes the Rust RobotSituation 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: The PlayerId of the current robot.
  • situation.has_ball: A boolean (true or false) 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.