Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## [16.7.0] - unreleased

### Added
- Implement `GameSubgameRep` (C++) and `Subgame` (Python), a first-class object representing a subgame,
exposing its root node, parent and child subgames, and the infosets constituting its difference
(the infosets belonging to the subgame but not to any of its child subgames).
On `Game`, add `subgames` in the reverse topological order produced by the detection algorithm,
`root_subgame`, `terminal_subgames`, and `subgame_root` (the smallest subgame containing a given infoset).
Builds on the subgame-root detection added in 16.6.0. (#585)

### Fixed
- Corrected resizing of row and column index labels in strategic form so pivoting works correctly. (#844)
- Corrected incorrect output of strategic game tables to .nfg files if strategies have previously been
Expand Down
15 changes: 14 additions & 1 deletion doc/pygambit.api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Representation of games
Infoset
Action
Strategy
Subgame


Creating, reading, and writing games
Expand Down Expand Up @@ -109,6 +110,10 @@ Information about the game
Game.infosets
Game.nodes
Game.contingencies
Game.subgames
Game.root_subgame
Game.terminal_subgames
Game.subgame_root

.. autosummary::
:toctree: api/
Expand Down Expand Up @@ -149,7 +154,15 @@ Information about the game
Node.player
Node.is_successor_of
Node.plays
Node.own_prior_action

.. autosummary::
:toctree: api/

Subgame.game
Subgame.root
Subgame.parent
Subgame.children
Subgame.difference

.. autosummary::

Expand Down
42 changes: 41 additions & 1 deletion src/games/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ using GamePlayer = GameObjectPtr<GamePlayerRep>;
class GameNodeRep;
using GameNode = GameObjectPtr<GameNodeRep>;

class GameSubgameRep;
using GameSubgame = GameObjectPtr<GameSubgameRep>;

class GameRep;
using Game = std::shared_ptr<GameRep>;

Expand Down Expand Up @@ -608,6 +611,34 @@ inline void ValidateDistribution(const Array<Number> &p_probs, const bool p_norm
}
}

class GameSubgameRep : public std::enable_shared_from_this<GameSubgameRep> {
friend class GameTreeRep;

bool m_valid{true};
GameRep *m_game;
GameNodeRep *m_root;
std::weak_ptr<GameSubgameRep> m_parent;
std::vector<std::shared_ptr<GameSubgameRep>> m_children;
std::vector<std::shared_ptr<GameInfosetRep>> m_subgameDifference;

public:
using SubgameCollection = ElementCollection<GameSubgame, GameSubgameRep>;
using InfosetCollection = ElementCollection<GameSubgame, GameInfosetRep>;

GameSubgameRep(GameRep *p_game, GameNodeRep *p_root) : m_game(p_game), m_root(p_root) {}
~GameSubgameRep() = default;

bool IsValid() const { return m_valid; }
void Invalidate() { m_valid = false; }

Game GetGame() const;
GameNode GetRoot() const { return m_root->shared_from_this(); }

GameSubgame GetParent() const;
SubgameCollection GetChildren() const;
InfosetCollection GetSubgameDifference() const;
};

enum class TraversalOrder { Preorder, Postorder };

class CartesianProductSpace {
Expand Down Expand Up @@ -940,7 +971,14 @@ 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(); }
virtual std::vector<GameSubgame> GetSubgames() const { throw UndefinedException(); }

/// Returns the subgame rooted at the root of the game, or null if the game has no subgames
virtual GameSubgame GetRootSubgame() const { throw UndefinedException(); }
/// Returns the root of the smallest subgame containing the information set
virtual GameNode GetSubgameRoot(const GameInfoset &) const { throw UndefinedException(); }
/// Returns the terminal subgames (subgames with no child subgames)
virtual std::vector<GameSubgame> GetTerminalSubgames() const { throw UndefinedException(); }

//@}

Expand Down Expand Up @@ -1252,6 +1290,8 @@ inline GamePlayerRep::Infosets GamePlayerRep::GetInfosets() const
return Infosets(std::const_pointer_cast<GamePlayerRep>(shared_from_this()), &m_infosets);
}

inline Game GameSubgameRep::GetGame() const { return m_game->shared_from_this(); }

//=======================================================================

/// Factory function to create new game tree
Expand Down
136 changes: 126 additions & 10 deletions src/games/gametree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include <set>
#include <stack>
#include <unordered_map>
#include <unordered_set>
#include <variant>

#include "gambit.h"
Expand Down Expand Up @@ -284,6 +285,28 @@ void GameTreeRep::Reveal(GameInfoset p_atInfoset, GamePlayer p_player)
InvalidateTreeOrdering();
}

//========================================================================
// class GameSubgameRep
//========================================================================

GameSubgame GameSubgameRep::GetParent() const
{
auto p = m_parent.lock();
return p;
}

GameSubgameRep::SubgameCollection GameSubgameRep::GetChildren() const
{
return SubgameCollection(std::const_pointer_cast<GameSubgameRep>(shared_from_this()),
&m_children);
}

GameSubgameRep::InfosetCollection GameSubgameRep::GetSubgameDifference() const
{
return InfosetCollection(std::const_pointer_cast<GameSubgameRep>(shared_from_this()),
&m_subgameDifference);
}

//========================================================================
// class GameNodeRep
//========================================================================
Expand Down Expand Up @@ -374,18 +397,16 @@ bool GameNodeRep::IsSuccessorOf(GameNode p_node) const

bool GameNodeRep::IsSubgameRoot() const
{
// 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();
}

auto *tree_game = static_cast<GameTreeRep *>(m_game);
if (tree_game->m_subgames.empty()) {
if (tree_game->m_subgameCache.empty()) {
tree_game->BuildSubgameRoots();
}

return contains(tree_game->m_subgames, const_cast<GameNodeRep *>(this));
return tree_game->m_subgameCache.count(const_cast<GameNodeRep *>(this)) > 0;
}

bool GameNodeRep::IsStrategyReachable() const
Expand Down Expand Up @@ -858,6 +879,30 @@ bool GameTreeRep::IsAbsentMinded(const GameInfoset &p_infoset) const
return contains(m_absentMindedInfosets, p_infoset.get());
}

GameSubgame GameTreeRep::GetRootSubgame() const
{
if (m_subgameCache.empty()) {
BuildSubgameRoots();
}
auto it = m_subgameCache.find(m_root.get());
return (it == m_subgameCache.end()) ? nullptr : it->second;
}

GameNode GameTreeRep::GetSubgameRoot(const GameInfoset &p_infoset) const
{
if (p_infoset->GetGame().get() != this) {
throw MismatchException();
}
if (m_subgameCache.empty()) {
BuildSubgameRoots();
}
auto *n = p_infoset->m_members.front().get();
while (m_subgameCache.find(n) == m_subgameCache.end()) {
n = n->m_parent;
}
return n->shared_from_this();
}

//------------------------------------------------------------------------
// GameTreeRep: Managing the representation
//------------------------------------------------------------------------
Expand Down Expand Up @@ -924,6 +969,10 @@ void GameTreeRep::ClearComputedValues() const
const_cast<GameTreeRep *>(this)->m_unreachableNodes = nullptr;
m_absentMindedInfosets.clear();
m_subgames.clear();
for (const auto &[node, subgame] : m_subgameCache) {
subgame->Invalidate();
}
m_subgameCache.clear();
m_computedValues = false;
}

Expand Down Expand Up @@ -1129,7 +1178,7 @@ void GameTreeRep::BuildUnreachableNodes() const

void GameTreeRep::BuildSubgameRoots() const
{
if (!m_subgames.empty()) {
if (!m_subgameCache.empty() || m_root->IsTerminal()) {
return;
}

Expand Down Expand Up @@ -1227,18 +1276,85 @@ void GameTreeRep::BuildSubgameRoots() const

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

// Phase 3: Build subgame tree with subgame differences
struct SubgameVisitor {
const std::unordered_set<GameNodeRep *> &m_roots;
std::unordered_map<GameNodeRep *, std::shared_ptr<GameSubgameRep>> &m_cache;
GameTreeRep *m_game;
// Subgame roots on the current DFS path, innermost at back
std::vector<GameNodeRep *> m_stack;
std::unordered_set<GameInfosetRep *> m_infosetVisited;

DFSCallbackResult OnEnter(const GameNode &p_node, int)
{
if (p_node->IsTerminal()) {
return DFSCallbackResult::Continue;
}
GameNodeRep *node = p_node.get();
if (contains(m_roots, node)) {
auto subgame = std::make_shared<GameSubgameRep>(m_game, node);
if (!m_stack.empty()) {
auto &parent_subgame = m_cache.at(m_stack.back());
subgame->m_parent = parent_subgame;
parent_subgame->m_children.push_back(subgame);
}
m_cache.emplace(node, std::move(subgame));
m_stack.push_back(node);
}
if (m_infosetVisited.insert(node->m_infoset).second) {
m_cache.at(m_stack.back())
->m_subgameDifference.emplace_back(node->m_infoset->shared_from_this());
}
return DFSCallbackResult::Continue;
}

DFSCallbackResult OnExit(const GameNode &p_node, int)
{
if (!m_stack.empty() && m_stack.back() == p_node.get()) {
m_stack.pop_back();
}
return DFSCallbackResult::Continue;
}

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

const std::unordered_set<GameNodeRep *> subgame_root_set(m_subgames.begin(), m_subgames.end());

SubgameVisitor subgame_visitor{subgame_root_set, m_subgameCache,
const_cast<GameTreeRep *>(this)};
WalkDFS(game, m_root, TraversalOrder::Preorder, subgame_visitor);
}

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

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

std::vector<GameSubgame> GameTreeRep::GetTerminalSubgames() const
{
if (m_subgameCache.empty()) {
BuildSubgameRoots();
}
std::vector<GameSubgame> result;
for (auto *rep : m_subgames) {
const auto &subgame = m_subgameCache.at(rep);
if (subgame->m_children.empty()) {
result.emplace_back(subgame);
}
}
return result;
}
Expand Down
7 changes: 6 additions & 1 deletion src/games/gametree.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#define GAMETREE_H

#include "gameexpl.h"
#include <unordered_map>

namespace Gambit {

Expand All @@ -48,6 +49,7 @@ class GameTreeRep final : public GameExplicitRep {
mutable std::unique_ptr<std::set<GameNodeRep *>> m_unreachableNodes;
mutable std::set<GameInfosetRep *> m_absentMindedInfosets;
mutable std::vector<GameNodeRep *> m_subgames;
mutable std::unordered_map<GameNodeRep *, std::shared_ptr<GameSubgameRep>> m_subgameCache;

/// @name Private auxiliary functions
//@{
Expand Down Expand Up @@ -100,7 +102,10 @@ 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;
std::vector<GameSubgame> GetSubgames() const override;
GameSubgame GetRootSubgame() const override;
GameNode GetSubgameRoot(const GameInfoset &) const override;
std::vector<GameSubgame> GetTerminalSubgames() const override;
//@}

/// @name Players
Expand Down
Loading
Loading