Skip to main content

thrust_rl/multi_agent/
environment.rs

1//! Multi-agent environment trait.
2//!
3//! Extends the base [`crate::env::Environment`] trait to support multiple
4//! agents interacting in the same game instance, enabling cooperative,
5//! competitive, and mixed-motive scenarios.
6//!
7//! # Multi-discrete action spaces
8//!
9//! This trait supports both pure-discrete and *factored* (multi-discrete)
10//! action spaces. Each agent's action is a `Vec<i64>` whose length equals the
11//! number of action dimensions for that agent:
12//!
13//! - Pure-discrete agent with `n` choices: action vector has length 1 (e.g.
14//!   `vec![3]` to pick action `3`).
15//! - Factored Bucket-Brigade-style agent with `[house_index, mode]`: action
16//!   vector has length 2 (e.g. `vec![7, 1]` for "house 7, mode 1").
17//!
18//! The per-agent layout is published via
19//! [`MultiAgentEnvironment::agent_action_space`].
20//!
21//! # Burn migration note
22//!
23//! This trait holds no tensor types — it only deals in `Vec<f32>` host
24//! observations and `Vec<i64>` host actions. The Burn-native
25//! [`crate::multi_agent::joint`] trainer is the place where Burn tensors enter
26//! the picture; the environment surface stays backend-agnostic.
27
28use std::collections::HashMap;
29
30use crate::env::Environment;
31
32/// Multi-agent environment trait.
33///
34/// Environments implementing this trait support multiple agents interacting
35/// in the same game instance. The base [`Environment`] trait provides the
36/// single-agent reset / observation / action-space methods; this trait
37/// extends it with the per-agent variants needed by the multi-agent
38/// trainers.
39pub trait MultiAgentEnvironment: Environment {
40    /// Number of agents in this environment.
41    fn num_agents(&self) -> usize;
42
43    /// Get observation for a specific agent.
44    ///
45    /// # Arguments
46    ///
47    /// * `agent_id` - Index of the agent (`0..num_agents`).
48    fn get_agent_observation(&self, agent_id: usize) -> Vec<f32>;
49
50    /// Per-agent action-space layout.
51    ///
52    /// Returns one bin count per action dimension for the agent. For a
53    /// pure-discrete agent with `n` choices this is `vec![n]`; for a
54    /// factored agent with `[house_index, mode]` this is `vec![10, 2]`.
55    ///
56    /// The length of this vector must match the length of each per-agent
57    /// action passed to [`MultiAgentEnvironment::step_multi`], and it must
58    /// match the per-dim cardinalities of any policy driving this agent.
59    fn agent_action_space(&self, agent_id: usize) -> Vec<usize>;
60
61    /// Step the environment with multiple actions (one per agent).
62    ///
63    /// # Arguments
64    ///
65    /// * `actions` - One action vector per agent. `actions.len()` must equal
66    ///   [`MultiAgentEnvironment::num_agents`]. Each inner `Vec<i64>` carries
67    ///   one entry per action dimension and must match the layout reported by
68    ///   [`MultiAgentEnvironment::agent_action_space`].
69    ///
70    /// # Returns
71    ///
72    /// Multi-agent result containing observations, rewards, and termination
73    /// flags for each agent.
74    fn step_multi(&mut self, actions: &[Vec<i64>]) -> MultiAgentResult;
75
76    /// Get which agents are currently active (not terminated).
77    fn active_agents(&self) -> Vec<bool>;
78}
79
80/// Result of a multi-agent environment step.
81///
82/// Contains per-agent observations, rewards, and termination flags.
83#[derive(Debug, Clone)]
84pub struct MultiAgentResult {
85    /// Observations for each agent.
86    pub observations: Vec<Vec<f32>>,
87
88    /// Rewards for each agent.
89    pub rewards: Vec<f32>,
90
91    /// Terminal states for each agent (true = episode ended naturally,
92    /// e.g. agent died or goal reached). GAE zeroes the value-function
93    /// bootstrap across these transitions.
94    pub terminated: Vec<bool>,
95
96    /// Truncation flags for each agent (true = episode ended due to a
97    /// time limit or external reset, not a terminal state). GAE keeps
98    /// the value-function bootstrap across these transitions because
99    /// the trajectory was still "alive" in the value sense. Follows the
100    /// gym/gymnasium convention.
101    pub truncated: Vec<bool>,
102
103    /// Additional information (shared across all agents).
104    pub info: HashMap<String, String>,
105}
106
107impl MultiAgentResult {
108    /// Create a new multi-agent result.
109    pub fn new(
110        observations: Vec<Vec<f32>>,
111        rewards: Vec<f32>,
112        terminated: Vec<bool>,
113        truncated: Vec<bool>,
114    ) -> Self {
115        Self { observations, rewards, terminated, truncated, info: HashMap::new() }
116    }
117
118    /// Check if all agents are done (either terminated or truncated).
119    pub fn all_done(&self) -> bool {
120        self.terminated.iter().zip(&self.truncated).all(|(term, trunc)| *term || *trunc)
121    }
122
123    /// Check if any agent is done.
124    pub fn any_done(&self) -> bool {
125        self.terminated.iter().zip(&self.truncated).any(|(term, trunc)| *term || *trunc)
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_multi_agent_result_all_done() {
135        let result = MultiAgentResult::new(
136            vec![vec![0.0; 4], vec![0.0; 4]],
137            vec![0.0, 0.0],
138            vec![true, true],
139            vec![false, false],
140        );
141
142        assert!(result.all_done());
143        assert!(result.any_done());
144    }
145
146    #[test]
147    fn test_multi_agent_result_any_done() {
148        let result = MultiAgentResult::new(
149            vec![vec![0.0; 4], vec![0.0; 4]],
150            vec![0.0, 0.0],
151            vec![true, false],
152            vec![false, false],
153        );
154
155        assert!(!result.all_done());
156        assert!(result.any_done());
157    }
158
159    #[test]
160    fn test_multi_agent_result_none_done() {
161        let result = MultiAgentResult::new(
162            vec![vec![0.0; 4], vec![0.0; 4]],
163            vec![0.0, 0.0],
164            vec![false, false],
165            vec![false, false],
166        );
167
168        assert!(!result.all_done());
169        assert!(!result.any_done());
170    }
171}