Skip to content

Commit a0780ef

Browse files
committed
Refactor implementation of GameTreeRep::IsPerfectRecall, add a new iterator class to zip actions and children; extend tests
Added #include <optional> Fix clang-tidy Add ActionsIterator class to zip actions and children; refactor PR-checker using a separate BuildInfosetParents method Eliminate duplicates in m_infosetParents and improve iterator organization
1 parent edbe2e6 commit a0780ef

11 files changed

Lines changed: 244 additions & 65 deletions

src/games/game.h

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ template <class P, class T> class ElementCollection {
105105
return *this;
106106
}
107107
value_type operator*() const { return m_container->at(m_index); }
108+
const P &GetOwner() const { return m_owner; }
108109
};
109110

110111
ElementCollection() = default;
@@ -458,6 +459,23 @@ class GameNodeRep : public GameObject {
458459
public:
459460
using Children = ElementCollection<GameNode, GameNodeRep>;
460461

462+
/// @brief A range class for iterating over a node's (action, child) pairs.
463+
class Actions {
464+
private:
465+
GameNode m_owner{nullptr};
466+
467+
public:
468+
class iterator;
469+
470+
explicit Actions(GameNode p_owner);
471+
472+
iterator begin() const;
473+
iterator end() const;
474+
};
475+
476+
/// @brief Returns a collection for iterating over this node's (action, child) pairs.
477+
Actions GetActions() const;
478+
461479
Game GetGame() const;
462480

463481
const std::string &GetLabel() const { return m_label; }
@@ -488,6 +506,97 @@ class GameNodeRep : public GameObject {
488506
bool IsSubgameRoot() const;
489507
};
490508

509+
class GameNodeRep::Actions::iterator {
510+
public:
511+
/// @name Iterator
512+
//@{
513+
using iterator_category = std::forward_iterator_tag;
514+
using value_type = std::pair<GameAction, GameNode>;
515+
using difference_type = std::ptrdiff_t;
516+
using pointer = value_type *;
517+
using reference = value_type;
518+
//@}
519+
520+
private:
521+
/// @brief An iterator to the action at the parent's information set.
522+
GameInfosetRep::Actions::iterator m_action_it;
523+
/// @brief An iterator to the child node.
524+
GameNodeRep::Children::iterator m_child_it;
525+
526+
public:
527+
/// @name Lifecycle
528+
//@{
529+
/// Default constructor. Creates an iterator in a past-the-end state.
530+
iterator() = default;
531+
532+
/// Creates a new iterator that zips an action iterator and a child iterator.
533+
iterator(GameInfosetRep::Actions::iterator p_action_it,
534+
GameNodeRep::Children::iterator p_child_it);
535+
//@}
536+
537+
/// @name Iterator Operations
538+
//@{
539+
/// Returns the current action-child pair.
540+
reference operator*() const { return {*m_action_it, *m_child_it}; }
541+
542+
/// Advances the iterator to the next pair (pre-increment).
543+
iterator &operator++()
544+
{
545+
++m_action_it;
546+
++m_child_it;
547+
return *this;
548+
}
549+
550+
/// Advances the iterator to the next pair (post-increment).
551+
iterator operator++(int)
552+
{
553+
iterator tmp = *this;
554+
++(*this);
555+
return tmp;
556+
}
557+
558+
/// Compares two iterators for equality.
559+
bool operator==(const iterator &p_other) const
560+
{
561+
// Comparing one of the wrapped iterators is sufficient as they move in lockstep.
562+
return m_child_it == p_other.m_child_it;
563+
}
564+
565+
/// Compares two iterators for inequality.
566+
bool operator!=(const iterator &p_other) const { return !(*this == p_other); }
567+
//@}
568+
569+
GameNode GetOwner() const;
570+
};
571+
572+
inline GameNodeRep::Actions::Actions(GameNode p_owner) : m_owner(p_owner) {}
573+
574+
inline GameNodeRep::Actions GameNodeRep::GetActions() const { return Actions(this); }
575+
576+
inline GameNodeRep::Actions::iterator GameNodeRep::Actions::begin() const
577+
{
578+
if (m_owner->IsTerminal()) {
579+
return end();
580+
}
581+
return {m_owner->GetInfoset()->GetActions().begin(), m_owner->GetChildren().begin()};
582+
}
583+
584+
inline GameNodeRep::Actions::iterator GameNodeRep::Actions::end() const
585+
{
586+
if (m_owner->IsTerminal()) {
587+
return {};
588+
}
589+
return {m_owner->GetInfoset()->GetActions().end(), m_owner->GetChildren().end()};
590+
}
591+
592+
inline GameNodeRep::Actions::iterator::iterator(GameInfosetRep::Actions::iterator p_action_it,
593+
GameNodeRep::Children::iterator p_child_it)
594+
: m_action_it(p_action_it), m_child_it(p_child_it)
595+
{
596+
}
597+
598+
inline GameNode GameNodeRep::Actions::iterator::GetOwner() const { return m_child_it.GetOwner(); }
599+
491600
/// This is the class for representing an arbitrary finite game.
492601
class GameRep : public std::enable_shared_from_this<GameRep> {
493602
friend class GameOutcomeRep;
@@ -648,15 +757,8 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
648757
/// Returns the set of terminal nodes which are descendants of members of an action
649758
virtual std::vector<GameNode> GetPlays(GameAction action) const { throw UndefinedException(); }
650759

651-
/// Returns true if the game is perfect recall. If not,
652-
/// a pair of violating information sets is returned in the parameters.
653-
virtual bool IsPerfectRecall(GameInfoset &, GameInfoset &) const = 0;
654760
/// Returns true if the game is perfect recall
655-
bool IsPerfectRecall() const
656-
{
657-
GameInfoset s, t;
658-
return IsPerfectRecall(s, t);
659-
}
761+
virtual bool IsPerfectRecall() const = 0;
660762
//@}
661763

662764
/// @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/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: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
#include <iostream>
2424
#include <algorithm>
2525
#include <numeric>
26+
#include <stack>
27+
#include <set>
2628

2729
#include "gambit.h"
2830
#include "gametree.h"
@@ -732,54 +734,18 @@ bool GameTreeRep::IsConstSum() const
732734
}
733735
}
734736

735-
bool GameTreeRep::IsPerfectRecall(GameInfoset &s1, GameInfoset &s2) const
737+
bool GameTreeRep::IsPerfectRecall() const
736738
{
737-
for (auto player : m_players) {
738-
for (size_t i = 1; i <= player->m_infosets.size(); i++) {
739-
auto *iset1 = player->m_infosets[i - 1];
740-
for (size_t j = 1; j <= player->m_infosets.size(); j++) {
741-
auto *iset2 = player->m_infosets[j - 1];
742-
743-
bool precedes = false;
744-
GameAction action = nullptr;
745-
746-
for (size_t m = 1; m <= iset2->m_members.size(); m++) {
747-
size_t n;
748-
for (n = 1; n <= iset1->m_members.size(); n++) {
749-
if (iset2->GetMember(m)->IsSuccessorOf(iset1->GetMember(n)) &&
750-
iset1->GetMember(n) != iset2->GetMember(m)) {
751-
precedes = true;
752-
for (const auto &act : iset1->GetActions()) {
753-
if (iset2->GetMember(m)->IsSuccessorOf(iset1->GetMember(n)->GetChild(act))) {
754-
if (action != nullptr && action != act) {
755-
s1 = iset1;
756-
s2 = iset2;
757-
return false;
758-
}
759-
action = act;
760-
}
761-
}
762-
break;
763-
}
764-
}
765-
766-
if (i == j && precedes) {
767-
s1 = iset1;
768-
s2 = iset2;
769-
return false;
770-
}
739+
if (m_infosetParents.empty() && !GetRoot()->IsTerminal()) {
740+
const_cast<GameTreeRep *>(this)->BuildInfosetParents();
741+
}
771742

772-
if (n > iset1->m_members.size() && precedes) {
773-
s1 = iset1;
774-
s2 = iset2;
775-
return false;
776-
}
777-
}
778-
}
779-
}
743+
if (GetRoot()->IsTerminal()) {
744+
return true;
780745
}
781746

782-
return true;
747+
return std::all_of(m_infosetParents.cbegin(), m_infosetParents.cend(),
748+
[](const auto &pair) { return pair.second.size() <= 1; });
783749
}
784750

785751
//------------------------------------------------------------------------
@@ -855,6 +821,7 @@ void GameTreeRep::ClearComputedValues() const
855821
player->m_strategies.clear();
856822
}
857823
const_cast<GameTreeRep *>(this)->m_nodePlays.clear();
824+
const_cast<GameTreeRep *>(this)->m_infosetParents.clear();
858825
m_computedValues = false;
859826
}
860827

@@ -893,6 +860,43 @@ std::vector<GameNodeRep *> GameTreeRep::BuildConsistentPlaysRecursiveImpl(GameNo
893860
return consistent_plays;
894861
}
895862

863+
void GameTreeRep::BuildInfosetParents()
864+
{
865+
std::map<GamePlayer, std::stack<GameAction>> prior_actions;
866+
std::stack<GameNodeRep::Actions::iterator> position;
867+
868+
for (auto player : m_players) {
869+
prior_actions[player].emplace(nullptr);
870+
}
871+
prior_actions[GetChance()].emplace(nullptr);
872+
prior_actions[GetRoot()->GetPlayer()].emplace(nullptr);
873+
874+
position.emplace(GetRoot()->GetActions().begin());
875+
m_infosetParents[GetRoot()->GetInfoset()].insert(nullptr);
876+
877+
while (!position.empty()) {
878+
GameNodeRep::Actions::iterator &current_it = position.top();
879+
const GameNode parent = current_it.GetOwner();
880+
881+
if (current_it != parent->GetActions().end()) {
882+
auto [action, child] = *current_it;
883+
884+
prior_actions[parent->GetPlayer()].top() = action;
885+
886+
if (!child->IsTerminal()) {
887+
m_infosetParents[child->GetInfoset()].insert(prior_actions[child->GetPlayer()].top());
888+
position.emplace(child->GetActions().begin());
889+
prior_actions[child->GetPlayer()].emplace(nullptr);
890+
}
891+
++current_it;
892+
}
893+
else {
894+
prior_actions[parent->GetPlayer()].pop();
895+
position.pop();
896+
}
897+
}
898+
}
899+
896900
//------------------------------------------------------------------------
897901
// GameTreeRep: Writing data files
898902
//------------------------------------------------------------------------

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> {

tests/test_extensive.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,28 @@ def test_game_add_players_nolabel():
4949
game.add_player()
5050

5151

52-
def test_game_is_perfect_recall():
53-
game = games.read_from_file("perfect_recall.efg")
54-
assert game.is_perfect_recall
52+
@pytest.mark.parametrize("game_filename,expected_result", [
53+
# Games that have perfect recall
54+
("e01.efg", True),
55+
("e02.efg", True),
56+
("cent3.efg", True),
57+
("poker.efg", True),
58+
("basic_extensive_game.efg", True),
5559
56-
57-
def test_game_is_not_perfect_recall():
58-
game = games.read_from_file("not_perfect_recall.efg")
59-
assert not game.is_perfect_recall
60+
# Games that do not have perfect recall
61+
("wichardt.efg", False), # forgetting past action
62+
("noPR-action-selten-horse.efg", False), # forgetting past action
63+
("noPR-information-no-deflate.efg", False), # forgetting past information
64+
("noPR-AM.efg", False), # absent-mindedness
65+
("noPR-action-AM.efg", False), # absent-mindedness + forgetting past action
66+
])
67+
def test_is_perfect_recall(game_filename: str, expected_result: bool):
68+
"""
69+
Verify the IsPerfectRecall implementation against a suite of games
70+
with and without the perfect recall property.
71+
"""
72+
game = games.read_from_file(game_filename)
73+
assert game.is_perfect_recall == expected_result
6074

6175

6276
def test_getting_payoff_by_label_string():
@@ -95,7 +109,7 @@ def test_outcome_index_exception_label():
95109
"game,strategy_labels,np_arrays_of_rsf",
96110
[
97111
###############################################################################
98-
# # 1 player; reduction; generic payoffs
112+
# 1 player; reduction; generic payoffs
99113
(
100114
games.create_reduction_one_player_generic_payoffs_efg(),
101115
[["11", "12", "2*", "3*", "4*"]],

tests/test_games/noPR-AM.efg

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
EFG 2 R "Untitled Extensive Game" { "Player 1" "Player 2" }
2+
""
3+
4+
p "" 1 1 "" { "1" "2" } 0
5+
p "" 2 1 "" { "1" "2" } 0
6+
p "" 1 1 "" { "1" "2" } 0
7+
t "" 1 "Outcome 1" { 1, 1 }
8+
p "" 2 2 "" { "1" "2" "3" } 0
9+
t "" 2 "Outcome 2" { 0, 2 }
10+
t "" 5 "Outcome 5" { 0, 5 }
11+
t "" 6 "Outcome 6" { 0, 6 }
12+
t "" 3 "Outcome 3" { 0, 3 }
13+
t "" 4 "Outcome 4" { 2, 0 }

0 commit comments

Comments
 (0)