use bucket_brigade_core::{Action, AgentObservation, BucketBrigade, Scenario};
use crate::env::{Environment, SpaceInfo, SpaceType, StepInfo, StepResult};
pub const NUM_HOUSES: usize = 10;
pub const ACTION_DIMS: usize = 3;
pub const SCENARIO_INFO_LEN: usize = 12;
pub struct BucketBrigadeMaEnv {
inner: BucketBrigade,
scenario: Scenario,
num_agents: usize,
num_houses: usize,
}
#[derive(Debug, Clone)]
pub struct MaStepResult {
pub rewards: Vec<f32>,
pub done: bool,
pub observations: Vec<Vec<f32>>,
}
impl BucketBrigadeMaEnv {
pub fn new(scenario: Scenario, num_agents: usize, seed: Option<u64>) -> Self {
let num_houses = scenario.num_houses as usize;
let inner = BucketBrigade::new(scenario.clone(), num_agents, seed);
Self { inner, scenario, num_agents, num_houses }
}
pub fn from_scenario_id(
scenario_id: &str,
num_agents: Option<usize>,
seed: Option<u64>,
) -> Result<Self, String> {
let scenario = super::registry::get_scenario_by_id(scenario_id)?;
let n = num_agents
.or_else(|| super::registry::default_num_agents_for(scenario_id))
.unwrap_or(super::registry::DEFAULT_NUM_AGENTS);
Ok(Self::new(scenario, n, seed))
}
pub fn num_agents(&self) -> usize {
self.num_agents
}
pub fn num_houses(&self) -> usize {
self.num_houses
}
pub fn action_dims(&self) -> Vec<i64> {
vec![self.num_houses as i64, 2, 2]
}
pub fn obs_dim(&self) -> usize {
1 + self.num_houses
+ self.num_agents + self.num_agents + ACTION_DIMS * self.num_agents + self.num_agents + SCENARIO_INFO_LEN
}
pub fn scenario(&self) -> &Scenario {
&self.scenario
}
#[allow(dead_code)]
pub(super) fn inner(&self) -> &BucketBrigade {
&self.inner
}
pub fn reset(&mut self, seed: Option<u64>) -> Vec<Vec<f32>> {
if let Some(seed) = seed {
self.inner = BucketBrigade::new(self.scenario.clone(), self.num_agents, Some(seed));
} else {
self.inner.reset();
}
self.collect_observations()
}
pub fn step(&mut self, actions: &[Action]) -> MaStepResult {
assert_eq!(
actions.len(),
self.num_agents,
"BucketBrigadeMaEnv::step: expected {} actions, got {}",
self.num_agents,
actions.len()
);
let rust_actions: Vec<Action> = actions.to_vec();
let result = self.inner.step(&rust_actions);
let observations = self.collect_observations();
MaStepResult { rewards: result.rewards, done: result.done, observations }
}
fn collect_observations(&self) -> Vec<Vec<f32>> {
(0..self.num_agents)
.map(|i| {
flatten_observation(
&self.inner.get_observation(i),
self.num_houses,
i,
self.num_agents,
)
})
.collect()
}
}
impl Environment for BucketBrigadeMaEnv {
type Action = i64;
type State = ();
fn reset(&mut self) {
let _ = BucketBrigadeMaEnv::reset(self, None);
}
fn get_observation(&self) -> Vec<f32> {
flatten_observation(&self.inner.get_observation(0), self.num_houses, 0, self.num_agents)
}
fn step(&mut self, action: i64) -> StepResult {
let signal = (action % 2) as u8;
let mode = ((action / 2) % 2) as u8;
let house = ((action / 4) % self.num_houses as i64) as u8;
let actions: Vec<Action> = (0..self.num_agents).map(|_| [house, mode, signal]).collect();
let r = BucketBrigadeMaEnv::step(self, &actions);
StepResult {
observation: r.observations.into_iter().next().unwrap_or_default(),
reward: r.rewards.first().copied().unwrap_or(0.0),
terminated: r.done,
truncated: false,
info: StepInfo::default(),
}
}
fn observation_space(&self) -> SpaceInfo {
SpaceInfo { shape: vec![self.obs_dim()], space_type: SpaceType::Box }
}
fn action_space(&self) -> SpaceInfo {
SpaceInfo { shape: vec![], space_type: SpaceType::Discrete(self.num_houses * 2 * 2) }
}
fn render(&self) -> Vec<u8> {
Vec::new()
}
fn close(&mut self) {}
fn clone_state(&self) {}
fn restore_state(&mut self, _state: &()) {}
}
#[cfg(feature = "training")]
impl crate::multi_agent::JointEnv for BucketBrigadeMaEnv {
fn reset_joint(&mut self, seed: Option<u64>) -> Vec<Vec<f32>> {
BucketBrigadeMaEnv::reset(self, seed)
}
fn step_joint(&mut self, actions: &[Vec<i64>]) -> crate::multi_agent::JointStepResult {
debug_assert_eq!(
actions.len(),
self.num_agents,
"BucketBrigadeMaEnv::step_joint: expected {} agent actions, got {}",
self.num_agents,
actions.len()
);
let acts: Vec<Action> = actions
.iter()
.map(|a| {
debug_assert_eq!(
a.len(),
ACTION_DIMS,
"per-agent action must be length {} ([house, mode, signal]), got {}",
ACTION_DIMS,
a.len()
);
[a[0] as u8, a[1] as u8, a[2] as u8]
})
.collect();
let res = BucketBrigadeMaEnv::step(self, &acts);
crate::multi_agent::JointStepResult {
rewards: res.rewards,
done: res.done,
observations: res.observations,
}
}
}
pub(crate) fn flatten_observation(
obs: &AgentObservation,
num_houses: usize,
agent_id: usize,
num_agents: usize,
) -> Vec<f32> {
let n = 1 + num_houses
+ obs.signals.len()
+ obs.locations.len()
+ obs.last_actions.len() * ACTION_DIMS
+ obs.round1_signals.len()
+ obs.scenario_info.len();
let mut out = Vec::with_capacity(n);
let denom = (num_agents.saturating_sub(1)).max(1) as f32;
out.push(agent_id as f32 / denom);
debug_assert_eq!(obs.houses.len(), num_houses);
out.extend(obs.houses.iter().map(|&v| v as f32));
out.extend(obs.signals.iter().map(|&v| v as f32));
out.extend(obs.locations.iter().map(|&v| v as f32));
for [house, mode, signal] in &obs.last_actions {
out.push(*house as f32);
out.push(*mode as f32);
out.push(*signal as f32);
}
out.extend(obs.round1_signals.iter().map(|&v| v as f32));
out.extend(obs.scenario_info.iter().copied());
out
}