Skip to main content

extensive_form/
tree.rs

1//! Game tree construction and traversal.
2
3use crate::node::{NodeData, NodeId, Payoff, PlayerId};
4use std::collections::HashMap;
5
6/// A game tree representing an extensive-form game.
7#[derive(Clone, Debug)]
8pub struct GameTree {
9    /// Name of the game.
10    pub name: String,
11    /// All nodes indexed by their ID.
12    pub nodes: HashMap<NodeId, NodeData>,
13    /// The root node ID.
14    pub root: Option<NodeId>,
15    /// Next available node ID.
16    next_id: NodeId,
17    /// Number of players.
18    pub num_players: usize,
19}
20
21impl GameTree {
22    /// Create a new empty game tree.
23    pub fn new(name: &str) -> Self {
24        Self {
25            name: name.to_string(),
26            nodes: HashMap::new(),
27            root: None,
28            next_id: 0,
29            num_players: 2,
30        }
31    }
32
33    /// Create a game tree with a specified number of players.
34    pub fn with_players(name: &str, num_players: usize) -> Self {
35        Self {
36            name: name.to_string(),
37            nodes: HashMap::new(),
38            root: None,
39            next_id: 0,
40            num_players,
41        }
42    }
43
44    /// Allocate a new node ID.
45    fn alloc_id(&mut self) -> NodeId {
46        let id = self.next_id;
47        self.next_id += 1;
48        id
49    }
50
51    /// Add a decision node and return its ID.
52    pub fn add_decision(&mut self, player: PlayerId, label: &str) -> NodeId {
53        let id = self.alloc_id();
54        let node = NodeData::decision(id, player, label);
55        self.nodes.insert(id, node);
56        if self.root.is_none() {
57            self.root = Some(id);
58        }
59        id
60    }
61
62    /// Add a terminal node with payoffs.
63    pub fn add_terminal(&mut self, payoffs: Vec<Payoff>, label: &str) -> NodeId {
64        let id = self.alloc_id();
65        let node = NodeData::terminal(id, payoffs, label);
66        self.nodes.insert(id, node);
67        id
68    }
69
70    /// Add a chance node.
71    pub fn add_chance(&mut self, probabilities: Vec<f64>, label: &str) -> NodeId {
72        let id = self.alloc_id();
73        let node = NodeData::chance(id, probabilities, label);
74        self.nodes.insert(id, node);
75        if self.root.is_none() {
76            self.root = Some(id);
77        }
78        id
79    }
80
81    /// Add an action from parent to child with a label.
82    pub fn add_action(&mut self, parent: NodeId, action: &str, child: NodeId) {
83        if let Some(node) = self.nodes.get_mut(&parent) {
84            node.actions.push(action.to_string());
85            node.children.push(child);
86        }
87        if let Some(child_node) = self.nodes.get_mut(&child) {
88            child_node.parent = Some(parent);
89            child_node.incoming_action = Some(action.to_string());
90        }
91    }
92
93    /// Get a node by ID.
94    pub fn get_node(&self, id: NodeId) -> Option<&NodeData> {
95        self.nodes.get(&id)
96    }
97
98    /// Get a mutable node by ID.
99    pub fn get_node_mut(&mut self, id: NodeId) -> Option<&mut NodeData> {
100        self.nodes.get_mut(&id)
101    }
102
103    /// Get all terminal nodes.
104    pub fn terminals(&self) -> Vec<&NodeData> {
105        self.nodes.values().filter(|n| n.is_terminal()).collect()
106    }
107
108    /// Get the depth of a node (distance from root).
109    pub fn depth(&self, id: NodeId) -> usize {
110        let mut depth = 0;
111        let mut current = id;
112        while let Some(node) = self.nodes.get(&current) {
113            match node.parent {
114                Some(p) => {
115                    depth += 1;
116                    current = p;
117                }
118                None => break,
119            }
120        }
121        depth
122    }
123
124    /// Get the path from root to a given node.
125    pub fn path_to(&self, id: NodeId) -> Vec<NodeId> {
126        let mut path = vec![id];
127        let mut current = id;
128        while let Some(node) = self.nodes.get(&current) {
129            match node.parent {
130                Some(p) => {
131                    path.push(p);
132                    current = p;
133                }
134                None => break,
135            }
136        }
137        path.reverse();
138        path
139    }
140
141    /// Count total nodes.
142    pub fn node_count(&self) -> usize {
143        self.nodes.len()
144    }
145
146    /// Build an Ultimatum game: P1 proposes split, P2 accepts/rejects.
147    pub fn ultimatum_game(total: Payoff) -> Self {
148        let mut tree = Self::new("Ultimatum Game");
149        tree.num_players = 2;
150
151        // P1 chooses how much to offer: Low or High
152        let root = tree.add_decision(0, "P1 Proposes");
153        let high = total * 0.8;
154        let low = total * 0.2;
155
156        // P2 responses for high offer
157        let p2_high = tree.add_decision(1, "P2 Responds to High");
158        let t_ha = tree.add_terminal(vec![total - high, high], "High Accepted");
159        let t_hr = tree.add_terminal(vec![0.0, 0.0], "High Rejected");
160
161        tree.add_action(p2_high, "Accept", t_ha);
162        tree.add_action(p2_high, "Reject", t_hr);
163
164        // P2 responses for low offer
165        let p2_low = tree.add_decision(1, "P2 Responds to Low");
166        let t_la = tree.add_terminal(vec![total - low, low], "Low Accepted");
167        let t_lr = tree.add_terminal(vec![0.0, 0.0], "Low Rejected");
168
169        tree.add_action(p2_low, "Accept", t_la);
170        tree.add_action(p2_low, "Reject", t_lr);
171
172        tree.add_action(root, "High Offer", p2_high);
173        tree.add_action(root, "Low Offer", p2_low);
174
175        tree
176    }
177
178    /// Build a Centipede game with `n` rounds.
179    pub fn centipede_game(n: usize, pot: Payoff, growth: Payoff) -> Self {
180        let mut tree = Self::new("Centipede Game");
181        tree.num_players = 2;
182
183        let root = tree.add_decision(0, "P1 Turn 1");
184        Self::build_centipede(&mut tree, root, n, pot, growth, 0);
185        tree
186    }
187
188    fn build_centipede(
189        tree: &mut Self,
190        current: NodeId,
191        rounds_left: usize,
192        pot: Payoff,
193        growth: Payoff,
194        round: usize,
195    ) {
196        let player = round % 2;
197        let take_share = if player == 0 { 0.7 } else { 0.3 };
198        let pass_pot = pot + growth;
199
200        // Take action
201        let t_take = tree.add_terminal(
202            vec![pot * take_share, pot * (1.0 - take_share)],
203            &format!("P{} Takes", player),
204        );
205        tree.add_action(current, "Take", t_take);
206
207        // Pass action
208        if rounds_left > 1 {
209            let next = tree.add_decision(1 - player, &format!("P{} Turn {}", 1 - player, round + 2));
210            tree.add_action(current, "Pass", next);
211            Self::build_centipede(tree, next, rounds_left - 1, pass_pot, growth, round + 1);
212        } else {
213            // Final round: forced split
214            let t_end = tree.add_terminal(
215                vec![pass_pot * 0.5, pass_pot * 0.5],
216                "Game Ends Cooperatively",
217            );
218            tree.add_action(current, "Pass", t_end);
219        }
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_empty_tree() {
229        let tree = GameTree::new("Empty");
230        assert_eq!(tree.node_count(), 0);
231        assert!(tree.root.is_none());
232    }
233
234    #[test]
235    fn test_simple_tree() {
236        let mut tree = GameTree::new("Simple");
237        let root = tree.add_decision(0, "Root");
238        let left = tree.add_terminal(vec![1.0, 2.0], "Left");
239        let right = tree.add_terminal(vec![3.0, 0.0], "Right");
240        tree.add_action(root, "L", left);
241        tree.add_action(root, "R", right);
242        assert_eq!(tree.node_count(), 3);
243        assert_eq!(tree.root, Some(root));
244    }
245
246    #[test]
247    fn test_ultimatum_game() {
248        let tree = GameTree::ultimatum_game(10.0);
249        assert_eq!(tree.node_count(), 7);
250        assert_eq!(tree.terminals().len(), 4);
251    }
252
253    #[test]
254    fn test_centipede_game() {
255        let tree = GameTree::centipede_game(3, 1.0, 1.0);
256        assert!(tree.node_count() > 3);
257        assert!(tree.terminals().len() >= 3);
258    }
259
260    #[test]
261    fn test_depth() {
262        let mut tree = GameTree::new("Depth");
263        let root = tree.add_decision(0, "Root");
264        let child = tree.add_terminal(vec![1.0], "Child");
265        tree.add_action(root, "Go", child);
266        assert_eq!(tree.depth(root), 0);
267        assert_eq!(tree.depth(child), 1);
268    }
269
270    #[test]
271    fn test_path_to() {
272        let mut tree = GameTree::new("Path");
273        let root = tree.add_decision(0, "Root");
274        let mid = tree.add_decision(1, "Mid");
275        let leaf = tree.add_terminal(vec![1.0, 2.0], "Leaf");
276        tree.add_action(root, "Go", mid);
277        tree.add_action(mid, "Go", leaf);
278        let path = tree.path_to(leaf);
279        assert_eq!(path.len(), 3);
280    }
281}