Skip to content
Merged
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
3 changes: 3 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

### Added
- Tests for EFG Nash solvers -- `enumpoly_solve`, `lp_solve`, `lcp_solve` -- in behavior stratgegies
- In `pygambit`, `Node` objects now have a read-only property `own_prior_action` and `Infoset` objects
have a read-only property `own_prior_actions` to retrieve the last action or the set of last actions
taken by the player before reaching the node or information set, respectively. (#582)
- In `pygambit`, `Node` objects now have a read-only property `is_strategy_reachable` to determine
if the node is reachable by at least one pure strategy profile. This proves useful for identifying
unreachable parts of the game tree in games with absent-mindedness. (#629)
Expand Down
2 changes: 2 additions & 0 deletions doc/pygambit.api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ Information about the game
Node.player
Node.is_successor_of
Node.plays
Node.own_prior_action

.. autosummary::

Expand All @@ -162,6 +163,7 @@ Information about the game
Infoset.members
Infoset.precedes
Infoset.plays
Infoset.own_prior_actions

.. autosummary::

Expand Down
13 changes: 13 additions & 0 deletions src/games/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ class GameInfosetRep : public std::enable_shared_from_this<GameInfosetRep> {

bool Precedes(GameNode) const;

std::set<GameAction> GetOwnPriorActions() const;

const Number &GetActionProb(const GameAction &p_action) const
{
if (p_action->GetInfoset().get() != this) {
Expand Down Expand Up @@ -492,6 +494,7 @@ class GameNodeRep : public std::enable_shared_from_this<GameNodeRep> {
bool IsTerminal() const { return m_children.empty(); }
GamePlayer GetPlayer() const { return (m_infoset) ? m_infoset->GetPlayer() : nullptr; }
GameAction GetPriorAction() const; // returns null if root node
GameAction GetOwnPriorAction() const;
GameNode GetParent() const { return (m_parent) ? m_parent->shared_from_this() : nullptr; }
GameNode GetNextSibling() const;
GameNode GetPriorSibling() const;
Expand Down Expand Up @@ -899,6 +902,11 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
virtual std::vector<GameInfoset> GetInfosets() const { throw UndefinedException(); }
/// Sort the information sets for each player in a canonical order
virtual void SortInfosets() {}
/// Returns the set of actions taken by the infoset's owner before reaching this infoset
virtual std::set<GameAction> GetOwnPriorActions(const GameInfoset &p_infoset) const
{
throw UndefinedException();
}
//@}

/// @name Outcomes
Expand Down Expand Up @@ -926,6 +934,11 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
virtual size_t NumNodes() const = 0;
/// Returns the number of non-terminal nodes in the game
virtual size_t NumNonterminalNodes() const = 0;
/// Returns the last action taken by the node's owner before reaching this node
virtual GameAction GetOwnPriorAction(const GameNode &p_node) const
{
throw UndefinedException();
}
//@}

/// @name Modification
Expand Down
140 changes: 113 additions & 27 deletions src/games/gametree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,16 @@ GameAction GameNodeRep::GetPriorAction() const
return nullptr;
}

GameAction GameNodeRep::GetOwnPriorAction() const
{
return m_game->GetOwnPriorAction(std::const_pointer_cast<GameNodeRep>(shared_from_this()));
}

std::set<GameAction> GameInfosetRep::GetOwnPriorActions() const
{
return m_game->GetOwnPriorActions(std::const_pointer_cast<GameInfosetRep>(shared_from_this()));
}

void GameNodeRep::DeleteOutcome(GameOutcomeRep *outc)
{
m_game->IncrementVersion();
Expand Down Expand Up @@ -393,7 +403,7 @@ bool GameNodeRep::IsStrategyReachable() const
auto tree_game = static_cast<GameTreeRep *>(m_game);

if (!tree_game->m_unreachableNodes) {
tree_game->BuildInfosetParents();
tree_game->BuildUnreachableNodes();
}

// A node is reachable if it is NOT in the set of unreachable nodes.
Expand Down Expand Up @@ -750,15 +760,16 @@ bool GameTreeRep::IsConstSum() const

bool GameTreeRep::IsPerfectRecall() const
{
if (m_infosetParents.empty() && !m_root->IsTerminal()) {
const_cast<GameTreeRep *>(this)->BuildInfosetParents();
if (!m_ownPriorActionInfo && !m_root->IsTerminal()) {
BuildOwnPriorActions();
}

if (GetRoot()->IsTerminal()) {
return true;
}

return std::all_of(m_infosetParents.cbegin(), m_infosetParents.cend(),
return std::all_of(m_ownPriorActionInfo->infoset_map.cbegin(),
m_ownPriorActionInfo->infoset_map.cend(),
[](const auto &pair) { return pair.second.size() <= 1; });
}

Expand Down Expand Up @@ -811,7 +822,7 @@ void GameTreeRep::ClearComputedValues() const
player->m_strategies.clear();
}
const_cast<GameTreeRep *>(this)->m_nodePlays.clear();
const_cast<GameTreeRep *>(this)->m_infosetParents.clear();
m_ownPriorActionInfo = nullptr;
const_cast<GameTreeRep *>(this)->m_unreachableNodes = nullptr;
m_computedValues = false;
}
Expand Down Expand Up @@ -853,34 +864,119 @@ std::vector<GameNodeRep *> GameTreeRep::BuildConsistentPlaysRecursiveImpl(GameNo
return consistent_plays;
}

void GameTreeRep::BuildInfosetParents()
void GameTreeRep::BuildOwnPriorActions() const
{
m_infosetParents.clear();
m_unreachableNodes = std::make_unique<std::set<GameNodeRep *>>();
auto info = std::make_shared<OwnPriorActionInfo>();

if (m_root->IsTerminal()) {
m_infosetParents[m_root->m_infoset].insert(nullptr);
m_ownPriorActionInfo = info;
return;
}

using AbsentMindedEdge = std::pair<GameAction, GameNode>;
using ActiveEdge = std::variant<GameNodeRep::Actions::iterator, AbsentMindedEdge>;
std::stack<ActiveEdge> position;
info->node_map[m_root.get()] = nullptr;
if (m_root->m_infoset) {
info->infoset_map[m_root->m_infoset].insert(nullptr);
}

using ActiveEdge = GameNodeRep::Actions::iterator;

std::stack<ActiveEdge> position;
std::map<GamePlayer, std::stack<GameAction>> prior_actions;
std::map<GameInfoset, GameAction> path_choices;

for (auto player_rep : m_players) {
prior_actions[GamePlayer(player_rep)].emplace(nullptr);
}
prior_actions[GamePlayer(m_chance)].emplace(nullptr);

position.emplace(m_root->GetActions().begin());
prior_actions[m_root->m_infoset->m_player->shared_from_this()].emplace(nullptr);
if (m_root->m_infoset) {
m_infosetParents[m_root->m_infoset].insert(nullptr);
prior_actions[m_root->m_infoset->m_player->shared_from_this()].emplace(nullptr);
}

while (!position.empty()) {
ActiveEdge &current_edge = position.top();
auto node = current_edge.GetOwner();

if (current_edge == node->GetActions().end()) {
if (node->m_infoset) {
prior_actions.at(node->m_infoset->m_player->shared_from_this()).pop();
}
position.pop();
continue;
}

auto [action, child] = *current_edge;
++current_edge;

if (node->m_infoset) {
prior_actions.at(node->m_infoset->m_player->shared_from_this()).top() = action;
}

if (!child->IsTerminal()) {
if (child->m_infoset) {
auto child_player = child->m_infoset->m_player->shared_from_this();
auto prior_action = prior_actions.at(child_player).top();
GameActionRep *raw_prior = prior_action ? prior_action.get() : nullptr;

info->node_map[child.get()] = raw_prior;
info->infoset_map[child->m_infoset].insert(raw_prior);

position.emplace(child->GetActions().begin());
prior_actions.at(child_player).emplace(nullptr);
}
else {
position.emplace(child->GetActions().begin());
}
}
}
m_ownPriorActionInfo = info;
}

GameAction GameTreeRep::GetOwnPriorAction(const GameNode &p_node) const
{
if (!m_ownPriorActionInfo) {
BuildOwnPriorActions();
}

auto it = m_ownPriorActionInfo->node_map.find(p_node.get());
if (it != m_ownPriorActionInfo->node_map.end() && it->second) {
return it->second->shared_from_this();
}
return nullptr;
}

std::set<GameAction> GameTreeRep::GetOwnPriorActions(const GameInfoset &p_infoset) const
{
if (!m_ownPriorActionInfo) {
BuildOwnPriorActions();
}

std::set<GameAction> result;
auto it = m_ownPriorActionInfo->infoset_map.find(p_infoset.get());

if (it != m_ownPriorActionInfo->infoset_map.end()) {
for (auto *ptr : it->second) {
result.insert(ptr ? ptr->shared_from_this() : nullptr);
}
}
return result;
}

void GameTreeRep::BuildUnreachableNodes()
{
m_unreachableNodes = std::make_unique<std::set<GameNodeRep *>>();

if (m_root->IsTerminal()) {
return;
}

using AbsentMindedEdge = std::pair<GameAction, GameNode>;
using ActiveEdge = std::variant<GameNodeRep::Actions::iterator, AbsentMindedEdge>;

std::stack<ActiveEdge> position;
std::map<GameInfoset, GameAction> path_choices;
position.emplace(m_root->GetActions().begin());

while (!position.empty()) {
ActiveEdge &current_edge = position.top();
GameNode child, node;
Expand All @@ -891,7 +987,6 @@ void GameTreeRep::BuildInfosetParents()
node = current_it.GetOwner();

if (current_it == node->GetActions().end()) {
prior_actions.at(node->m_infoset->m_player->shared_from_this()).pop();
position.pop();
path_choices.erase(node->m_infoset->shared_from_this());
continue;
Expand All @@ -908,40 +1003,31 @@ void GameTreeRep::BuildInfosetParents()
child = node->GetChild(action);
}

prior_actions.at(node->m_infoset->m_player->shared_from_this()).top() = action;
if (!child->IsTerminal()) {
auto child_player = child->m_infoset->m_player->shared_from_this();
auto prior_action = prior_actions.at(child_player).top();
m_infosetParents[child->m_infoset].insert(prior_action ? prior_action.get() : nullptr);

// Check for Absent-Minded Re-entry of the infoset
if (path_choices.find(child->m_infoset->shared_from_this()) != path_choices.end()) {
const GameAction replay_action = path_choices.at(child->m_infoset->shared_from_this());
position.emplace(AbsentMindedEdge{replay_action, child});

// Start of the traversal of unreachable subtrees
// Mark siblings and the nodes in their subtrees as unreachable
for (const auto &[current_action, subtree_root] : child->GetActions()) {
if (current_action != replay_action) {

std::stack<GameNodeRep *> nodes_to_visit;
nodes_to_visit.push(subtree_root.get());

while (!nodes_to_visit.empty()) {
GameNodeRep *current_unreachable_node = nodes_to_visit.top();
nodes_to_visit.pop();
m_unreachableNodes->insert(current_unreachable_node);

for (const auto &unreachable_child : current_unreachable_node->GetChildren()) {
nodes_to_visit.push(unreachable_child.get());
}
}
}
}
// End of the traversal of unreachable subtrees
}
else {
position.emplace(child->GetActions().begin());
}
prior_actions.at(child_player).emplace(nullptr);
}
}
}
Expand Down
15 changes: 13 additions & 2 deletions src/games/gametree.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,20 @@ class GameTreeRep : public GameExplicitRep {
friend class GameInfosetRep;
friend class GameActionRep;

private:
struct OwnPriorActionInfo {
std::map<GameNodeRep *, GameActionRep *> node_map;
std::map<GameInfosetRep *, std::set<GameActionRep *>> infoset_map;
};

protected:
mutable bool m_computedValues{false};
std::shared_ptr<GameNodeRep> m_root;
std::shared_ptr<GamePlayerRep> m_chance;
std::size_t m_numNodes = 1;
std::size_t m_numNonterminalNodes = 0;
std::map<GameNodeRep *, std::vector<GameNodeRep *>> m_nodePlays;
std::map<GameInfosetRep *, std::set<GameActionRep *>> m_infosetParents;
mutable std::shared_ptr<OwnPriorActionInfo> m_ownPriorActionInfo;
mutable std::unique_ptr<std::set<GameNodeRep *>> m_unreachableNodes;

/// @name Private auxiliary functions
Expand Down Expand Up @@ -93,6 +99,8 @@ class GameTreeRep : public GameExplicitRep {
size_t NumNodes() const override { return m_numNodes; }
/// Returns the number of non-terminal nodes in the game
size_t NumNonterminalNodes() const override { return m_numNonterminalNodes; }
/// Returns the last action taken by the node's owner before reaching this node
GameAction GetOwnPriorAction(const GameNode &p_node) const override;
//@}

void DeleteOutcome(const GameOutcome &) override;
Expand All @@ -117,6 +125,8 @@ class GameTreeRep : public GameExplicitRep {
std::vector<GameInfoset> GetInfosets() const override;
/// Sort the information sets for each player in a canonical order
void SortInfosets() override;
/// Returns the set of actions taken by the infoset's owner before reaching this infoset
std::set<GameAction> GetOwnPriorActions(const GameInfoset &p_infoset) const override;
//@}

/// @name Modification
Expand Down Expand Up @@ -155,7 +165,8 @@ class GameTreeRep : public GameExplicitRep {

private:
std::vector<GameNodeRep *> BuildConsistentPlaysRecursiveImpl(GameNodeRep *node);
void BuildInfosetParents();
void BuildOwnPriorActions() const;
void BuildUnreachableNodes();
};

template <class T> class TreeMixedStrategyProfileRep : public MixedStrategyProfileRep<T> {
Expand Down
3 changes: 3 additions & 0 deletions src/pygambit/gambit.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ from libcpp.string cimport string
from libcpp.memory cimport shared_ptr, unique_ptr
from libcpp.list cimport list as stdlist
from libcpp.vector cimport vector as stdvector
from libcpp.set cimport set as stdset


cdef extern from "gambit.h":
Expand Down Expand Up @@ -145,6 +146,7 @@ cdef extern from "games/game.h":

bint IsChanceInfoset() except +
bint Precedes(c_GameNode) except +
stdset[c_GameAction] GetOwnPriorActions() except +

cdef cppclass c_GamePlayerRep "GamePlayerRep":
cppclass Infosets:
Expand Down Expand Up @@ -220,6 +222,7 @@ cdef extern from "games/game.h":
bint IsSubgameRoot() except +
bint IsStrategyReachable() except +
c_GameAction GetPriorAction() except +
c_GameAction GetOwnPriorAction() except +

cdef cppclass c_GameRep "GameRep":
cppclass Players:
Expand Down
Loading
Loading