Skip to content

Commit 7289d00

Browse files
authored
Implement own prior actions (#653)
Adds functions to return "own prior action" for nodes and "own prior actions" for information sets (where the latter is the set of own prior actions of its member nodes). Closes #582.
1 parent 12f563d commit 7289d00

10 files changed

Lines changed: 370 additions & 32 deletions

File tree

ChangeLog

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212

1313
### Added
1414
- Tests for EFG Nash solvers -- `enumpoly_solve`, `lp_solve`, `lcp_solve` -- in behavior stratgegies
15+
- In `pygambit`, `Node` objects now have a read-only property `own_prior_action` and `Infoset` objects
16+
have a read-only property `own_prior_actions` to retrieve the last action or the set of last actions
17+
taken by the player before reaching the node or information set, respectively. (#582)
1518
- In `pygambit`, `Node` objects now have a read-only property `is_strategy_reachable` to determine
1619
if the node is reachable by at least one pure strategy profile. This proves useful for identifying
1720
unreachable parts of the game tree in games with absent-mindedness. (#629)

doc/pygambit.api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ Information about the game
149149
Node.player
150150
Node.is_successor_of
151151
Node.plays
152+
Node.own_prior_action
152153

153154
.. autosummary::
154155

@@ -162,6 +163,7 @@ Information about the game
162163
Infoset.members
163164
Infoset.precedes
164165
Infoset.plays
166+
Infoset.own_prior_actions
165167

166168
.. autosummary::
167169

src/games/game.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ class GameInfosetRep : public std::enable_shared_from_this<GameInfosetRep> {
247247

248248
bool Precedes(GameNode) const;
249249

250+
std::set<GameAction> GetOwnPriorActions() const;
251+
250252
const Number &GetActionProb(const GameAction &p_action) const
251253
{
252254
if (p_action->GetInfoset().get() != this) {
@@ -492,6 +494,7 @@ class GameNodeRep : public std::enable_shared_from_this<GameNodeRep> {
492494
bool IsTerminal() const { return m_children.empty(); }
493495
GamePlayer GetPlayer() const { return (m_infoset) ? m_infoset->GetPlayer() : nullptr; }
494496
GameAction GetPriorAction() const; // returns null if root node
497+
GameAction GetOwnPriorAction() const;
495498
GameNode GetParent() const { return (m_parent) ? m_parent->shared_from_this() : nullptr; }
496499
GameNode GetNextSibling() const;
497500
GameNode GetPriorSibling() const;
@@ -899,6 +902,11 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
899902
virtual std::vector<GameInfoset> GetInfosets() const { throw UndefinedException(); }
900903
/// Sort the information sets for each player in a canonical order
901904
virtual void SortInfosets() {}
905+
/// Returns the set of actions taken by the infoset's owner before reaching this infoset
906+
virtual std::set<GameAction> GetOwnPriorActions(const GameInfoset &p_infoset) const
907+
{
908+
throw UndefinedException();
909+
}
902910
//@}
903911

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

931944
/// @name Modification

src/games/gametree.cc

Lines changed: 113 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,16 @@ GameAction GameNodeRep::GetPriorAction() const
324324
return nullptr;
325325
}
326326

327+
GameAction GameNodeRep::GetOwnPriorAction() const
328+
{
329+
return m_game->GetOwnPriorAction(std::const_pointer_cast<GameNodeRep>(shared_from_this()));
330+
}
331+
332+
std::set<GameAction> GameInfosetRep::GetOwnPriorActions() const
333+
{
334+
return m_game->GetOwnPriorActions(std::const_pointer_cast<GameInfosetRep>(shared_from_this()));
335+
}
336+
327337
void GameNodeRep::DeleteOutcome(GameOutcomeRep *outc)
328338
{
329339
m_game->IncrementVersion();
@@ -394,7 +404,7 @@ bool GameNodeRep::IsStrategyReachable() const
394404
auto tree_game = static_cast<GameTreeRep *>(m_game);
395405

396406
if (!tree_game->m_unreachableNodes) {
397-
tree_game->BuildInfosetParents();
407+
tree_game->BuildUnreachableNodes();
398408
}
399409

400410
// A node is reachable if it is NOT in the set of unreachable nodes.
@@ -786,15 +796,16 @@ Rational GameTreeRep::GetPlayerMaxPayoff(const GamePlayer &p_player) const
786796

787797
bool GameTreeRep::IsPerfectRecall() const
788798
{
789-
if (m_infosetParents.empty() && !m_root->IsTerminal()) {
790-
const_cast<GameTreeRep *>(this)->BuildInfosetParents();
799+
if (!m_ownPriorActionInfo && !m_root->IsTerminal()) {
800+
BuildOwnPriorActions();
791801
}
792802

793803
if (GetRoot()->IsTerminal()) {
794804
return true;
795805
}
796806

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

@@ -847,7 +858,7 @@ void GameTreeRep::ClearComputedValues() const
847858
player->m_strategies.clear();
848859
}
849860
const_cast<GameTreeRep *>(this)->m_nodePlays.clear();
850-
const_cast<GameTreeRep *>(this)->m_infosetParents.clear();
861+
m_ownPriorActionInfo = nullptr;
851862
const_cast<GameTreeRep *>(this)->m_unreachableNodes = nullptr;
852863
m_computedValues = false;
853864
}
@@ -889,34 +900,119 @@ std::vector<GameNodeRep *> GameTreeRep::BuildConsistentPlaysRecursiveImpl(GameNo
889900
return consistent_plays;
890901
}
891902

892-
void GameTreeRep::BuildInfosetParents()
903+
void GameTreeRep::BuildOwnPriorActions() const
893904
{
894-
m_infosetParents.clear();
895-
m_unreachableNodes = std::make_unique<std::set<GameNodeRep *>>();
905+
auto info = std::make_shared<OwnPriorActionInfo>();
896906

897907
if (m_root->IsTerminal()) {
898-
m_infosetParents[m_root->m_infoset].insert(nullptr);
908+
m_ownPriorActionInfo = info;
899909
return;
900910
}
901911

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

917+
using ActiveEdge = GameNodeRep::Actions::iterator;
918+
919+
std::stack<ActiveEdge> position;
906920
std::map<GamePlayer, std::stack<GameAction>> prior_actions;
907-
std::map<GameInfoset, GameAction> path_choices;
908921

909922
for (auto player_rep : m_players) {
910923
prior_actions[GamePlayer(player_rep)].emplace(nullptr);
911924
}
912925
prior_actions[GamePlayer(m_chance)].emplace(nullptr);
913926

914927
position.emplace(m_root->GetActions().begin());
915-
prior_actions[m_root->m_infoset->m_player->shared_from_this()].emplace(nullptr);
916928
if (m_root->m_infoset) {
917-
m_infosetParents[m_root->m_infoset].insert(nullptr);
929+
prior_actions[m_root->m_infoset->m_player->shared_from_this()].emplace(nullptr);
930+
}
931+
932+
while (!position.empty()) {
933+
ActiveEdge &current_edge = position.top();
934+
auto node = current_edge.GetOwner();
935+
936+
if (current_edge == node->GetActions().end()) {
937+
if (node->m_infoset) {
938+
prior_actions.at(node->m_infoset->m_player->shared_from_this()).pop();
939+
}
940+
position.pop();
941+
continue;
942+
}
943+
944+
auto [action, child] = *current_edge;
945+
++current_edge;
946+
947+
if (node->m_infoset) {
948+
prior_actions.at(node->m_infoset->m_player->shared_from_this()).top() = action;
949+
}
950+
951+
if (!child->IsTerminal()) {
952+
if (child->m_infoset) {
953+
auto child_player = child->m_infoset->m_player->shared_from_this();
954+
auto prior_action = prior_actions.at(child_player).top();
955+
GameActionRep *raw_prior = prior_action ? prior_action.get() : nullptr;
956+
957+
info->node_map[child.get()] = raw_prior;
958+
info->infoset_map[child->m_infoset].insert(raw_prior);
959+
960+
position.emplace(child->GetActions().begin());
961+
prior_actions.at(child_player).emplace(nullptr);
962+
}
963+
else {
964+
position.emplace(child->GetActions().begin());
965+
}
966+
}
967+
}
968+
m_ownPriorActionInfo = info;
969+
}
970+
971+
GameAction GameTreeRep::GetOwnPriorAction(const GameNode &p_node) const
972+
{
973+
if (!m_ownPriorActionInfo) {
974+
BuildOwnPriorActions();
975+
}
976+
977+
auto it = m_ownPriorActionInfo->node_map.find(p_node.get());
978+
if (it != m_ownPriorActionInfo->node_map.end() && it->second) {
979+
return it->second->shared_from_this();
980+
}
981+
return nullptr;
982+
}
983+
984+
std::set<GameAction> GameTreeRep::GetOwnPriorActions(const GameInfoset &p_infoset) const
985+
{
986+
if (!m_ownPriorActionInfo) {
987+
BuildOwnPriorActions();
988+
}
989+
990+
std::set<GameAction> result;
991+
auto it = m_ownPriorActionInfo->infoset_map.find(p_infoset.get());
992+
993+
if (it != m_ownPriorActionInfo->infoset_map.end()) {
994+
for (auto *ptr : it->second) {
995+
result.insert(ptr ? ptr->shared_from_this() : nullptr);
996+
}
997+
}
998+
return result;
999+
}
1000+
1001+
void GameTreeRep::BuildUnreachableNodes()
1002+
{
1003+
m_unreachableNodes = std::make_unique<std::set<GameNodeRep *>>();
1004+
1005+
if (m_root->IsTerminal()) {
1006+
return;
9181007
}
9191008

1009+
using AbsentMindedEdge = std::pair<GameAction, GameNode>;
1010+
using ActiveEdge = std::variant<GameNodeRep::Actions::iterator, AbsentMindedEdge>;
1011+
1012+
std::stack<ActiveEdge> position;
1013+
std::map<GameInfoset, GameAction> path_choices;
1014+
position.emplace(m_root->GetActions().begin());
1015+
9201016
while (!position.empty()) {
9211017
ActiveEdge &current_edge = position.top();
9221018
GameNode child, node;
@@ -927,7 +1023,6 @@ void GameTreeRep::BuildInfosetParents()
9271023
node = current_it.GetOwner();
9281024

9291025
if (current_it == node->GetActions().end()) {
930-
prior_actions.at(node->m_infoset->m_player->shared_from_this()).pop();
9311026
position.pop();
9321027
path_choices.erase(node->m_infoset->shared_from_this());
9331028
continue;
@@ -944,40 +1039,31 @@ void GameTreeRep::BuildInfosetParents()
9441039
child = node->GetChild(action);
9451040
}
9461041

947-
prior_actions.at(node->m_infoset->m_player->shared_from_this()).top() = action;
9481042
if (!child->IsTerminal()) {
949-
auto child_player = child->m_infoset->m_player->shared_from_this();
950-
auto prior_action = prior_actions.at(child_player).top();
951-
m_infosetParents[child->m_infoset].insert(prior_action ? prior_action.get() : nullptr);
952-
1043+
// Check for Absent-Minded Re-entry of the infoset
9531044
if (path_choices.find(child->m_infoset->shared_from_this()) != path_choices.end()) {
9541045
const GameAction replay_action = path_choices.at(child->m_infoset->shared_from_this());
9551046
position.emplace(AbsentMindedEdge{replay_action, child});
9561047

957-
// Start of the traversal of unreachable subtrees
1048+
// Mark siblings and the nodes in their subtrees as unreachable
9581049
for (const auto &[current_action, subtree_root] : child->GetActions()) {
9591050
if (current_action != replay_action) {
960-
9611051
std::stack<GameNodeRep *> nodes_to_visit;
9621052
nodes_to_visit.push(subtree_root.get());
963-
9641053
while (!nodes_to_visit.empty()) {
9651054
GameNodeRep *current_unreachable_node = nodes_to_visit.top();
9661055
nodes_to_visit.pop();
9671056
m_unreachableNodes->insert(current_unreachable_node);
968-
9691057
for (const auto &unreachable_child : current_unreachable_node->GetChildren()) {
9701058
nodes_to_visit.push(unreachable_child.get());
9711059
}
9721060
}
9731061
}
9741062
}
975-
// End of the traversal of unreachable subtrees
9761063
}
9771064
else {
9781065
position.emplace(child->GetActions().begin());
9791066
}
980-
prior_actions.at(child_player).emplace(nullptr);
9811067
}
9821068
}
9831069
}

src/games/gametree.h

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,20 @@ class GameTreeRep : public GameExplicitRep {
3232
friend class GameInfosetRep;
3333
friend class GameActionRep;
3434

35+
private:
36+
struct OwnPriorActionInfo {
37+
std::map<GameNodeRep *, GameActionRep *> node_map;
38+
std::map<GameInfosetRep *, std::set<GameActionRep *>> infoset_map;
39+
};
40+
3541
protected:
3642
mutable bool m_computedValues{false};
3743
std::shared_ptr<GameNodeRep> m_root;
3844
std::shared_ptr<GamePlayerRep> m_chance;
3945
std::size_t m_numNodes = 1;
4046
std::size_t m_numNonterminalNodes = 0;
4147
std::map<GameNodeRep *, std::vector<GameNodeRep *>> m_nodePlays;
42-
std::map<GameInfosetRep *, std::set<GameActionRep *>> m_infosetParents;
48+
mutable std::shared_ptr<OwnPriorActionInfo> m_ownPriorActionInfo;
4349
mutable std::unique_ptr<std::set<GameNodeRep *>> m_unreachableNodes;
4450

4551
/// @name Private auxiliary functions
@@ -98,6 +104,8 @@ class GameTreeRep : public GameExplicitRep {
98104
size_t NumNodes() const override { return m_numNodes; }
99105
/// Returns the number of non-terminal nodes in the game
100106
size_t NumNonterminalNodes() const override { return m_numNonterminalNodes; }
107+
/// Returns the last action taken by the node's owner before reaching this node
108+
GameAction GetOwnPriorAction(const GameNode &p_node) const override;
101109
//@}
102110

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

127137
/// @name Modification
@@ -160,7 +170,8 @@ class GameTreeRep : public GameExplicitRep {
160170

161171
private:
162172
std::vector<GameNodeRep *> BuildConsistentPlaysRecursiveImpl(GameNodeRep *node);
163-
void BuildInfosetParents();
173+
void BuildOwnPriorActions() const;
174+
void BuildUnreachableNodes();
164175
};
165176

166177
template <class T> class TreeMixedStrategyProfileRep : public MixedStrategyProfileRep<T> {

src/pygambit/gambit.pxd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ from libcpp.string cimport string
33
from libcpp.memory cimport shared_ptr, unique_ptr
44
from libcpp.list cimport list as stdlist
55
from libcpp.vector cimport vector as stdvector
6+
from libcpp.set cimport set as stdset
67

78

89
cdef extern from "gambit.h":
@@ -145,6 +146,7 @@ cdef extern from "games/game.h":
145146

146147
bint IsChanceInfoset() except +
147148
bint Precedes(c_GameNode) except +
149+
stdset[c_GameAction] GetOwnPriorActions() except +
148150

149151
cdef cppclass c_GamePlayerRep "GamePlayerRep":
150152
cppclass Infosets:
@@ -220,6 +222,7 @@ cdef extern from "games/game.h":
220222
bint IsSubgameRoot() except +
221223
bint IsStrategyReachable() except +
222224
c_GameAction GetPriorAction() except +
225+
c_GameAction GetOwnPriorAction() except +
223226

224227
cdef cppclass c_GameRep "GameRep":
225228
cppclass Players:

0 commit comments

Comments
 (0)