Skip to content

Commit a72ead3

Browse files
authored
Replace GameTreeRep::IsPerfectRecall with efficient single-pass algorithm
* Replaces `IsPerfectRecall` with new algorithm requiring a single pass of the game tree. This will be substantially more efficient in almost all cases. * Extends test suite with examples of games of perfect and imperfect recall. Closes #484.
1 parent 61f7803 commit a72ead3

16 files changed

Lines changed: 370 additions & 64 deletions

src/games/game.h

Lines changed: 109 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -412,12 +412,29 @@ class GameNodeRep : public std::enable_shared_from_this<GameNodeRep> {
412412
public:
413413
using Children = ElementCollection<GameNode, GameNodeRep>;
414414

415+
/// @brief A range class for iterating over a node's (action, child) pairs.
416+
class Actions {
417+
private:
418+
const GameNodeRep *m_owner{nullptr};
419+
420+
public:
421+
class iterator;
422+
423+
Actions(const GameNodeRep *p_owner);
424+
425+
iterator begin() const;
426+
iterator end() const;
427+
};
428+
415429
GameNodeRep(GameRep *e, GameNodeRep *p);
416430
~GameNodeRep();
417431

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

435+
/// @brief Returns a collection for iterating over this node's (action, child) pairs.
436+
Actions GetActions() const;
437+
421438
Game GetGame() const;
422439

423440
const std::string &GetLabel() const { return m_label; }
@@ -451,6 +468,97 @@ class GameNodeRep : public std::enable_shared_from_this<GameNodeRep> {
451468
bool IsSubgameRoot() const;
452469
};
453470

471+
class GameNodeRep::Actions::iterator {
472+
public:
473+
/// @name Iterator
474+
//@{
475+
using iterator_category = std::forward_iterator_tag;
476+
using value_type = std::pair<GameAction, GameNode>;
477+
using difference_type = std::ptrdiff_t;
478+
using pointer = value_type *;
479+
using reference = value_type;
480+
//@}
481+
482+
private:
483+
/// @brief An iterator to the action at the parent's information set.
484+
GameInfosetRep::Actions::iterator m_action_it;
485+
/// @brief An iterator to the child node.
486+
GameNodeRep::Children::iterator m_child_it;
487+
488+
public:
489+
/// @name Lifecycle
490+
//@{
491+
/// Default constructor. Creates an iterator in a past-the-end state.
492+
iterator() = default;
493+
494+
/// Creates a new iterator that zips an action iterator and a child iterator.
495+
iterator(GameInfosetRep::Actions::iterator p_action_it,
496+
GameNodeRep::Children::iterator p_child_it);
497+
//@}
498+
499+
/// @name Iterator Operations
500+
//@{
501+
/// Returns the current action-child pair.
502+
reference operator*() const { return {*m_action_it, *m_child_it}; }
503+
504+
/// Advances the iterator to the next pair (pre-increment).
505+
iterator &operator++()
506+
{
507+
++m_action_it;
508+
++m_child_it;
509+
return *this;
510+
}
511+
512+
/// Advances the iterator to the next pair (post-increment).
513+
iterator operator++(int)
514+
{
515+
iterator tmp = *this;
516+
++(*this);
517+
return tmp;
518+
}
519+
520+
/// Compares two iterators for equality.
521+
bool operator==(const iterator &p_other) const
522+
{
523+
// Comparing one of the wrapped iterators is sufficient as they move in lockstep.
524+
return m_child_it == p_other.m_child_it;
525+
}
526+
527+
/// Compares two iterators for inequality.
528+
bool operator!=(const iterator &p_other) const { return !(*this == p_other); }
529+
//@}
530+
531+
GameNode GetOwner() const;
532+
};
533+
534+
inline GameNodeRep::Actions::Actions(const GameNodeRep *p_owner) : m_owner(p_owner) {}
535+
536+
inline GameNodeRep::Actions GameNodeRep::GetActions() const { return {Actions(this)}; }
537+
538+
inline GameNodeRep::Actions::iterator GameNodeRep::Actions::begin() const
539+
{
540+
if (m_owner->IsTerminal()) {
541+
return end();
542+
}
543+
return {m_owner->GetInfoset()->GetActions().begin(), m_owner->GetChildren().begin()};
544+
}
545+
546+
inline GameNodeRep::Actions::iterator GameNodeRep::Actions::end() const
547+
{
548+
if (m_owner->IsTerminal()) {
549+
return {};
550+
}
551+
return {m_owner->GetInfoset()->GetActions().end(), m_owner->GetChildren().end()};
552+
}
553+
554+
inline GameNodeRep::Actions::iterator::iterator(GameInfosetRep::Actions::iterator p_action_it,
555+
GameNodeRep::Children::iterator p_child_it)
556+
: m_action_it(p_action_it), m_child_it(p_child_it)
557+
{
558+
}
559+
560+
inline GameNode GameNodeRep::Actions::iterator::GetOwner() const { return m_child_it.GetOwner(); }
561+
454562
/// This is the class for representing an arbitrary finite game.
455563
class GameRep : public std::enable_shared_from_this<GameRep> {
456564
friend class GameOutcomeRep;
@@ -611,15 +719,8 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
611719
/// Returns the set of terminal nodes which are descendants of members of an action
612720
virtual std::vector<GameNode> GetPlays(GameAction action) const { throw UndefinedException(); }
613721

614-
/// Returns true if the game is perfect recall. If not,
615-
/// a pair of violating information sets is returned in the parameters.
616-
virtual bool IsPerfectRecall(GameInfoset &, GameInfoset &) const = 0;
617722
/// Returns true if the game is perfect recall
618-
bool IsPerfectRecall() const
619-
{
620-
GameInfoset s, t;
621-
return IsPerfectRecall(s, t);
622-
}
723+
virtual bool IsPerfectRecall() const = 0;
623724
//@}
624725

625726
/// @name Writing data files

src/games/gameagg.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class GameAGGRep : public GameRep {
8484
//@{
8585
bool IsTree() const override { return false; }
8686
bool IsAgg() const override { return true; }
87-
bool IsPerfectRecall(GameInfoset &, GameInfoset &) const override { return true; }
87+
bool IsPerfectRecall() const override { return true; }
8888
bool IsConstSum() const override;
8989
/// Returns the smallest payoff to any player in any outcome of the game
9090
Rational GetMinPayoff() const override { return Rational(aggPtr->getMinPayoff()); }

src/games/gamebagg.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ class GameBAGGRep : public GameRep {
9191
//@{
9292
bool IsTree() const override { return false; }
9393
virtual bool IsBagg() const { return true; }
94-
bool IsPerfectRecall(GameInfoset &, GameInfoset &) const override { return true; }
94+
bool IsPerfectRecall() const override { return true; }
9595
bool IsConstSum() const override { throw UndefinedException(); }
9696
/// Returns the smallest payoff to any player in any outcome of the game
9797
Rational GetMinPayoff() const override { return Rational(baggPtr->getMinPayoff()); }

src/games/gameobject.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ template <class P, class T> class ElementCollection {
141141
return *this;
142142
}
143143
value_type operator*() const { return m_container->at(m_index); }
144+
145+
inline const P &GetOwner() const { return m_owner; }
144146
};
145147

146148
ElementCollection() = default;

src/games/gametable.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class GameTableRep : public GameExplicitRep {
5757
//@{
5858
bool IsTree() const override { return false; }
5959
bool IsConstSum() const override;
60-
bool IsPerfectRecall(GameInfoset &, GameInfoset &) const override { return true; }
60+
bool IsPerfectRecall() const override { return true; }
6161
//@}
6262

6363
/// @name Dimensions of the game

src/games/gametree.cc

Lines changed: 83 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
#include <iostream>
2424
#include <algorithm>
2525
#include <numeric>
26+
#include <stack>
27+
#include <set>
28+
#include <variant>
2629

2730
#include "gambit.h"
2831
#include "gametree.h"
@@ -748,54 +751,18 @@ bool GameTreeRep::IsConstSum() const
748751
}
749752
}
750753

751-
bool GameTreeRep::IsPerfectRecall(GameInfoset &s1, GameInfoset &s2) const
754+
bool GameTreeRep::IsPerfectRecall() const
752755
{
753-
for (auto player : m_players) {
754-
for (size_t i = 1; i <= player->m_infosets.size(); i++) {
755-
auto iset1 = player->m_infosets[i - 1];
756-
for (size_t j = 1; j <= player->m_infosets.size(); j++) {
757-
auto iset2 = player->m_infosets[j - 1];
758-
759-
bool precedes = false;
760-
GameAction action = nullptr;
761-
762-
for (size_t m = 1; m <= iset2->m_members.size(); m++) {
763-
size_t n;
764-
for (n = 1; n <= iset1->m_members.size(); n++) {
765-
if (iset2->GetMember(m)->IsSuccessorOf(iset1->GetMember(n)) &&
766-
iset1->GetMember(n) != iset2->GetMember(m)) {
767-
precedes = true;
768-
for (const auto &act : iset1->GetActions()) {
769-
if (iset2->GetMember(m)->IsSuccessorOf(iset1->GetMember(n)->GetChild(act))) {
770-
if (action != nullptr && action != act) {
771-
s1 = iset1;
772-
s2 = iset2;
773-
return false;
774-
}
775-
action = act;
776-
}
777-
}
778-
break;
779-
}
780-
}
781-
782-
if (i == j && precedes) {
783-
s1 = iset1;
784-
s2 = iset2;
785-
return false;
786-
}
756+
if (m_infosetParents.empty() && !m_root->IsTerminal()) {
757+
const_cast<GameTreeRep *>(this)->BuildInfosetParents();
758+
}
787759

788-
if (n > iset1->m_members.size() && precedes) {
789-
s1 = iset1;
790-
s2 = iset2;
791-
return false;
792-
}
793-
}
794-
}
795-
}
760+
if (GetRoot()->IsTerminal()) {
761+
return true;
796762
}
797763

798-
return true;
764+
return std::all_of(m_infosetParents.cbegin(), m_infosetParents.cend(),
765+
[](const auto &pair) { return pair.second.size() <= 1; });
799766
}
800767

801768
//------------------------------------------------------------------------
@@ -872,6 +839,7 @@ void GameTreeRep::ClearComputedValues() const
872839
player->m_strategies.clear();
873840
}
874841
const_cast<GameTreeRep *>(this)->m_nodePlays.clear();
842+
const_cast<GameTreeRep *>(this)->m_infosetParents.clear();
875843
m_computedValues = false;
876844
}
877845

@@ -912,6 +880,77 @@ std::vector<GameNodeRep *> GameTreeRep::BuildConsistentPlaysRecursiveImpl(GameNo
912880
return consistent_plays;
913881
}
914882

883+
void GameTreeRep::BuildInfosetParents()
884+
{
885+
if (m_root->IsTerminal()) {
886+
m_infosetParents[m_root->m_infoset].insert(nullptr);
887+
return;
888+
}
889+
890+
using AbsentMindedEdge = std::pair<GameAction, GameNode>;
891+
using ActiveEdge = std::variant<GameNodeRep::Actions::iterator, AbsentMindedEdge>;
892+
std::stack<ActiveEdge> position;
893+
894+
std::map<GamePlayer, std::stack<GameAction>> prior_actions;
895+
std::map<GameInfoset, GameAction> path_choices;
896+
897+
for (auto player_rep : m_players) {
898+
prior_actions[GamePlayer(player_rep)].emplace(nullptr);
899+
}
900+
prior_actions[GamePlayer(m_chance)].emplace(nullptr);
901+
902+
position.emplace(m_root->GetActions().begin());
903+
prior_actions[m_root->m_infoset->m_player->shared_from_this()].emplace(nullptr);
904+
if (m_root->m_infoset) {
905+
m_infosetParents[m_root->m_infoset].insert(nullptr);
906+
}
907+
908+
while (!position.empty()) {
909+
ActiveEdge &current_edge = position.top();
910+
GameNode child, node;
911+
GameAction action;
912+
913+
if (std::holds_alternative<GameNodeRep::Actions::iterator>(current_edge)) {
914+
auto &current_it = std::get<GameNodeRep::Actions::iterator>(current_edge);
915+
node = current_it.GetOwner();
916+
917+
if (current_it == node->GetActions().end()) {
918+
prior_actions.at(node->m_infoset->m_player->shared_from_this()).pop();
919+
position.pop();
920+
path_choices.erase(node->m_infoset->shared_from_this());
921+
continue;
922+
}
923+
else {
924+
std::tie(action, child) = *current_it;
925+
++current_it;
926+
path_choices[node->m_infoset->shared_from_this()] = action;
927+
}
928+
}
929+
else {
930+
std::tie(action, node) = std::get<AbsentMindedEdge>(current_edge);
931+
position.pop();
932+
child = node->GetChild(action);
933+
}
934+
935+
prior_actions.at(node->m_infoset->m_player->shared_from_this()).top() = action;
936+
937+
if (!child->IsTerminal()) {
938+
auto child_player = child->m_infoset->m_player->shared_from_this();
939+
auto prior_action = prior_actions.at(child_player).top();
940+
m_infosetParents[child->m_infoset].insert(prior_action ? prior_action.get() : nullptr);
941+
942+
if (path_choices.find(child->m_infoset->shared_from_this()) != path_choices.end()) {
943+
const GameAction replay_action = path_choices.at(child->m_infoset->shared_from_this());
944+
position.emplace(AbsentMindedEdge{replay_action, child});
945+
}
946+
else {
947+
position.emplace(child->GetActions().begin());
948+
}
949+
prior_actions.at(child_player).emplace(nullptr);
950+
}
951+
}
952+
}
953+
915954
//------------------------------------------------------------------------
916955
// GameTreeRep: Writing data files
917956
//------------------------------------------------------------------------

src/games/gametree.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class GameTreeRep : public GameExplicitRep {
3939
std::size_t m_numNodes = 1;
4040
std::size_t m_numNonterminalNodes = 0;
4141
std::map<GameNodeRep *, std::vector<GameNodeRep *>> m_nodePlays;
42+
std::map<GameInfosetRep *, std::set<GameActionRep *>> m_infosetParents;
4243

4344
/// @name Private auxiliary functions
4445
//@{
@@ -72,8 +73,7 @@ class GameTreeRep : public GameExplicitRep {
7273
//@{
7374
bool IsTree() const override { return true; }
7475
bool IsConstSum() const override;
75-
using GameRep::IsPerfectRecall;
76-
bool IsPerfectRecall(GameInfoset &, GameInfoset &) const override;
76+
bool IsPerfectRecall() const override;
7777
/// Turn on or off automatic canonicalization of the game
7878
void SetCanonicalization(bool p_doCanon) const
7979
{
@@ -160,6 +160,7 @@ class GameTreeRep : public GameExplicitRep {
160160

161161
private:
162162
std::vector<GameNodeRep *> BuildConsistentPlaysRecursiveImpl(GameNodeRep *node);
163+
void BuildInfosetParents();
163164
};
164165

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

0 commit comments

Comments
 (0)