Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [16.7.0] - unreleased

### Added
- Implement `GameSubgameRep` (C++) and `Subgame` (Python), a first-class object representing a subgame. (#585)
- Games can be materialised directly from OpenSpiel games if `pyspiel` is installed. (#917)

### Fixed
Expand Down
11 changes: 11 additions & 0 deletions 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,8 @@ Information about the game
Game.infosets
Game.nodes
Game.contingencies
Game.subgames
Game.minimal_subgame

.. autosummary::
:toctree: api/
Expand Down Expand Up @@ -151,6 +154,14 @@ Information about the game
Node.plays
Node.own_prior_action

.. autosummary::
:toctree: api/

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

.. autosummary::

:toctree: api/
Expand Down
37 changes: 36 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,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(); }
virtual std::vector<GameSubgame> GetSubgames() const { throw UndefinedException(); }
/// Returns the smallest subgame containing the information set
virtual GameSubgame GetMinimalSubgame(const GameInfoset &) const { throw UndefinedException(); }

//@}

Expand Down Expand Up @@ -1252,6 +1285,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
127 changes: 108 additions & 19 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,13 @@ 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()) {
tree_game->BuildSubgameRoots();
}

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

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

GameSubgame GameTreeRep::GetMinimalSubgame(const GameInfoset &p_infoset) const
{
if (p_infoset->GetGame().get() != this) {
throw MismatchException();
}
BuildSubgameRoots();
auto *n = p_infoset->m_members.front().get();
auto it = m_subgameData.m_subgameByRoot.find(n);
while (it == m_subgameData.m_subgameByRoot.end()) {
n = n->m_parent;
it = m_subgameData.m_subgameByRoot.find(n);
}
return it->second;
}

//------------------------------------------------------------------------
// GameTreeRep: Managing the representation
//------------------------------------------------------------------------
Expand Down Expand Up @@ -923,7 +956,7 @@ void GameTreeRep::ClearComputedValues() const
m_ownPriorActionInfo = nullptr;
const_cast<GameTreeRep *>(this)->m_unreachableNodes = nullptr;
m_absentMindedInfosets.clear();
m_subgames.clear();
m_subgameData.Invalidate();
m_computedValues = false;
}

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

void GameTreeRep::BuildSubgameRoots() const
{
if (!m_subgames.empty()) {
if (m_subgameData.m_valid) {
return;
}
if (m_root->IsTerminal()) {
m_subgameData.m_valid = true;
return;
}

Expand Down Expand Up @@ -1242,20 +1279,72 @@ void GameTreeRep::BuildSubgameRoots() const
SpanVisitor span_visitor{disc, hull};
WalkDFS(game, m_root, TraversalOrder::Postorder, span_visitor);

BridgeVisitor bridge_visitor{disc, hull, m_subgames};
BridgeVisitor bridge_visitor{disc, hull, m_subgameData.m_subgamePostorder};
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_subgameData.m_subgamePostorder.begin(), m_subgameData.m_subgamePostorder.end());

SubgameVisitor subgame_visitor{subgame_root_set, m_subgameData.m_subgameByRoot,
const_cast<GameTreeRep *>(this)};
WalkDFS(game, m_root, TraversalOrder::Preorder, subgame_visitor);
m_subgameData.m_valid = true;
}

std::vector<GameNode> GameTreeRep::GetSubgames() const
std::vector<GameSubgame> 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());
BuildSubgameRoots();
std::vector<GameSubgame> result;
result.reserve(m_subgameData.m_subgamePostorder.size());
for (auto *rep : m_subgameData.m_subgamePostorder) {
result.emplace_back(m_subgameData.m_subgameByRoot.at(rep));
}
return result;
}
Expand Down
25 changes: 23 additions & 2 deletions 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 @@ -47,7 +48,25 @@ 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;
// The subgames of the game, held in two synchronized forms:
// m_subgamePostorder for iteration (children before parents),
// m_subgameByRoot for O(1) lookup by root node and ownership of the GameSubgameRep objects.
struct SubgameData {
std::vector<GameNodeRep *> m_subgamePostorder;
std::unordered_map<GameNodeRep *, std::shared_ptr<GameSubgameRep>> m_subgameByRoot;
bool m_valid{false};

void Invalidate()
{
for (const auto &[node, subgame] : m_subgameByRoot) {
subgame->Invalidate();
}
m_subgamePostorder.clear();
m_subgameByRoot.clear();
m_valid = false;
}
};
mutable SubgameData m_subgameData;

/// @name Private auxiliary functions
//@{
Expand Down Expand Up @@ -100,7 +119,8 @@ 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 GetMinimalSubgame(const GameInfoset &) const override;
//@}

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

Expand Down
35 changes: 35 additions & 0 deletions src/pygambit/gambit.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ cdef extern from "games/game.h":
cdef cppclass c_GameStrategy "GameObjectPtr<GameStrategyRep>":
c_GameStrategyRep *deref "get"() except +RuntimeError

cdef cppclass c_GameSubgame "GameObjectPtr<GameSubgameRep>":
bool operator ==(c_GameSubgame) except +
bool operator !=(c_GameSubgame) except +
c_GameSubgameRep *deref "get"() except +RuntimeError

cdef cppclass c_PureStrategyProfile "PureStrategyProfile":
shared_ptr[c_PureStrategyProfileRep] deref "operator->"() except +
c_PureStrategyProfile(c_PureStrategyProfile) except +
Expand Down Expand Up @@ -219,6 +224,33 @@ cdef extern from "games/game.h":
c_GameAction GetPriorAction() except +
c_GameAction GetOwnPriorAction() except +

cdef cppclass c_GameSubgameRep "GameSubgameRep":
cppclass SubgameCollection:
cppclass iterator:
c_GameSubgame operator *()
iterator operator++()
bint operator ==(iterator)
bint operator !=(iterator)
int size() except +
iterator begin() except +
iterator end() except +

cppclass InfosetCollection:
cppclass iterator:
c_GameInfoset operator *()
iterator operator++()
bint operator ==(iterator)
bint operator !=(iterator)
int size() except +
iterator begin() except +
iterator end() except +

c_Game GetGame() except +
c_GameNode GetRoot() except +
c_GameSubgame GetParent() except +
SubgameCollection GetChildren() except +
InfosetCollection GetSubgameDifference() except +

cdef cppclass c_GameRep "GameRep":
cppclass Players:
cppclass iterator:
Expand Down Expand Up @@ -316,6 +348,9 @@ cdef extern from "games/game.h":
c_PureStrategyProfile NewPureStrategyProfile() # except + doesn't compile
c_MixedStrategyProfile[T] NewMixedStrategyProfile[T](T) # except + doesn't compile

c_GameSubgame GetMinimalSubgame(c_GameInfoset) except +
stdvector[c_GameSubgame] GetSubgames() except +

c_Game NewTree() except +
c_Game NewTable(stdvector[int]) except +

Expand Down
Loading
Loading