Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 

Repository files navigation

Scarif AMR Game

A warehouse robotics simulation built on ROS 2 Jazzy and Gazebo. An autonomous mobile robot (AMR) navigates a 10x10 meter warehouse, picking up and delivering items between colored zones to score points. The system is designed as a testbed for comparing navigation strategies -- from simple heuristics to LLM-driven policies -- with full episode logging for analysis.

+------------------------------+
|          WAREHOUSE           |
|                              |
|  [PICKUP_B]       [PICKUP_A]|
|  (-3, 3)           (3, 3)   |
|          [obstacles]         |
|                              |
|         [START/ROBOT]        |
|           (0, 0)             |
|                              |
|  [DROPOFF_2]     [DROPOFF_1] |
|  (-3,-3)    [CHARGER] (3,-3)|
|              (0,-3)          |
+------------------------------+

Table of Contents


Setup

Requirements

  • Ubuntu 24.04
  • ROS 2 Jazzy
  • Python 3.12
  • colcon

Install dependencies

# Gazebo simulation + bridge packages
sudo apt install ros-jazzy-ros-gz ros-jazzy-ros-gz-bridge ros-jazzy-ros-gz-sim

# Optional: for external LLM policy
pip install openai

Build

cd ~/Scarif_AMR
source /opt/ros/jazzy/setup.bash
colcon build
source install/setup.bash

You must run source install/setup.bash in every new terminal before using any ros2 commands with this workspace.


Quick Start

source /opt/ros/jazzy/setup.bash
source install/setup.bash
ros2 launch amr_game_bringup play_heuristic.launch.py

This starts Gazebo (headless), spawns the robot, runs the game loop with a heuristic navigator, opens RViz, and begins logging to ~/.amr_game_logs/.

To run without the RViz window:

ros2 launch amr_game_bringup play_heuristic.launch.py viz:=false

How It Works

The Game Loop

The system runs a continuous sense-plan-act loop:

                    1 Hz
  Observation -----> Policy -----> ActionCommand
      ^                                |
      |                                v
  Executor <---- /odom, /lidar    Executor
  publishes       feedback        receives action
  observation                     executes turn+drive
      ^                                |
      |                                v
  Game Master                    Result (bool)
  checks zones,                  success/failure
  awards points                       |
      |                                v
      +------- Logger writes JSONL step record
  1. Executor publishes an ObservationState after each action (or on startup).
  2. Policy node reads the observation and computes an ActionCommand -- a turn angle, forward distance, and speed.
  3. Executor receives the command and executes it in two phases:
    • Turn phase: P-controller rotates to the target heading (tolerance: 0.05 rad).
    • Drive phase: P-controller drives the target distance (tolerance: 0.05 m).
    • If lidar detects an obstacle within 0.3 m ahead, the executor stops and reports failure.
    • If the robot is stuck for >3 seconds, a recovery maneuver backs up 0.3 m and turns 45 degrees.
  4. Game master monitors the robot's position against zone boundaries (0.5 m radius). When the robot reaches the pickup zone, the task transitions to the dropoff phase. Reaching the dropoff zone awards points and generates a new task.
  5. Logger records each completed step (observation, action, result) as a JSONL line.

Tasks and Scoring

Each task is a pickup-dropoff pair. The game master randomly selects one of two pickup zones and one of two dropoff zones. Each completed delivery awards 10 points. Tasks time out after 60 seconds if not completed. An episode ends after 10 tasks or 300 seconds, whichever comes first.

Battery

Battery starts at 100% and drains at 0.5% per meter traveled. When the robot enters the CHARGER zone (0, -3), battery recharges at 5% per second. If battery reaches 0%, the episode ends. The heuristic policy automatically routes to the charger when battery drops below 20%.


Architecture

Packages

The workspace contains 7 ROS 2 packages:

Package Build Type Description
amr_game_msgs ament_cmake Custom .msg definitions shared by all nodes
amr_game_description ament_cmake Robot SDF model, warehouse world SDF, Gazebo-ROS bridge config
amr_action_executor ament_python Closed-loop motion executor with P-controllers and safety checks
amr_game_master ament_python Game logic: task generation, zone detection, scoring, battery, episodes
amr_llm_policy ament_python Policy node with pluggable adapters (heuristic, mock LLM, external LLM)
amr_game_logger ament_python Writes each step to a JSONL log file
amr_game_bringup ament_python Launch files and configuration that tie everything together

Build order is enforced by dependencies: amr_game_msgs builds first (messages), then the rest in parallel.

Nodes

Node Package Subscribes Publishes
game_master_node amr_game_master /odom /game_state, /zone_markers
executor_node amr_action_executor /policy/action, /odom, /lidar, /game_state /cmd_vel, /executor/result, /observation
policy_node amr_llm_policy /observation, /executor/result /policy/action
logger_node amr_game_logger /observation, /policy/action, /executor/result, /game_state --

Topics

Topic Type Direction Description
/cmd_vel geometry_msgs/Twist ROS -> Gazebo Velocity commands to the robot
/odom nav_msgs/Odometry Gazebo -> ROS Robot position and velocity
/lidar sensor_msgs/LaserScan Gazebo -> ROS 360-sample lidar scan
/imu sensor_msgs/Imu Gazebo -> ROS Inertial measurement unit
/clock rosgraph_msgs/Clock Gazebo -> ROS Simulation time
/tf tf2_msgs/TFMessage Gazebo -> ROS Coordinate frame transforms
/game_state amr_game_msgs/GameState Game master Score, battery, current task, episode status
/observation amr_game_msgs/ObservationState Executor Full observation vector for the policy
/policy/action amr_game_msgs/ActionCommand Policy Commanded turn, drive distance, speed
/executor/result std_msgs/Bool Executor True if action succeeded, false if blocked
/zone_markers visualization_msgs/MarkerArray Game master Zone cylinders and labels for RViz

Services

Service Type Description
/reset_episode std_srvs/Empty Reset score, battery, task, and timer
ros2 service call /reset_episode std_srvs/srv/Empty

The Robot

The SDF model is a differential-drive robot with:

  • Chassis: 30x20x10 cm box, 5 kg
  • Drive wheels: 2 wheels (4 cm radius), 24 cm separation
  • Caster: Frictionless rear ball for stability
  • Lidar: 360-sample GPU lidar on top (8 m max range, 10 Hz)
  • IMU: 50 Hz inertial sensor

The DiffDrive plugin accepts Twist messages on /cmd_vel and publishes odometry on /odom.

The Warehouse

A 10x10 m enclosed space with:

  • 4 perimeter walls
  • 4 box obstacles at various positions
  • 6 colored ground zones (visible in Gazebo GUI and as RViz markers)

Launch Files

All launch files are in amr_game_bringup:

Launch File What It Starts
sim.launch.py Gazebo + robot spawn + ROS-GZ bridge
game.launch.py Game master + executor + logger
play_heuristic.launch.py sim + game + heuristic policy + RViz
play_mock_llm.launch.py sim + game + mock LLM policy + RViz
play_external_llm.launch.py sim + game + external LLM policy + RViz

Launch Arguments

All play_*.launch.py files:

Argument Default Description
viz true Open RViz with preconfigured view
headless true Run Gazebo in server-only mode (no Gazebo GUI)

play_external_llm.launch.py additionally:

Argument Default Description
openai_api_key '' API key (or set OPENAI_API_KEY env var)
openai_model gpt-4o-mini Which OpenAI model to query

Examples

# Heuristic with RViz (default)
ros2 launch amr_game_bringup play_heuristic.launch.py

# Heuristic, no visualization
ros2 launch amr_game_bringup play_heuristic.launch.py viz:=false

# Heuristic with Gazebo GUI (needs display)
ros2 launch amr_game_bringup play_heuristic.launch.py headless:=false

# External LLM
ros2 launch amr_game_bringup play_external_llm.launch.py openai_api_key:=sk-...

# Run sim and game separately
ros2 launch amr_game_bringup sim.launch.py          # terminal 1
ros2 launch amr_game_bringup game.launch.py          # terminal 2

Configuration

All tunable parameters live in src/amr_game_bringup/config/game_params.yaml and are loaded by every node via the launch files.

Game Rules

Parameter Default Description
task_points 10 Points per completed delivery
task_time_limit 60.0 s Time before a task expires
max_tasks_per_episode 10 Episode ends after this many tasks
episode_timeout 300.0 s Episode hard time limit
battery_drain_per_meter 0.5 Battery % lost per meter traveled
battery_charge_rate 5.0 Battery % gained per second at charger

Motion Executor

Parameter Default Description
turn_kp 2.0 Proportional gain for yaw controller
drive_kp 1.0 Proportional gain for distance controller
turn_tolerance 0.05 rad Acceptable yaw error (~3 degrees)
drive_tolerance 0.05 m Acceptable position error
obstacle_distance 0.3 m Lidar stop threshold
stuck_timeout 3.0 s Time before recovery triggers
recovery_backup 0.3 m How far to reverse during recovery
recovery_turn 0.785 rad How much to turn during recovery (45 deg)
control_rate 20.0 Hz Executor inner loop frequency

Policy

Parameter Default Description
policy_rate 1.0 Hz How often the policy runs

Action Limits

Actions are clamped to safe ranges before being sent to the executor:

Dimension Range
d_forward_meters [-2.0, 2.0]
d_turn_radians [-pi, pi]
speed_mps [0.0, 1.0]

Custom Messages

Defined in amr_game_msgs:

ActionCommand.msg

float64 d_forward_meters    # Distance to drive (negative = reverse)
float64 d_turn_radians      # Angle to turn before driving
float64 speed_mps           # Movement speed
string  reason              # Human-readable explanation

TaskInfo.msg

string  task_id             # Unique identifier (8-char hex)
string  pickup_zone         # e.g. "PICKUP_A"
string  dropoff_zone        # e.g. "DROPOFF_1"
float64 time_remaining      # Seconds until timeout
int32   points              # Reward for completion

GameState.msg

int32      score            # Cumulative episode score
float64    battery_pct      # 0-100
TaskInfo   current_task
TaskInfo[] pending_tasks
bool       episode_active

ObservationState.msg

The full observation vector consumed by policy adapters:

geometry_msgs/Pose   pose                # Robot position + orientation
geometry_msgs/Twist  velocity            # Current linear + angular velocity
float64              battery             # Battery percentage
int32                score               # Current score
TaskInfo             task                # Active task details
float64[36]          lidar_histogram     # Min range per 10-degree sector
ActionCommand        last_action         # Previous action
bool                 last_action_succeeded

EpisodeStep.msg

int32                step_number
ObservationState     observation
ActionCommand        action
float64              reward_delta
bool                 collision
builtin_interfaces/Time timestamp

Policy Adapters

The policy_node uses a pluggable adapter pattern. Set the policy_type parameter to select which adapter runs.

heuristic (default)

A handwritten navigation strategy:

  1. Compute atan2 heading to the current goal zone.
  2. Turn toward the goal, then drive forward (up to 2 m per step).
  3. If lidar detects an obstacle ahead, reduce step size and turn to avoid.
  4. If battery < 20%, override the goal to CHARGER.

This is deterministic and requires no external services.

mock_llm

Returns a fixed action every step: drive 0.5 m forward at 0.3 m/s with no turning. Useful for testing the pipeline without needing real decision-making.

external_llm

Calls the OpenAI API each step:

  1. Formats the ObservationState into a structured prompt (see prompt_template.py).
  2. Sends the prompt to the configured model with temperature=0.2.
  3. Parses the JSON response into an ActionCommand.
  4. Falls back to a safe stop if the response can't be parsed.

The prompt includes the robot's position, battery, task, lidar histogram, and a description of all zone positions. The LLM is instructed to respond with a JSON object containing d_forward_meters, d_turn_radians, speed_mps, and reason.

Requires pip install openai and an API key.

Writing a Custom Adapter

Create a Python module with a compute_action(obs: ObservationState) -> ActionCommand function and place it in amr_llm_policy/adapters/. Then add an import branch in policy_node.py for your new policy_type string.


Visualization

RViz launches automatically with all play_*.launch.py files (disable with viz:=false).

The preconfigured RViz view shows:

  • Top-down orthographic camera covering the full warehouse
  • Grid at 1 m spacing
  • LaserScan (red dots) from /lidar
  • Odometry arrows (green) showing the robot's path from /odom
  • TF frames for the robot links
  • Zone markers: colored cylinders at each zone with text labels, plus a white ring around the current goal

Warehouse Zones

Zone Position Color Role
START (0, 0) Green Robot spawn point
PICKUP_A (3, 3) Yellow Pickup location
PICKUP_B (-3, 3) Orange Pickup location
DROPOFF_1 (3, -3) Blue Delivery location
DROPOFF_2 (-3, -3) Purple Delivery location
CHARGER (0, -3) Cyan Battery recharge station

Logging

Every completed action step is written to a JSONL file in ~/.amr_game_logs/. A new file is created for each episode.

Filename format: episode_YYYYMMDD_HHMMSS.jsonl

Record format (one JSON object per line):

{
  "step": 1,
  "timestamp": 1770433460.14,
  "observation": {
    "pose": {"x": 1.44, "y": 1.32, "z": 0.0},
    "velocity": {"linear_x": 0.05, "linear_y": 0.0, "angular_z": 0.0},
    "battery": 99.06,
    "score": 0,
    "task": {
      "task_id": "dcecdfdd",
      "pickup_zone": "PICKUP_A",
      "dropoff_zone": "DROPOFF_2",
      "time_remaining": 58.0,
      "points": 10
    },
    "lidar_histogram": [5.1, 5.18, ...],
    "last_action_succeeded": true
  },
  "action": {
    "d_forward_meters": 2.0,
    "d_turn_radians": 0.785,
    "speed_mps": 0.3,
    "reason": "heading to pickup PICKUP_A"
  },
  "result": {
    "collision": false,
    "succeeded": true
  },
  "game_state": {
    "score": 0,
    "battery_pct": 99.06,
    "episode_active": true,
    "current_task": {
      "task_id": "dcecdfdd",
      "pickup_zone": "PICKUP_A",
      "dropoff_zone": "DROPOFF_2"
    }
  }
}

Working with Logs

import json

with open("~/.amr_game_logs/episode_20260206_220406.jsonl") as f:
    steps = [json.loads(line) for line in f]

# Collision rate
collisions = sum(1 for s in steps if s["result"]["collision"])
print(f"{collisions}/{len(steps)} steps had collisions")

# Final score
print(f"Final score: {steps[-1]['game_state']['score']}")

Testing

source /opt/ros/jazzy/setup.bash
colcon build
source install/setup.bash
colcon test
colcon test-result --verbose

The test suite covers:

Test File Package What It Tests
test_lidar_utils.py amr_action_executor Lidar binning, forward clearance checks, edge cases
test_executor_logic.py amr_action_executor Quaternion-to-yaw extraction, angle normalization
test_action_clamping.py amr_llm_policy Action value clamping at all boundaries
test_schema.py amr_game_logger JSONL record validation
test_zones.py amr_game_master Zone definitions, coordinate integrity

Project Structure

Scarif_AMR/
  src/
    amr_game_msgs/               # Custom message definitions
      msg/
        ActionCommand.msg
        TaskInfo.msg
        GameState.msg
        ObservationState.msg
        EpisodeStep.msg
      CMakeLists.txt
      package.xml

    amr_game_description/        # Robot + world models
      models/amr_robot/
        model.sdf                # Diff-drive robot with lidar + IMU
      worlds/
        warehouse.sdf            # 10x10m warehouse with zones + obstacles
      config/
        bridge.yaml              # ROS-Gazebo topic bridge mapping
      launch/
        spawn.launch.py          # Gazebo + spawn + bridge

    amr_action_executor/         # Motion execution
      amr_action_executor/
        executor_node.py         # P-controller turn/drive with safety
        lidar_utils.py           # Histogram binning, clearance checks
      test/
        test_lidar_utils.py
        test_executor_logic.py

    amr_game_master/             # Game logic
      amr_game_master/
        game_master_node.py      # Tasks, scoring, battery, zones, markers
        zones.py                 # Zone position constants
      test/
        test_zones.py

    amr_llm_policy/              # Policy adapters
      amr_llm_policy/
        policy_node.py           # Adapter dispatcher, action clamping
        prompt_template.py       # Observation -> LLM prompt formatter
        adapters/
          heuristic_policy.py    # atan2 navigation
          mock_llm_policy.py     # Fixed safe action
          external_llm_policy.py # OpenAI API integration
      test/
        test_action_clamping.py

    amr_game_logger/             # Episode logging
      amr_game_logger/
        logger_node.py           # JSONL writer
        schema.py                # Message-to-dict conversion, validation
      test/
        test_schema.py

    amr_game_bringup/            # Launch orchestration
      launch/
        sim.launch.py
        game.launch.py
        play_heuristic.launch.py
        play_mock_llm.launch.py
        play_external_llm.launch.py
      config/
        game_params.yaml         # All tunable parameters
        amr_game.rviz            # RViz display configuration

About

AMR Game — ROS 2 + Gazebo warehouse robot simulation with LLM policy adapters

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages