use crate::multi_agent::joint::{JointEnv, JointStepResult};
#[derive(Debug, Clone)]
pub struct MatchingPennies {
step: usize,
}
impl MatchingPennies {
pub const EPISODE_LEN: usize = 16;
pub const NUM_AGENTS: usize = 2;
pub const OBS_DIM: usize = 1;
pub const ACTION_DIM: usize = 2;
pub fn new() -> Self {
Self { step: 0 }
}
}
impl Default for MatchingPennies {
fn default() -> Self {
Self::new()
}
}
impl JointEnv for MatchingPennies {
fn reset_joint(&mut self, _seed: Option<u64>) -> Vec<Vec<f32>> {
self.step = 0;
vec![vec![0.0; Self::OBS_DIM]; Self::NUM_AGENTS]
}
fn step_joint(&mut self, actions: &[Vec<i64>]) -> JointStepResult {
debug_assert_eq!(actions.len(), Self::NUM_AGENTS, "matching pennies requires 2 agents");
debug_assert_eq!(actions[0].len(), 1, "agent 0 must supply a 1-d action");
debug_assert_eq!(actions[1].len(), 1, "agent 1 must supply a 1-d action");
let a0 = actions[0][0];
let a1 = actions[1][0];
let matcher_reward = if a0 == a1 { 1.0_f32 } else { -1.0_f32 };
let rewards = vec![matcher_reward, -matcher_reward];
self.step += 1;
let done = self.step >= Self::EPISODE_LEN;
JointStepResult {
rewards,
done,
observations: vec![vec![0.0; Self::OBS_DIM]; Self::NUM_AGENTS],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zero_sum_every_step() {
for a0 in 0..2 {
for a1 in 0..2 {
let mut env = MatchingPennies::new();
env.reset_joint(None);
let res = env.step_joint(&[vec![a0], vec![a1]]);
let total: f32 = res.rewards.iter().sum();
assert!(
total.abs() < 1e-6,
"rewards not zero-sum for ({a0}, {a1}): {:?}",
res.rewards
);
}
}
}
#[test]
fn test_matcher_wins_on_agreement() {
for a in 0..2 {
let mut env = MatchingPennies::new();
env.reset_joint(None);
let res = env.step_joint(&[vec![a], vec![a]]);
assert_eq!(res.rewards[0], 1.0, "matcher should win on agreement (action {a})");
assert_eq!(res.rewards[1], -1.0, "mismatcher should lose on agreement (action {a})");
}
}
#[test]
fn test_mismatcher_wins_on_disagreement() {
let pairs = [(0_i64, 1_i64), (1, 0)];
for (a0, a1) in pairs {
let mut env = MatchingPennies::new();
env.reset_joint(None);
let res = env.step_joint(&[vec![a0], vec![a1]]);
assert_eq!(res.rewards[0], -1.0, "matcher should lose on ({a0}, {a1})");
assert_eq!(res.rewards[1], 1.0, "mismatcher should win on ({a0}, {a1})");
}
}
#[test]
fn test_episode_done_after_episode_len() {
let mut env = MatchingPennies::new();
env.reset_joint(None);
for step in 0..MatchingPennies::EPISODE_LEN {
let res = env.step_joint(&[vec![0], vec![0]]);
let expected_done = step + 1 >= MatchingPennies::EPISODE_LEN;
assert_eq!(res.done, expected_done, "done flag wrong at step {step}");
}
}
#[test]
fn test_reset_restores_step_counter() {
let mut env = MatchingPennies::new();
env.reset_joint(None);
for _ in 0..MatchingPennies::EPISODE_LEN {
env.step_joint(&[vec![0], vec![0]]);
}
env.reset_joint(None);
for step in 0..MatchingPennies::EPISODE_LEN - 1 {
let res = env.step_joint(&[vec![0], vec![0]]);
assert!(!res.done, "should not be done at step {step} after reset");
}
let last = env.step_joint(&[vec![0], vec![0]]);
assert!(last.done, "should be done on final step after reset");
}
#[test]
fn test_observation_shape() {
let mut env = MatchingPennies::new();
let obs = env.reset_joint(None);
assert_eq!(obs.len(), MatchingPennies::NUM_AGENTS);
for o in &obs {
assert_eq!(o.len(), MatchingPennies::OBS_DIM);
}
let res = env.step_joint(&[vec![0], vec![0]]);
assert_eq!(res.observations.len(), MatchingPennies::NUM_AGENTS);
for o in &res.observations {
assert_eq!(o.len(), MatchingPennies::OBS_DIM);
}
}
}