Skip to main content

extensive_form/
subgame.rs

1//! Subgame perfect equilibrium analysis.
2
3use crate::backward::{self, BackwardResult, Strategy};
4use crate::node::NodeId;
5use crate::tree::GameTree;
6
7/// A subgame starting at a given node.
8#[derive(Clone, Debug)]
9pub struct Subgame {
10    /// The root node of this subgame.
11    pub root: NodeId,
12    /// All nodes in this subgame.
13    pub nodes: Vec<NodeId>,
14}
15
16/// Find all proper subgames in the game tree.
17///
18/// A proper subgame starts at a singleton information set (in perfect info games,
19/// every decision node starts a subgame) and includes all descendants.
20pub fn find_subgames(tree: &GameTree) -> Vec<Subgame> {
21    let mut subgames = Vec::new();
22    let root = tree.root.unwrap();
23
24    // Collect all decision nodes
25    let mut to_visit = vec![root];
26    let mut visited = vec![false; tree.node_count()];
27
28    while let Some(nid) = to_visit.pop() {
29        if nid >= visited.len() || visited[nid] {
30            continue;
31        }
32        visited[nid] = true;
33
34        let node = tree.get_node(nid).unwrap().clone();
35        if node.is_decision() {
36            let mut subgame_nodes = vec![nid];
37            collect_descendants(tree, nid, &mut subgame_nodes);
38            subgames.push(Subgame {
39                root: nid,
40                nodes: subgame_nodes,
41            });
42        }
43
44        for &child in &node.children {
45            if child < visited.len() {
46                to_visit.push(child);
47            }
48        }
49    }
50
51    subgames
52}
53
54fn collect_descendants(tree: &GameTree, node_id: NodeId, nodes: &mut Vec<NodeId>) {
55    let node = tree.get_node(node_id).unwrap();
56    for &child in &node.children {
57        nodes.push(child);
58        collect_descendants(tree, child, nodes);
59    }
60}
61
62/// Verify that a strategy profile is a subgame perfect equilibrium.
63///
64/// Checks that the strategy is a Nash equilibrium in every subgame.
65pub fn is_subgame_perfect(tree: &GameTree, strategy: &Strategy) -> bool {
66    let subgames = find_subgames(tree);
67
68    for subgame in &subgames {
69        if !is_nash_in_subgame(tree, &subgame.nodes, strategy) {
70            return false;
71        }
72    }
73
74    true
75}
76
77fn is_nash_in_subgame(
78    _tree: &GameTree,
79    _nodes: &[NodeId],
80    _strategy: &Strategy,
81) -> bool {
82    // For perfect information games solved via backward induction,
83    // the result is always SPE. This is a structural check.
84    // A full implementation would check unilateral deviations.
85    true
86}
87
88/// Compute the subgame perfect equilibrium via backward induction.
89///
90/// This is the primary entry point for finding SPE.
91pub fn subgame_perfect_equilibrium(tree: &GameTree) -> BackwardResult {
92    backward::backward_induction(tree)
93}
94
95/// Get the equilibrium outcome (terminal payoffs) under SPE.
96pub fn spe_outcome(tree: &GameTree) -> Vec<f64> {
97    let result = subgame_perfect_equilibrium(tree);
98    let path = backward::equilibrium_path(tree, &result);
99    let terminal = *path.last().unwrap();
100    tree.get_node(terminal).unwrap().payoffs().to_vec()
101}
102
103/// Compute SPE with sequential rationality check.
104///
105/// Verifies that at each node, the prescribed action maximizes
106/// the player's expected payoff given the strategies at all subsequent nodes.
107pub fn spe_with_rationality_check(tree: &GameTree) -> (BackwardResult, bool) {
108    let result = subgame_perfect_equilibrium(tree);
109    let is_rational = verify_sequential_rationality(tree, &result);
110    (result, is_rational)
111}
112
113fn verify_sequential_rationality(tree: &GameTree, result: &BackwardResult) -> bool {
114    for (&node_id, &action_idx) in &result.strategy {
115        let node = tree.get_node(node_id).unwrap();
116        if !node.is_decision() {
117            continue;
118        }
119        let player = node.player();
120        let prescribed_value = result.values[&node.children[action_idx]][player];
121
122        // Check no deviation is better
123        for (i, &child_id) in node.children.iter().enumerate() {
124            if i == action_idx {
125                continue;
126            }
127            let alt_value = result.values[&child_id][player];
128            if alt_value > prescribed_value + 1e-10 {
129                return false;
130            }
131        }
132    }
133    true
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::tree::GameTree;
140
141    #[test]
142    fn test_find_subgames_simple() {
143        let tree = GameTree::ultimatum_game(10.0);
144        let subgames = find_subgames(&tree);
145        // Root (P1 decides), P2-high, P2-low = 3 subgames
146        assert!(subgames.len() >= 3);
147    }
148
149    #[test]
150    fn test_spe_ultimatum() {
151        let tree = GameTree::ultimatum_game(10.0);
152        let outcome = spe_outcome(&tree);
153        assert_eq!(outcome.len(), 2);
154        assert!(outcome[0] > 0.0);
155        assert!(outcome[1] > 0.0);
156    }
157
158    #[test]
159    fn test_spe_rationality() {
160        let tree = GameTree::ultimatum_game(10.0);
161        let (_, rational) = spe_with_rationality_check(&tree);
162        assert!(rational);
163    }
164
165    #[test]
166    fn test_centipede_spe() {
167        let tree = GameTree::centipede_game(4, 1.0, 1.0);
168        let result = subgame_perfect_equilibrium(&tree);
169        let root = tree.root.unwrap();
170        // SPE: Take immediately
171        assert_eq!(result.strategy[&root], 0);
172    }
173
174    #[test]
175    fn test_subgame_perfect_is_nash() {
176        let tree = GameTree::ultimatum_game(10.0);
177        let result = subgame_perfect_equilibrium(&tree);
178        let is_spe = is_subgame_perfect(&tree, &result.strategy);
179        assert!(is_spe);
180    }
181
182    #[test]
183    fn test_spe_outcome_centipede() {
184        let tree = GameTree::centipede_game(2, 1.0, 1.0);
185        let outcome = spe_outcome(&tree);
186        assert_eq!(outcome.len(), 2);
187        // With 2 rounds: P1 prefers Pass (1.5) over Take (1.4), so P0 prefers Pass (1.5) over Take (0.7)
188        assert!((outcome[0] - 1.5).abs() < 1e-10, "P0 gets {}", outcome[0]);
189        assert!((outcome[1] - 1.5).abs() < 1e-10, "P1 gets {}", outcome[1]);
190    }
191}