1use crate::{
45 env::{Environment, SpaceInfo, SpaceType, StepInfo, StepResult},
46 multi_agent::{
47 comms::{CommunicatingEnvironment, Delivery, place_message, split_action},
48 environment::{MultiAgentEnvironment, MultiAgentResult},
49 joint::{JointEnv, JointStepResult},
50 },
51};
52
53pub const SPEAKER: usize = 0;
55pub const LISTENER: usize = 1;
57
58#[derive(Debug, Clone)]
63pub struct SignalingGame {
64 vocab: usize,
67 hidden: i64,
69 received: i64,
72}
73
74impl SignalingGame {
75 pub const NUM_AGENTS: usize = 2;
77
78 pub fn new(vocab: usize) -> Self {
85 assert!(vocab > 0, "SignalingGame requires vocab >= 1");
86 Self { vocab, hidden: 0, received: -1 }
87 }
88
89 pub fn with_hidden(vocab: usize, hidden: i64) -> Self {
96 assert!(vocab > 0, "SignalingGame requires vocab >= 1");
97 assert!(
98 hidden >= 0 && (hidden as usize) < vocab,
99 "hidden token {hidden} out of range 0..{vocab}"
100 );
101 Self { vocab, hidden, received: -1 }
102 }
103
104 pub fn vocab(&self) -> usize {
106 self.vocab
107 }
108
109 pub fn hidden(&self) -> i64 {
111 self.hidden
112 }
113
114 pub fn received(&self) -> i64 {
117 self.received
118 }
119
120 pub fn set_hidden(&mut self, hidden: i64) {
126 assert!(
127 hidden >= 0 && (hidden as usize) < self.vocab,
128 "hidden token {hidden} out of range 0..{}",
129 self.vocab
130 );
131 self.hidden = hidden;
132 self.received = -1;
133 }
134
135 fn speaker_obs(&self) -> Vec<f32> {
137 let mut obs = vec![0.0_f32; self.vocab];
138 obs[self.hidden as usize] = 1.0;
139 obs
140 }
141
142 fn listener_obs(&self) -> Vec<f32> {
146 let mut obs = vec![-1.0_f32; self.message_obs_size(LISTENER)];
147 place_message(&mut obs, 0, &[self.received]);
148 obs
149 }
150}
151
152impl Default for SignalingGame {
153 fn default() -> Self {
155 Self::new(2)
156 }
157}
158
159impl Environment for SignalingGame {
160 type Action = i64;
161 type State = (i64, i64);
164
165 fn reset(&mut self) {
166 self.received = -1;
167 }
168
169 fn get_observation(&self) -> Vec<f32> {
172 self.speaker_obs()
173 }
174
175 fn step(&mut self, action: i64) -> StepResult {
179 let r = self.step_multi(&[vec![action], vec![action]]);
180 StepResult {
181 observation: r.observations.into_iter().next().unwrap_or_default(),
182 reward: r.rewards.first().copied().unwrap_or(0.0),
183 terminated: r.terminated.first().copied().unwrap_or(false),
184 truncated: false,
185 info: StepInfo::default(),
186 }
187 }
188
189 fn observation_space(&self) -> SpaceInfo {
190 SpaceInfo { shape: vec![self.vocab], space_type: SpaceType::Box }
191 }
192
193 fn action_space(&self) -> SpaceInfo {
194 SpaceInfo { shape: vec![], space_type: SpaceType::Discrete(self.vocab) }
195 }
196
197 fn render(&self) -> Vec<u8> {
198 Vec::new()
199 }
200
201 fn close(&mut self) {}
202
203 fn clone_state(&self) -> (i64, i64) {
204 (self.hidden, self.received)
205 }
206
207 fn restore_state(&mut self, state: &(i64, i64)) {
208 self.hidden = state.0;
209 self.received = state.1;
210 }
211}
212
213impl MultiAgentEnvironment for SignalingGame {
214 fn num_agents(&self) -> usize {
215 Self::NUM_AGENTS
216 }
217
218 fn get_agent_observation(&self, agent_id: usize) -> Vec<f32> {
219 match agent_id {
220 SPEAKER => self.speaker_obs(),
221 LISTENER => self.listener_obs(),
222 other => panic!("SignalingGame has 2 agents; no agent {other}"),
223 }
224 }
225
226 fn agent_action_space(&self, agent_id: usize) -> Vec<usize> {
227 match agent_id {
228 SPEAKER => Vec::new(),
230 LISTENER => vec![self.vocab],
232 other => panic!("SignalingGame has 2 agents; no agent {other}"),
233 }
234 }
235
236 fn step_multi(&mut self, actions: &[Vec<i64>]) -> MultiAgentResult {
237 assert_eq!(actions.len(), Self::NUM_AGENTS, "signaling game requires 2 action vectors");
238
239 let (_speaker_task, speaker_msg) =
242 split_action(&actions[SPEAKER], self.agent_action_space(SPEAKER).len());
243 let (listener_task, _listener_msg) =
245 split_action(&actions[LISTENER], self.agent_action_space(LISTENER).len());
246
247 debug_assert!(self.delivery().reaches(SPEAKER, LISTENER));
249 self.received = speaker_msg.first().copied().unwrap_or(-1);
250
251 let guess = listener_task.first().copied().unwrap_or(-1);
253 let correct = guess == self.hidden;
254 let reward = if correct { 1.0_f32 } else { 0.0 };
255
256 MultiAgentResult::new(
257 vec![self.speaker_obs(), self.listener_obs()],
258 vec![reward, reward],
259 vec![true, true],
261 vec![false, false],
262 )
263 }
264
265 fn active_agents(&self) -> Vec<bool> {
266 vec![true; Self::NUM_AGENTS]
267 }
268}
269
270impl JointEnv for SignalingGame {
276 fn reset_joint(&mut self, _seed: Option<u64>) -> Vec<Vec<f32>> {
277 self.reset();
280 (0..self.num_agents()).map(|i| self.get_agent_observation(i)).collect()
281 }
282
283 fn step_joint(&mut self, actions: &[Vec<i64>]) -> JointStepResult {
284 let result = self.step_multi(actions);
285 JointStepResult {
286 rewards: result.rewards,
287 done: result.terminated.iter().all(|&t| t),
289 observations: result.observations,
290 }
291 }
292}
293
294impl CommunicatingEnvironment for SignalingGame {
295 fn message_vocab(&self, agent_id: usize) -> Vec<usize> {
296 match agent_id {
297 SPEAKER => vec![self.vocab],
299 _ => Vec::new(),
301 }
302 }
303
304 fn message_obs_size(&self, agent_id: usize) -> usize {
305 match agent_id {
306 LISTENER => 1,
308 _ => 0,
310 }
311 }
312
313 fn delivery(&self) -> Delivery {
314 Delivery::Broadcast
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn comms_layout_matches_design() {
324 let env = SignalingGame::new(4);
325 assert_eq!(env.agent_action_space(SPEAKER), Vec::<usize>::new());
327 assert_eq!(env.message_vocab(SPEAKER), vec![4]);
328 assert_eq!(env.message_obs_size(SPEAKER), 0);
329 assert_eq!(env.agent_action_space(LISTENER), vec![4]);
331 assert_eq!(env.message_vocab(LISTENER), Vec::<usize>::new());
332 assert_eq!(env.message_obs_size(LISTENER), 1);
333 assert_eq!(env.delivery(), Delivery::Broadcast);
334 }
335
336 #[test]
337 fn speaker_observes_hidden_token_one_hot() {
338 let env = SignalingGame::with_hidden(5, 3);
339 let obs = env.get_agent_observation(SPEAKER);
340 assert_eq!(obs, vec![0.0, 0.0, 0.0, 1.0, 0.0]);
341 }
342
343 #[test]
344 fn message_round_trips_speaker_to_listener() {
345 let mut env = SignalingGame::with_hidden(4, 2);
348 let token = 2_i64;
349 let result = env.step_multi(&[vec![token], vec![0]]);
351
352 let listener_obs = &result.observations[LISTENER];
354 assert_eq!(listener_obs.len(), env.message_obs_size(LISTENER));
355 assert_eq!(listener_obs[0], token as f32);
356 assert_eq!(env.get_agent_observation(LISTENER)[0], token as f32);
358 assert_eq!(env.received(), token);
359 }
360
361 #[test]
362 fn reward_is_one_when_listener_guesses_hidden_token() {
363 let mut env = SignalingGame::with_hidden(3, 1);
364 let correct = env.step_multi(&[vec![1], vec![1]]);
366 assert_eq!(correct.rewards, vec![1.0, 1.0]);
367
368 let mut env = SignalingGame::with_hidden(3, 1);
370 let wrong = env.step_multi(&[vec![1], vec![0]]);
371 assert_eq!(wrong.rewards, vec![0.0, 0.0]);
372 }
373
374 #[test]
375 fn listener_obs_before_step_is_sentinel() {
376 let env = SignalingGame::new(3);
377 assert_eq!(env.get_agent_observation(LISTENER), vec![-1.0]);
379 }
380
381 #[test]
382 fn reset_clears_received_message() {
383 let mut env = SignalingGame::with_hidden(4, 0);
384 env.step_multi(&[vec![3], vec![0]]);
385 assert_eq!(env.received(), 3);
386 env.reset();
387 assert_eq!(env.received(), -1);
388 assert_eq!(env.get_agent_observation(LISTENER), vec![-1.0]);
389 }
390
391 #[test]
392 fn snapshot_restore_round_trips() {
393 let mut env = SignalingGame::with_hidden(4, 2);
394 env.step_multi(&[vec![3], vec![0]]);
395 let snap = env.clone_state();
396 env.step_multi(&[vec![1], vec![0]]);
397 assert_eq!(env.received(), 1);
398 env.restore_state(&snap);
399 assert_eq!(env.hidden(), 2);
400 assert_eq!(env.received(), 3);
401 }
402
403 #[test]
404 fn num_agents_and_active() {
405 let env = SignalingGame::default();
406 assert_eq!(env.num_agents(), 2);
407 assert_eq!(env.active_agents(), vec![true, true]);
408 }
409}