Skip to content
Merged
8 changes: 7 additions & 1 deletion ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
### Changed
- `Game.comment` has been renamed to `Game.description`

### Added
- Implement linear-time algorithm to find all root nodes of proper subgames, using an adaptation of
Tarjan's (1974) algorithm for finding bridges in an undirected graph. Subgame roots are cached so
subsequent lookup is constant-time (if the game is unchanged). (#584)

### Fixed
- `enumpoly` would take a very long time on some supports where an equilibrium is located on the
boundary of the projected game. Search is now restricted to the interior of the space ruling
these out; these will always be found by another projection. (#756)
- In the graphical interface, the logit correspondence display would fail and terminate the program
on very small (<10^{-300}) probabilities.
- The new subgame root computation fixes a bug which failed to detect subgames where the subgame
root node is a member of an absent-minded infoset. (#584)

## [16.5.1] - unreleased

### Fixed
- `Game.reveal` raised a null pointer access exception or dumped core in some cases (#749)


## [16.5.0] - 2026-01-05

### Fixed
Expand Down
3 changes: 3 additions & 0 deletions src/games/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,9 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
}
return false;
}
/// Returns a list of all subgame roots in the game
virtual std::vector<GameNode> GetSubgames() const { throw UndefinedException(); }

//@}

/// @name Writing data files
Expand Down
153 changes: 129 additions & 24 deletions src/games/gametree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
//

#include <iostream>
#include <algorithm>
#include <functional>
#include <iostream>
#include <limits>
#include <numeric>
#include <stack>
#include <set>
#include <stack>
#include <unordered_map>
#include <variant>

#include "gambit.h"
Expand Down Expand Up @@ -373,32 +375,18 @@ bool GameNodeRep::IsSuccessorOf(GameNode p_node) const

bool GameNodeRep::IsSubgameRoot() const
{
// First take care of a couple easy cases
if (m_children.empty() || m_infoset->m_members.size() > 1) {
return false;
}
if (!m_parent) {
return true;
// TODO: Currently O(S) per call where S = number of subgames.
// Will become O(1) when GameSubgameRep adds a back-pointer (like m_infoset).
if (m_children.empty()) {
return !GetParent();
}

// A node is a subgame root if and only if in every information set,
// either all members succeed the node in the tree,
// or all members do not succeed the node in the tree.
for (auto player : m_game->GetPlayers()) {
for (auto infoset : player->GetInfosets()) {
const bool precedes = infoset->m_members.front()->IsSuccessorOf(
std::const_pointer_cast<GameNodeRep>(shared_from_this()));
if (std::any_of(std::next(infoset->m_members.begin()), infoset->m_members.end(),
[this, precedes](const std::shared_ptr<GameNodeRep> &m) {
return m->IsSuccessorOf(std::const_pointer_cast<GameNodeRep>(
shared_from_this())) != precedes;
})) {
return false;
}
}
auto *tree_game = static_cast<GameTreeRep *>(m_game);
if (tree_game->m_subgames.empty()) {
tree_game->BuildSubgameRoots();
}

return true;
return contains(tree_game->m_subgames, const_cast<GameNodeRep *>(this));
}

bool GameNodeRep::IsStrategyReachable() const
Expand Down Expand Up @@ -936,6 +924,7 @@ void GameTreeRep::ClearComputedValues() const
m_ownPriorActionInfo = nullptr;
const_cast<GameTreeRep *>(this)->m_unreachableNodes = nullptr;
m_absentMindedInfosets.clear();
m_subgames.clear();
m_computedValues = false;
}

Expand Down Expand Up @@ -1139,6 +1128,122 @@ void GameTreeRep::BuildUnreachableNodes() const
}
}

void GameTreeRep::BuildSubgameRoots() const
{
if (!m_subgames.empty()) {
return;
}

struct Range {
int m_min = std::numeric_limits<int>::max();
int m_max = 0;

void Merge(const Range &p_source)
{
m_min = std::min(m_min, p_source.m_min);
m_max = std::max(m_max, p_source.m_max);
}

bool operator==(const Range &p_other) const
{
return m_min == p_other.m_min && m_max == p_other.m_max;
}
};

std::unordered_map<GameNodeRep *, Range> disc;
std::unordered_map<GameInfosetRep *, Range> hull;

// Phase 1: Compute subtree spans and infoset hulls
struct SpanVisitor {
std::unordered_map<GameNodeRep *, Range> &m_disc;
std::unordered_map<GameInfosetRep *, Range> &m_hull;
int m_counter = 0;

static DFSCallbackResult OnEnter(GameNode, int) { return DFSCallbackResult::Continue; }
static DFSCallbackResult OnAction(GameNode, GameNode, int)
{
return DFSCallbackResult::Continue;
}
static void OnVisit(GameNode, int) {}

DFSCallbackResult OnExit(const GameNode &p_node, int)
{
GameNodeRep *node = p_node.get();
if (p_node->IsTerminal()) {
m_counter++;
m_disc[node] = {m_counter, m_counter};
}
else {
Range &node_disc = m_disc[node];
const auto &children = p_node->GetChildren();
node_disc.m_min = m_disc.at(children.front().get()).m_min;
node_disc.m_max = m_disc.at(children.back().get()).m_max;
m_hull[node->m_infoset].Merge(node_disc);
}
return DFSCallbackResult::Continue;
}
};

// Phase 2: Reachability and detection
struct BridgeVisitor {
const std::unordered_map<GameNodeRep *, Range> &m_disc;
const std::unordered_map<GameInfosetRep *, Range> &m_hull;
std::vector<GameNodeRep *> &m_subgames;
std::unordered_map<GameNodeRep *, Range> m_low;

static DFSCallbackResult OnEnter(GameNode, int) { return DFSCallbackResult::Continue; }
static DFSCallbackResult OnAction(GameNode, GameNode, int)
{
return DFSCallbackResult::Continue;
}
static void OnVisit(GameNode, int) {}

DFSCallbackResult OnExit(const GameNode &p_node, int)
{
GameNodeRep *node = p_node.get();
if (p_node->IsTerminal()) {
m_low[node] = m_disc.at(node);
return DFSCallbackResult::Continue;
}

Range &low = m_low[node];
low = m_hull.at(node->m_infoset);

for (const auto &child : p_node->GetChildren()) {
low.Merge(m_low.at(child.get()));
}

if (low == m_disc.at(node)) {
m_subgames.push_back(node);
}

return DFSCallbackResult::Continue;
}
};

auto game = std::const_pointer_cast<GameRep>(shared_from_this());

SpanVisitor span_visitor{disc, hull};
WalkDFS(game, m_root, TraversalOrder::Postorder, span_visitor);

BridgeVisitor bridge_visitor{disc, hull, m_subgames};
WalkDFS(game, m_root, TraversalOrder::Postorder, bridge_visitor);
}

std::vector<GameNode> GameTreeRep::GetSubgames() const
{
if (m_subgames.empty()) {
BuildSubgameRoots();
}

std::vector<GameNode> result;
result.reserve(m_subgames.size());
for (auto *rep : m_subgames) {
result.emplace_back(rep->shared_from_this());
}
return result;
}

//------------------------------------------------------------------------
// GameTreeRep: Writing data files
//------------------------------------------------------------------------
Expand Down
3 changes: 3 additions & 0 deletions src/games/gametree.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class GameTreeRep final : public GameExplicitRep {
mutable std::shared_ptr<OwnPriorActionInfo> m_ownPriorActionInfo;
mutable std::unique_ptr<std::set<GameNodeRep *>> m_unreachableNodes;
mutable std::set<GameInfosetRep *> m_absentMindedInfosets;
mutable std::vector<GameNodeRep *> m_subgames;

/// @name Private auxiliary functions
//@{
Expand Down Expand Up @@ -98,6 +99,7 @@ class GameTreeRep final : public GameExplicitRep {
/// Returns the largest payoff to the player in any play of the game
Rational GetPlayerMaxPayoff(const GamePlayer &) const override;
bool IsAbsentMinded(const GameInfoset &p_infoset) const override;
std::vector<GameNode> GetSubgames() const override;
//@}

/// @name Players
Expand Down Expand Up @@ -182,6 +184,7 @@ class GameTreeRep final : public GameExplicitRep {
std::vector<GameNodeRep *> BuildConsistentPlaysRecursiveImpl(GameNodeRep *node);
void BuildOwnPriorActions() const;
void BuildUnreachableNodes() const;
void BuildSubgameRoots() const;
};

template <class T> class TreeMixedStrategyProfileRep : public MixedStrategyProfileRep<T> {
Expand Down
14 changes: 14 additions & 0 deletions tests/test_games/AM-subgames.efg
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
EFG 2 R "Untitled Extensive Game" { "Player 1" "Player 2" }
""

p "" 1 1 "" { "1" "2" } 0
p "" 1 1 "" { "1" "2" } 0
p "" 2 1 "" { "1" "2" } 0
t "" 1 "Outcome 1" { 1, -1 }
t "" 2 "Outcome 2" { 2, -2 }
p "" 2 3 "" { "1" "2" } 0
t "" 3 "Outcome 3" { 3, -3 }
t "" 4 "Outcome 4" { 4, -4 }
p "" 2 2 "" { "1" "2" } 0
t "" 5 "Outcome 5" { 5, -5 }
t "" 6 "Outcome 6" { 6, -6 }
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
EFG 2 R "Untitled Extensive Game" { "Player 1" "Player 2" }
""

p "" 1 1 "" { "1" "1" } 0
t "" 1 "Outcome 1" { 1, -1 }
p "" 2 1 "" { "1" "1" "1" "1" "1" } 0
p "" 1 2 "" { "1" "1" } 0
t "" 2 "Outcome 2" { 2, -2 }
p "" 1 3 "" { "1" "1" } 0
t "" 3 "Outcome 3" { 3, -3 }
t "" 4 "Outcome 4" { 4, -4 }
p "" 1 3 "" { "1" "1" } 0
p "" 1 2 "" { "1" "1" } 0
t "" 5 "Outcome 5" { 5, -5 }
t "" 6 "Outcome 6" { 6, -6 }
t "" 7 "Outcome 7" { 7, -7 }
p "" 2 2 "" { "1" "1" } 0
t "" 8 "Outcome 8" { 8, -8 }
p "" 1 4 "" { "1" "1" } 0
p "" 2 3 "" { "1" "1" } 0
t "" 9 "Outcome 9" { 9, -9 }
t "" 10 "Outcome 10" { 10, -10 }
p "" 2 4 "" { "1" "1" } 0
t "" 11 "Outcome 11" { 11, -11 }
p "" 1 5 "" { "1" "1" } 0
p "" 1 6 "" { "1" "1" } 0
p "" 2 5 "" { "1" "1" } 0
t "" 12 "Outcome 12" { 12, -12 }
t "" 13 "Outcome 13" { 13, -13 }
t "" 14 "Outcome 14" { 14, -14 }
p "" 1 6 "" { "1" "1" } 0
p "" 2 6 "" { "1" "1" } 0
c "" 1 "" { "1" 1/2 "1" 1/2 } 0
p "" 2 5 "" { "1" "1" } 0
p "" 2 3 "" { "1" "1" } 0
t "" 15 "Outcome 15" { 15, -15 }
t "" 16 "Outcome 16" { 16, -16 }
t "" 17 "Outcome 17" { 17, -17 }
p "" 2 5 "" { "1" "1" } 0
t "" 18 "Outcome 18" { 18, -18 }
t "" 19 "Outcome 19" { 19, -19 }
t "" 20 "Outcome 20" { 20, -20 }
p "" 2 6 "" { "1" "1" } 0
p "" 2 7 "" { "1" "1" } 0
t "" 21 "Outcome 21" { 21, -21 }
t "" 22 "Outcome 22" { 22, -22 }
p "" 2 7 "" { "1" "1" } 0
t "" 23 "Outcome 23" { 23, -23 }
t "" 24 "Outcome 24" { 24, -24 }
p "" 1 7 "" { "1" "1" } 0
t "" 25 "Outcome 25" { 25, -25 }
t "" 26 "Outcome 26" { 26, -26 }
p "" 1 7 "" { "1" "1" } 0
t "" 27 "Outcome 27" { 27, -27 }
t "" 28 "Outcome 28" { 28, -28 }
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
EFG 2 R "Untitled Extensive Game" { "Player 1" "Player 2" }
""

p "" 2 1 "" { "1" "2" } 0
p "" 1 1 "" { "1" "2" } 0
t "" 1 "Outcome 1" { 1, -1 }
t "" 2 "Outcome 2" { 2, -2 }
p "" 1 2 "" { "1" "2" } 0
p "" 2 2 "" { "1" "2" } 0
t "" 3 "Outcome 3" { 3, -3 }
p "" 1 3 "" { "1" "2" } 0
p "" 2 3 "" { "1" "1" } 0
t "" 4 "Outcome 4" { 4, -4 }
p "" 2 4 "" { "1" "1" } 0
t "" 20 "Outcome 20" { 20, -20 }
t "" 21 "Outcome 21" { 21, -21 }
p "" 2 4 "" { "1" "1" } 0
p "" 2 3 "" { "1" "1" } 0
t "" 5 "Outcome 5" { 5, -5 }
t "" 22 "Outcome 22" { 22, -22 }
t "" 23 "Outcome 23" { 23, -23 }
p "" 2 2 "" { "1" "2" } 0
p "" 2 5 "" { "1" "2" } 0
p "" 1 4 "" { "1" "2" } 0
p "" 2 6 "" { "1" "2" } 0
t "" 6 "Outcome 6" { 6, -6 }
t "" 7 "Outcome 7" { 7, -7 }
t "" 8 "Outcome 8" { 8, -8 }
p "" 1 5 "" { "1" "2" } 0
p "" 2 7 "" { "1" "2" } 0
t "" 9 "Outcome 9" { 9, -9 }
t "" 10 "Outcome 10" { 10, -10 }
p "" 2 7 "" { "1" "2" } 0
p "" 1 6 "" { "1" "2" } 0
p "" 2 8 "" { "1" "2" } 0
p "" 1 4 "" { "1" "2" } 0
t "" 11 "Outcome 11" { 11, -11 }
t "" 12 "Outcome 12" { 12, -12 }
p "" 1 4 "" { "1" "2" } 0
t "" 13 "Outcome 13" { 13, -13 }
t "" 14 "Outcome 14" { 14, -14 }
t "" 15 "Outcome 15" { 15, -15 }
p "" 1 6 "" { "1" "2" } 0
t "" 16 "Outcome 16" { 16, -16 }
t "" 17 "Outcome 17" { 17, -17 }
p "" 1 7 "" { "1" "2" } 0
t "" 18 "Outcome 18" { 18, -18 }
t "" 19 "Outcome 19" { 19, -19 }
Loading
Loading