use crate::node::{NodeData, NodeId, Payoff, PlayerId};
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct GameTree {
pub name: String,
pub nodes: HashMap<NodeId, NodeData>,
pub root: Option<NodeId>,
next_id: NodeId,
pub num_players: usize,
}
impl GameTree {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
nodes: HashMap::new(),
root: None,
next_id: 0,
num_players: 2,
}
}
pub fn with_players(name: &str, num_players: usize) -> Self {
Self {
name: name.to_string(),
nodes: HashMap::new(),
root: None,
next_id: 0,
num_players,
}
}
fn alloc_id(&mut self) -> NodeId {
let id = self.next_id;
self.next_id += 1;
id
}
pub fn add_decision(&mut self, player: PlayerId, label: &str) -> NodeId {
let id = self.alloc_id();
let node = NodeData::decision(id, player, label);
self.nodes.insert(id, node);
if self.root.is_none() {
self.root = Some(id);
}
id
}
pub fn add_terminal(&mut self, payoffs: Vec<Payoff>, label: &str) -> NodeId {
let id = self.alloc_id();
let node = NodeData::terminal(id, payoffs, label);
self.nodes.insert(id, node);
id
}
pub fn add_chance(&mut self, probabilities: Vec<f64>, label: &str) -> NodeId {
let id = self.alloc_id();
let node = NodeData::chance(id, probabilities, label);
self.nodes.insert(id, node);
if self.root.is_none() {
self.root = Some(id);
}
id
}
pub fn add_action(&mut self, parent: NodeId, action: &str, child: NodeId) {
if let Some(node) = self.nodes.get_mut(&parent) {
node.actions.push(action.to_string());
node.children.push(child);
}
if let Some(child_node) = self.nodes.get_mut(&child) {
child_node.parent = Some(parent);
child_node.incoming_action = Some(action.to_string());
}
}
pub fn get_node(&self, id: NodeId) -> Option<&NodeData> {
self.nodes.get(&id)
}
pub fn get_node_mut(&mut self, id: NodeId) -> Option<&mut NodeData> {
self.nodes.get_mut(&id)
}
pub fn terminals(&self) -> Vec<&NodeData> {
self.nodes.values().filter(|n| n.is_terminal()).collect()
}
pub fn depth(&self, id: NodeId) -> usize {
let mut depth = 0;
let mut current = id;
while let Some(node) = self.nodes.get(¤t) {
match node.parent {
Some(p) => {
depth += 1;
current = p;
}
None => break,
}
}
depth
}
pub fn path_to(&self, id: NodeId) -> Vec<NodeId> {
let mut path = vec![id];
let mut current = id;
while let Some(node) = self.nodes.get(¤t) {
match node.parent {
Some(p) => {
path.push(p);
current = p;
}
None => break,
}
}
path.reverse();
path
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn ultimatum_game(total: Payoff) -> Self {
let mut tree = Self::new("Ultimatum Game");
tree.num_players = 2;
let root = tree.add_decision(0, "P1 Proposes");
let high = total * 0.8;
let low = total * 0.2;
let p2_high = tree.add_decision(1, "P2 Responds to High");
let t_ha = tree.add_terminal(vec![total - high, high], "High Accepted");
let t_hr = tree.add_terminal(vec![0.0, 0.0], "High Rejected");
tree.add_action(p2_high, "Accept", t_ha);
tree.add_action(p2_high, "Reject", t_hr);
let p2_low = tree.add_decision(1, "P2 Responds to Low");
let t_la = tree.add_terminal(vec![total - low, low], "Low Accepted");
let t_lr = tree.add_terminal(vec![0.0, 0.0], "Low Rejected");
tree.add_action(p2_low, "Accept", t_la);
tree.add_action(p2_low, "Reject", t_lr);
tree.add_action(root, "High Offer", p2_high);
tree.add_action(root, "Low Offer", p2_low);
tree
}
pub fn centipede_game(n: usize, pot: Payoff, growth: Payoff) -> Self {
let mut tree = Self::new("Centipede Game");
tree.num_players = 2;
let root = tree.add_decision(0, "P1 Turn 1");
Self::build_centipede(&mut tree, root, n, pot, growth, 0);
tree
}
fn build_centipede(
tree: &mut Self,
current: NodeId,
rounds_left: usize,
pot: Payoff,
growth: Payoff,
round: usize,
) {
let player = round % 2;
let take_share = if player == 0 { 0.7 } else { 0.3 };
let pass_pot = pot + growth;
let t_take = tree.add_terminal(
vec![pot * take_share, pot * (1.0 - take_share)],
&format!("P{} Takes", player),
);
tree.add_action(current, "Take", t_take);
if rounds_left > 1 {
let next = tree.add_decision(1 - player, &format!("P{} Turn {}", 1 - player, round + 2));
tree.add_action(current, "Pass", next);
Self::build_centipede(tree, next, rounds_left - 1, pass_pot, growth, round + 1);
} else {
let t_end = tree.add_terminal(
vec![pass_pot * 0.5, pass_pot * 0.5],
"Game Ends Cooperatively",
);
tree.add_action(current, "Pass", t_end);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_tree() {
let tree = GameTree::new("Empty");
assert_eq!(tree.node_count(), 0);
assert!(tree.root.is_none());
}
#[test]
fn test_simple_tree() {
let mut tree = GameTree::new("Simple");
let root = tree.add_decision(0, "Root");
let left = tree.add_terminal(vec![1.0, 2.0], "Left");
let right = tree.add_terminal(vec![3.0, 0.0], "Right");
tree.add_action(root, "L", left);
tree.add_action(root, "R", right);
assert_eq!(tree.node_count(), 3);
assert_eq!(tree.root, Some(root));
}
#[test]
fn test_ultimatum_game() {
let tree = GameTree::ultimatum_game(10.0);
assert_eq!(tree.node_count(), 7);
assert_eq!(tree.terminals().len(), 4);
}
#[test]
fn test_centipede_game() {
let tree = GameTree::centipede_game(3, 1.0, 1.0);
assert!(tree.node_count() > 3);
assert!(tree.terminals().len() >= 3);
}
#[test]
fn test_depth() {
let mut tree = GameTree::new("Depth");
let root = tree.add_decision(0, "Root");
let child = tree.add_terminal(vec![1.0], "Child");
tree.add_action(root, "Go", child);
assert_eq!(tree.depth(root), 0);
assert_eq!(tree.depth(child), 1);
}
#[test]
fn test_path_to() {
let mut tree = GameTree::new("Path");
let root = tree.add_decision(0, "Root");
let mid = tree.add_decision(1, "Mid");
let leaf = tree.add_terminal(vec![1.0, 2.0], "Leaf");
tree.add_action(root, "Go", mid);
tree.add_action(mid, "Go", leaf);
let path = tree.path_to(leaf);
assert_eq!(path.len(), 3);
}
}