Skip to content

Commit a36b242

Browse files
committed
Refactor implementation of GameTreeRep::IsPerfectRecall, extend tests
Added #include <optional>
1 parent b017e1a commit a36b242

11 files changed

Lines changed: 138 additions & 61 deletions

src/games/game.h

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -561,15 +561,8 @@ class GameRep : public BaseGameRep {
561561
/// Returns the set of terminal nodes which are descendants of members of an action
562562
virtual std::vector<GameNode> GetPlays(GameAction action) const { throw UndefinedException(); }
563563

564-
/// Returns true if the game is perfect recall. If not,
565-
/// a pair of violating information sets is returned in the parameters.
566-
virtual bool IsPerfectRecall(GameInfoset &, GameInfoset &) const = 0;
567564
/// Returns true if the game is perfect recall
568-
bool IsPerfectRecall() const
569-
{
570-
GameInfoset s, t;
571-
return IsPerfectRecall(s, t);
572-
}
565+
virtual bool IsPerfectRecall() const = 0;
573566
//@}
574567

575568
/// @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: 53 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -732,50 +732,63 @@ bool GameTreeRep::IsConstSum() const
732732
}
733733
}
734734

735-
bool GameTreeRep::IsPerfectRecall(GameInfoset &s1, GameInfoset &s2) const
735+
bool GameTreeRep::IsPerfectRecall() const
736736
{
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-
}
737+
using ChildIterator = GameNodeRep::Children::iterator;
738+
using ActionIterator = GameInfosetRep::Actions::iterator;
765739

766-
if (i == j && precedes) {
767-
s1 = iset1;
768-
s2 = iset2;
769-
return false;
770-
}
740+
std::map<GamePlayer, std::stack<GameAction>> prior_actions;
741+
std::map<GameInfoset, std::vector<GameAction>> infoset_parents;
742+
std::stack<std::tuple<GameNode, ChildIterator, ActionIterator>> position;
771743

772-
if (n > iset1->m_members.size() && precedes) {
773-
s1 = iset1;
774-
s2 = iset2;
775-
return false;
776-
}
777-
}
744+
for (auto player : GetPlayers()) {
745+
prior_actions[player].emplace(nullptr);
746+
}
747+
prior_actions[GetChance()].emplace(nullptr);
748+
prior_actions[GetRoot()->GetPlayer()].emplace(nullptr);
749+
750+
if (GetRoot()->IsTerminal()) {
751+
return true;
752+
}
753+
754+
position.emplace(GetRoot(), GetRoot()->GetChildren().begin(),
755+
GetRoot()->GetInfoset()->GetActions().begin());
756+
757+
infoset_parents[GetRoot()->GetInfoset()].emplace_back(nullptr);
758+
759+
while (!position.empty()) {
760+
auto &[parent, child_it, action_it] = position.top();
761+
762+
if (child_it != parent->GetChildren().end()) {
763+
const GameNode child = *child_it;
764+
const GameAction action = *action_it;
765+
766+
prior_actions[parent->GetPlayer()].top() = action;
767+
768+
if (!child->IsTerminal()) {
769+
infoset_parents[child->GetInfoset()].push_back(prior_actions[child->GetPlayer()].top());
770+
position.emplace(child, child->GetChildren().begin(),
771+
child->GetInfoset()->GetActions().begin());
772+
prior_actions[child->GetPlayer()].emplace(nullptr);
778773
}
774+
++child_it;
775+
++action_it;
776+
}
777+
778+
else {
779+
prior_actions[parent->GetPlayer()].pop();
780+
position.pop();
781+
}
782+
}
783+
784+
for (const auto &[infoset, parent_action_options] : infoset_parents) {
785+
if (parent_action_options.size() == 1) {
786+
continue;
787+
}
788+
const std::set<std::optional<GameAction>> unique_parents(parent_action_options.begin(),
789+
parent_action_options.end());
790+
if (unique_parents.size() > 1) {
791+
return false;
779792
}
780793
}
781794

src/games/gametree.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,7 @@ class GameTreeRep : public GameExplicitRep {
7272
//@{
7373
bool IsTree() const override { return true; }
7474
bool IsConstSum() const override;
75-
using GameRep::IsPerfectRecall;
76-
bool IsPerfectRecall(GameInfoset &, GameInfoset &) const override;
75+
bool IsPerfectRecall() const override;
7776
/// Turn on or off automatic canonicalization of the game
7877
void SetCanonicalization(bool p_doCanon) const
7978
{

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 }
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
EFG 2 R "Untitled Extensive Game" { "Player 1" "Player 2" }
2+
""
3+
4+
p "" 1 1 "" { "1" "2" } 0
5+
p "" 1 1 "" { "1" "2" } 0
6+
p "" 2 1 "" { "1" "2" } 0
7+
t "" 1 "Outcome 1" { 1, -1 }
8+
t "" 2 "Outcome 2" { 2, -2 }
9+
p "" 2 2 "" { "1" "2" } 0
10+
t "" 3 "Outcome 2" { 3, -3 }
11+
t "" 4 "Outcome 2" { 4, -4 }
12+
p "" 1 1 "" { "1" "2" } 0
13+
p "" 2 3 "" { "1" "2" } 0
14+
t "" 5 "Outcome 2" { 5, -5 }
15+
t "" 6 "Outcome 2" { 6, -6 }
16+
p "" 2 4 "" { "1" "2" } 0
17+
t "" 7 "Outcome 2" { 7, -7 }
18+
t "" 8 "Outcome 2" { 8, -8 }
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
EFG 2 R "Selten's Horse': 2 players, imperfect recall" { "Player 1" "Player 2" }
2+
""
3+
4+
p "" 1 1 "(1,1)" { "R" "L" } 0
5+
p "" 2 1 "(2,1)" { "R" "L" } 0
6+
t "" 1 "Outcome 1" { 1, 1 }
7+
p "" 1 2 "(1,2)" { "R" "L" } 0
8+
t "" 2 "Outcome 2" { 4, 4 }
9+
t "" 3 "Outcome 3" { 0, 0 }
10+
p "" 1 2 "(1,2)" { "R" "L" } 0
11+
t "" 4 "Outcome 4" { 3, 2 }
12+
t "" 5 "Outcome 5" { 0, 0 }

0 commit comments

Comments
 (0)