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}