Skip to content

Commit 06e3aab

Browse files
committed
Extended test suite for the PR-checker
1 parent 8a75070 commit 06e3aab

7 files changed

Lines changed: 152 additions & 104 deletions

src/games/gametree.cc

Lines changed: 52 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <numeric>
2626
#include <stack>
2727
#include <set>
28+
#include <variant>
2829

2930
#include "gambit.h"
3031
#include "gametree.h"
@@ -736,48 +737,9 @@ bool GameTreeRep::IsConstSum() const
736737

737738
bool GameTreeRep::IsPerfectRecall() const
738739
{
739-
if (m_infosetParents.empty() && !GetRoot()->IsTerminal()) {
740-
const_cast<GameTreeRep *>(this)->BuildInfosetParents();
741-
}
742-
743-
// ====================================================================
744-
745-
std::cerr << "\n--- m_infosetParents ---\n";
746-
747740
if (m_infosetParents.empty() && !m_root->IsTerminal()) {
748-
std::cerr << " (Cache is empty or game is trivial)\n";
749-
}
750-
751-
// Iterate through the map to print its contents.
752-
// Assumes m_infosetParents uses raw pointers as keys/values now.
753-
for (const auto &[infoset_ptr, parent_actions_set] : m_infosetParents) {
754-
// Print the information set identifier.
755-
std::cerr << " - Infoset " << infoset_ptr->GetPlayer()->GetNumber() << "."
756-
<< infoset_ptr->GetNumber() << " (Player '" << infoset_ptr->GetPlayer()->GetLabel()
757-
<< "'):\n";
758-
759-
if (parent_actions_set.empty()) {
760-
std::cerr << " - (No parent actions recorded)\n";
761-
}
762-
763-
// Print each recorded parent action for this infoset.
764-
for (const auto &action_ptr : parent_actions_set) {
765-
if (action_ptr) {
766-
// If the action is not null, print its label and the infoset it belongs to.
767-
std::cerr << " - Reached via Action '" << action_ptr->GetLabel() << "' (from Infoset "
768-
<< action_ptr->GetInfoset()->GetPlayer()->GetNumber() << "."
769-
<< action_ptr->GetInfoset()->GetNumber() << ")\n";
770-
}
771-
else {
772-
// This case is for the root or for players who haven't acted yet on a path.
773-
std::cerr << " - Reached via null action\n";
774-
}
775-
}
741+
const_cast<GameTreeRep *>(this)->BuildInfosetParents();
776742
}
777-
std::cerr << "---------------------------\n";
778-
// ====================================================================
779-
// DEBUGGING PRINTS END HERE
780-
// ====================================================================
781743

782744
if (GetRoot()->IsTerminal()) {
783745
return true;
@@ -901,76 +863,71 @@ std::vector<GameNodeRep *> GameTreeRep::BuildConsistentPlaysRecursiveImpl(GameNo
901863

902864
void GameTreeRep::BuildInfosetParents()
903865
{
904-
// The main traversal stack. It holds iterators that explore the children nodes.
905-
// It does not contain entries for the nodes that are skipped over
906-
// or where the previously taken decision is taken again due to absent-mindedness.
907-
std::stack<GameNodeRep::Actions::iterator> position;
908-
// tracks actions taken by each player on the current path
909-
std::map<GamePlayer, std::stack<GameAction>> prior_actions;
910-
// stores the first action choice made for an infoset on a given exploration path
911-
std::map<GameInfoset, std::pair<GameNode, GameAction>> initial_choice;
912-
913866
if (m_root->IsTerminal()) {
914-
m_infosetParents[m_root->GetInfoset()].insert(nullptr);
867+
m_infosetParents[m_root->m_infoset].insert(nullptr);
915868
return;
916869
}
917870

918-
for (auto player : m_players) {
919-
prior_actions[player].emplace(nullptr);
871+
using AbsentMindedEdge = std::pair<GameAction, GameNode>;
872+
using ActiveEdge = std::variant<GameNodeRep::Actions::iterator, AbsentMindedEdge>;
873+
std::stack<ActiveEdge> position;
874+
875+
std::map<GamePlayer, std::stack<GameAction>> prior_actions;
876+
std::map<GameInfoset, GameAction> path_choices;
877+
878+
for (auto player_rep : m_players) {
879+
prior_actions[GamePlayer(player_rep)].emplace(nullptr);
920880
}
921-
prior_actions[m_chance].emplace(nullptr);
881+
prior_actions[GamePlayer(m_chance)].emplace(nullptr);
922882

923883
position.emplace(m_root->GetActions().begin());
924-
prior_actions[m_root->GetPlayer()].emplace(nullptr);
925-
m_infosetParents[m_root->GetInfoset()].insert(nullptr);
884+
prior_actions[m_root->m_infoset->m_player].emplace(nullptr);
885+
if (m_root->m_infoset) {
886+
m_infosetParents[m_root->m_infoset].insert(nullptr);
887+
}
926888

927889
while (!position.empty()) {
928-
auto &current_it = position.top();
929-
auto parent = current_it.GetOwner();
930-
931-
if (current_it != parent->GetActions().end()) {
932-
auto [action, child] = *current_it;
933-
934-
prior_actions[parent->GetPlayer()].top() = action;
935-
initial_choice[parent->GetInfoset()] = {parent, action};
936-
937-
// records every emplace made onto the prior_actions stack during a fast-forward.
938-
std::vector<GamePlayer> fast_forward_history;
939-
940-
// fast forward absent-minded child nodes
941-
auto initial_choice_it = initial_choice.find(child->GetInfoset());
942-
while (initial_choice_it != initial_choice.end()) {
943-
auto initial_action = initial_choice_it->second.second;
944-
auto prior_action_ff = prior_actions[child->GetPlayer()].top();
945-
m_infosetParents[child->GetInfoset()].insert(prior_action_ff);
890+
ActiveEdge &current_edge = position.top();
891+
GameNode child, node;
892+
GameAction action;
893+
894+
if (std::holds_alternative<GameNodeRep::Actions::iterator>(current_edge)) {
895+
auto &current_it = std::get<GameNodeRep::Actions::iterator>(current_edge);
896+
node = current_it.GetOwner();
897+
898+
if (current_it == node->GetActions().end()) {
899+
prior_actions.at(node->m_infoset->m_player).pop();
900+
position.pop();
901+
path_choices.erase(node->m_infoset);
902+
continue;
903+
}
904+
else {
905+
std::tie(action, child) = *current_it;
906+
++current_it;
907+
path_choices[node->m_infoset] = action;
908+
}
909+
}
910+
else {
911+
std::tie(action, node) = std::get<AbsentMindedEdge>(current_edge);
912+
position.pop();
913+
child = node->GetChild(action);
914+
}
946915

947-
auto newchild = child->GetChild(initial_action);
916+
prior_actions.at(node->m_infoset->m_player).top() = action;
948917

949-
prior_actions[child->GetPlayer()].emplace(initial_action);
950-
fast_forward_history.emplace_back(child->GetPlayer());
918+
if (!child->IsTerminal()) {
919+
auto child_player = child->m_infoset->m_player;
920+
auto prior_action = prior_actions.at(child_player).top();
921+
m_infosetParents[child->m_infoset].insert(prior_action);
951922

952-
child = newchild;
953-
initial_choice_it = initial_choice.find(newchild->GetInfoset());
923+
if (path_choices.find(child->m_infoset) != path_choices.end()) {
924+
const GameAction replay_action = path_choices.at(child->m_infoset);
925+
position.emplace(AbsentMindedEdge{replay_action, child});
954926
}
955-
956-
if (!child->IsTerminal()) {
957-
auto child_player = child->GetPlayer();
958-
auto prior_action_desc = prior_actions[child_player].top();
959-
m_infosetParents[child->GetInfoset()].insert(prior_action_desc);
927+
else {
960928
position.emplace(child->GetActions().begin());
961-
prior_actions[child_player].emplace(nullptr);
962-
}
963-
964-
++current_it;
965-
966-
for (auto it = fast_forward_history.rbegin(); it != fast_forward_history.rend(); ++it) {
967-
prior_actions.at(*it).pop();
968929
}
969-
}
970-
else {
971-
prior_actions.at(parent->GetPlayer()).pop();
972-
position.pop();
973-
initial_choice.erase(parent->GetInfoset());
930+
prior_actions.at(child_player).emplace(nullptr);
974931
}
975932
}
976933
}

tests/test_extensive.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,27 +49,55 @@ def test_game_add_players_nolabel():
4949
game.add_player()
5050

5151

52-
@pytest.mark.parametrize("game_filename,expected_result", [
53-
# Games that have perfect recall
52+
@pytest.mark.parametrize("game_input,expected_result", [
53+
# Games with perfect recall from files (game_input is a string)
5454
("e01.efg", True),
5555
("e02.efg", True),
5656
("cent3.efg", True),
5757
("poker.efg", True),
5858
("basic_extensive_game.efg", True),
5959
60-
# Games that do not have perfect recall
61-
("wichardt.efg", False), # forgetting past action
60+
# Games with perfect recall from generated games (game_input is a gbt.Game object)
61+
# - Centipede games
62+
(games.Centipede.get_test_data(N=3, m0=2, m1=7)[0], True),
63+
(games.Centipede.get_test_data(N=4, m0=2, m1=7)[0], True),
64+
# - Two-player binary tree games
65+
(games.BinEfgTwoPlayer.get_test_data(level=3)[0], True),
66+
(games.BinEfgTwoPlayer.get_test_data(level=4)[0], True),
67+
# - Three-player binary tree games
68+
(games.BinEfgThreePlayer.get_test_data(level=3)[0], True),
69+
70+
# Games with imperfect recall from files (game_input is a string)
71+
# - imperfect recall without absent-mindedness
72+
("wichardt.efg", False), # forgetting past action; Wichardt (GEB, 2008)
6273
("noPR-action-selten-horse.efg", False), # forgetting past action
6374
("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
75+
("gilboa_two_am_agents.efg", False), # forgetting past information; Gilboa (GEB, 1997)
76+
# - imperfect recall with absent-mindedness
77+
("noPR-AM-driver-one-player.efg", False), # 1 players, one infoset unreached
78+
("noPR-AM-driver-two-players.efg", False), # 2 players, one infoset unreached
79+
("noPR-action-AM.efg", False), # 2 players + forgetting past action; P1 has one infoset
80+
("noPR-action-AM2.efg", False), # 2 players + forgetting past action; P1 has >1 infoset
81+
("noPR-action-AM-two-hops.efg", False), # 2 players, one AM-infoset each
82+
83+
# Games with imperfect recall from generated games (game_input is a gbt.Game object)
84+
# - One-player binary tree games
85+
(games.BinEfgOnePlayerIR.get_test_data(level=3)[0], False),
86+
(games.BinEfgOnePlayerIR.get_test_data(level=4)[0], False),
6687
])
67-
def test_is_perfect_recall(game_filename: str, expected_result: bool):
88+
def test_is_perfect_recall(game_input, expected_result: bool):
6889
"""
6990
Verify the IsPerfectRecall implementation against a suite of games
70-
with and without the perfect recall property.
91+
with and without the perfect recall, from both files and generation.
7192
"""
72-
game = games.read_from_file(game_filename)
93+
game = None
94+
if isinstance(game_input, str):
95+
game = games.read_from_file(game_input)
96+
elif isinstance(game_input, gbt.Game):
97+
game = game_input
98+
else:
99+
pytest.fail(f"Unknown type for game_input: {type(game_input)}")
100+
73101
assert game.is_perfect_recall == expected_result
74102

75103

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
EFG 2 R "Untitled Extensive Game" { "Player 1" "Player 2" }
2+
""
3+
4+
c "" 1 "" { "1" 1/2 "2" 1/2 } 0
5+
p "" 1 1 "" { "1" "2" } 0
6+
t "" 1 "Outcome 1" { 1, -1 }
7+
p "" 1 2 "" { "1" "2" } 0
8+
t "" 2 "Outcome 2" { 2, -2 }
9+
t "" 3 "Outcome 3" { 3, -3 }
10+
p "" 1 2 "" { "1" "2" } 0
11+
p "" 1 1 "" { "1" "2" } 0
12+
t "" 4 "Outcome 4" { 4, -4 }
13+
t "" 5 "Outcome 5" { 5, -5 }
14+
t "" 6 "Outcome 6" { 6, -6 }
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
EFG 2 R "Untitled Extensive Game" { "Player 1" "Player 2" }
2+
""
3+
4+
p "" 1 1 "" { "1" "2" } 0
5+
p "" 1 2 "" { "1" "2" } 0
6+
p "" 1 1 "" { "1" "2" } 0
7+
t "" 1 "Outcome 1" { 1, -1 }
8+
p "" 1 3 "" { "1" "2" } 0
9+
t "" 2 "Outcome 2" { 2, -2 }
10+
t "" 5 "Outcome 5" { 5, -5 }
11+
t "" 3 "Outcome 3" { 3, -3 }
12+
t "" 4 "Outcome 4" { 4, -4 }
File renamed without changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
p "" 2 1 "" { "1" "2" } 0
8+
p "" 1 2 "" { "1" "2" } 0
9+
t "" 1 "Outcome 1" { 1, 1 }
10+
p "" 1 1 "" { "1" "2" } 0
11+
t "" 10 "Outcome 10" { 10, -10 }
12+
t "" 11 "Outcome 11" { 11, -11 }
13+
t "" 12 "Outcome 12" { 12, -12 }
14+
p "" 2 2 "" { "1" "2" "3" } 0
15+
t "" 5 "Outcome 2" { 0, 2 }
16+
t "" 6 "Outcome 5" { 0, 5 }
17+
t "" 7 "Outcome 6" { 0, 6 }
18+
t "" 8 "Outcome 3" { 0, 3 }
19+
t "" 9 "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 "" 1 2 "" { "1" "2" } 0
7+
t "" 1 "Outcome 1" { 1, -1 }
8+
t "" 2 "Outcome 2" { 2, -2 }
9+
p "" 2 1 "" { "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 "" 1 3 "" { "1" "2" } 0
14+
t "" 5 "Outcome 2" { 5, -5 }
15+
t "" 6 "Outcome 2" { 6, -6 }
16+
p "" 2 2 "" { "1" "2" } 0
17+
t "" 7 "Outcome 2" { 7, -7 }
18+
t "" 8 "Outcome 2" { 8, -8 }

0 commit comments

Comments
 (0)