use crate::{
env::{Environment, SpaceInfo, SpaceType, StepInfo, StepResult},
multi_agent::{
comms::{CommunicatingEnvironment, Delivery, place_message, split_action},
environment::{MultiAgentEnvironment, MultiAgentResult},
joint::{JointEnv, JointStepResult},
},
};
pub const SPEAKER: usize = 0;
pub const LISTENER: usize = 1;
#[derive(Debug, Clone)]
pub struct SignalingGame {
vocab: usize,
hidden: i64,
received: i64,
}
impl SignalingGame {
pub const NUM_AGENTS: usize = 2;
pub fn new(vocab: usize) -> Self {
assert!(vocab > 0, "SignalingGame requires vocab >= 1");
Self { vocab, hidden: 0, received: -1 }
}
pub fn with_hidden(vocab: usize, hidden: i64) -> Self {
assert!(vocab > 0, "SignalingGame requires vocab >= 1");
assert!(
hidden >= 0 && (hidden as usize) < vocab,
"hidden token {hidden} out of range 0..{vocab}"
);
Self { vocab, hidden, received: -1 }
}
pub fn vocab(&self) -> usize {
self.vocab
}
pub fn hidden(&self) -> i64 {
self.hidden
}
pub fn received(&self) -> i64 {
self.received
}
pub fn set_hidden(&mut self, hidden: i64) {
assert!(
hidden >= 0 && (hidden as usize) < self.vocab,
"hidden token {hidden} out of range 0..{}",
self.vocab
);
self.hidden = hidden;
self.received = -1;
}
fn speaker_obs(&self) -> Vec<f32> {
let mut obs = vec![0.0_f32; self.vocab];
obs[self.hidden as usize] = 1.0;
obs
}
fn listener_obs(&self) -> Vec<f32> {
let mut obs = vec![-1.0_f32; self.message_obs_size(LISTENER)];
place_message(&mut obs, 0, &[self.received]);
obs
}
}
impl Default for SignalingGame {
fn default() -> Self {
Self::new(2)
}
}
impl Environment for SignalingGame {
type Action = i64;
type State = (i64, i64);
fn reset(&mut self) {
self.received = -1;
}
fn get_observation(&self) -> Vec<f32> {
self.speaker_obs()
}
fn step(&mut self, action: i64) -> StepResult {
let r = self.step_multi(&[vec![action], vec![action]]);
StepResult {
observation: r.observations.into_iter().next().unwrap_or_default(),
reward: r.rewards.first().copied().unwrap_or(0.0),
terminated: r.terminated.first().copied().unwrap_or(false),
truncated: false,
info: StepInfo::default(),
}
}
fn observation_space(&self) -> SpaceInfo {
SpaceInfo { shape: vec![self.vocab], space_type: SpaceType::Box }
}
fn action_space(&self) -> SpaceInfo {
SpaceInfo { shape: vec![], space_type: SpaceType::Discrete(self.vocab) }
}
fn render(&self) -> Vec<u8> {
Vec::new()
}
fn close(&mut self) {}
fn clone_state(&self) -> (i64, i64) {
(self.hidden, self.received)
}
fn restore_state(&mut self, state: &(i64, i64)) {
self.hidden = state.0;
self.received = state.1;
}
}
impl MultiAgentEnvironment for SignalingGame {
fn num_agents(&self) -> usize {
Self::NUM_AGENTS
}
fn get_agent_observation(&self, agent_id: usize) -> Vec<f32> {
match agent_id {
SPEAKER => self.speaker_obs(),
LISTENER => self.listener_obs(),
other => panic!("SignalingGame has 2 agents; no agent {other}"),
}
}
fn agent_action_space(&self, agent_id: usize) -> Vec<usize> {
match agent_id {
SPEAKER => Vec::new(),
LISTENER => vec![self.vocab],
other => panic!("SignalingGame has 2 agents; no agent {other}"),
}
}
fn step_multi(&mut self, actions: &[Vec<i64>]) -> MultiAgentResult {
assert_eq!(actions.len(), Self::NUM_AGENTS, "signaling game requires 2 action vectors");
let (_speaker_task, speaker_msg) =
split_action(&actions[SPEAKER], self.agent_action_space(SPEAKER).len());
let (listener_task, _listener_msg) =
split_action(&actions[LISTENER], self.agent_action_space(LISTENER).len());
debug_assert!(self.delivery().reaches(SPEAKER, LISTENER));
self.received = speaker_msg.first().copied().unwrap_or(-1);
let guess = listener_task.first().copied().unwrap_or(-1);
let correct = guess == self.hidden;
let reward = if correct { 1.0_f32 } else { 0.0 };
MultiAgentResult::new(
vec![self.speaker_obs(), self.listener_obs()],
vec![reward, reward],
vec![true, true],
vec![false, false],
)
}
fn active_agents(&self) -> Vec<bool> {
vec![true; Self::NUM_AGENTS]
}
}
impl JointEnv for SignalingGame {
fn reset_joint(&mut self, _seed: Option<u64>) -> Vec<Vec<f32>> {
self.reset();
(0..self.num_agents()).map(|i| self.get_agent_observation(i)).collect()
}
fn step_joint(&mut self, actions: &[Vec<i64>]) -> JointStepResult {
let result = self.step_multi(actions);
JointStepResult {
rewards: result.rewards,
done: result.terminated.iter().all(|&t| t),
observations: result.observations,
}
}
}
impl CommunicatingEnvironment for SignalingGame {
fn message_vocab(&self, agent_id: usize) -> Vec<usize> {
match agent_id {
SPEAKER => vec![self.vocab],
_ => Vec::new(),
}
}
fn message_obs_size(&self, agent_id: usize) -> usize {
match agent_id {
LISTENER => 1,
_ => 0,
}
}
fn delivery(&self) -> Delivery {
Delivery::Broadcast
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn comms_layout_matches_design() {
let env = SignalingGame::new(4);
assert_eq!(env.agent_action_space(SPEAKER), Vec::<usize>::new());
assert_eq!(env.message_vocab(SPEAKER), vec![4]);
assert_eq!(env.message_obs_size(SPEAKER), 0);
assert_eq!(env.agent_action_space(LISTENER), vec![4]);
assert_eq!(env.message_vocab(LISTENER), Vec::<usize>::new());
assert_eq!(env.message_obs_size(LISTENER), 1);
assert_eq!(env.delivery(), Delivery::Broadcast);
}
#[test]
fn speaker_observes_hidden_token_one_hot() {
let env = SignalingGame::with_hidden(5, 3);
let obs = env.get_agent_observation(SPEAKER);
assert_eq!(obs, vec![0.0, 0.0, 0.0, 1.0, 0.0]);
}
#[test]
fn message_round_trips_speaker_to_listener() {
let mut env = SignalingGame::with_hidden(4, 2);
let token = 2_i64;
let result = env.step_multi(&[vec![token], vec![0]]);
let listener_obs = &result.observations[LISTENER];
assert_eq!(listener_obs.len(), env.message_obs_size(LISTENER));
assert_eq!(listener_obs[0], token as f32);
assert_eq!(env.get_agent_observation(LISTENER)[0], token as f32);
assert_eq!(env.received(), token);
}
#[test]
fn reward_is_one_when_listener_guesses_hidden_token() {
let mut env = SignalingGame::with_hidden(3, 1);
let correct = env.step_multi(&[vec![1], vec![1]]);
assert_eq!(correct.rewards, vec![1.0, 1.0]);
let mut env = SignalingGame::with_hidden(3, 1);
let wrong = env.step_multi(&[vec![1], vec![0]]);
assert_eq!(wrong.rewards, vec![0.0, 0.0]);
}
#[test]
fn listener_obs_before_step_is_sentinel() {
let env = SignalingGame::new(3);
assert_eq!(env.get_agent_observation(LISTENER), vec![-1.0]);
}
#[test]
fn reset_clears_received_message() {
let mut env = SignalingGame::with_hidden(4, 0);
env.step_multi(&[vec![3], vec![0]]);
assert_eq!(env.received(), 3);
env.reset();
assert_eq!(env.received(), -1);
assert_eq!(env.get_agent_observation(LISTENER), vec![-1.0]);
}
#[test]
fn snapshot_restore_round_trips() {
let mut env = SignalingGame::with_hidden(4, 2);
env.step_multi(&[vec![3], vec![0]]);
let snap = env.clone_state();
env.step_multi(&[vec![1], vec![0]]);
assert_eq!(env.received(), 1);
env.restore_state(&snap);
assert_eq!(env.hidden(), 2);
assert_eq!(env.received(), 3);
}
#[test]
fn num_agents_and_active() {
let env = SignalingGame::default();
assert_eq!(env.num_agents(), 2);
assert_eq!(env.active_agents(), vec![true, true]);
}
}