Skip to content

Commit ce79b7b

Browse files
committed
Change to only sorting information sets on demand.
The order in which information sets, and their members, was iterated was previously enforced to be the order encountered in a depth-first traversal of the tree. This scales poorly when building large games using many operations. This introduces a new operation, `SortInfosets` in C++ and `sort_infosets` in Python, which does this sorting, but only on demand when called. Sorting is done automatically in the following circumstances: * When reading an .efg file; * When creating the reduced strategic form of a game (to ensure consistent strategy labels); * After every operation in the GUI (where performance is not critical relative to the usefulness of having lists of information sets and members be independent of the editing order) Closes #483.
1 parent 07db8b4 commit ce79b7b

File tree

12 files changed

+117
-96
lines changed

12 files changed

+117
-96
lines changed

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222

2323
### Changed
2424
- Internally in C++ `std::shared_ptr` are now used to manage memory allocated for game objects. (#518)
25+
- The iteration order of a player's information sets, and of the members of an information set, now may
26+
depend on the order of operations to build a game tree. The previous behaviour - ensuring sorting
27+
by the order encountered in a depth-first traversal of the tree - can now be obtained by calling
28+
`SortInfosets` (C++) or `sort_infosets` (Python) on the game. (#483)
2529

2630
## [16.3.1] - unreleased
2731

doc/pygambit.api.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,9 @@ Transforming game information structure
6969
Game.set_player
7070
Game.set_infoset
7171
Game.leave_infoset
72-
Game.reveal
7372
Game.set_chance_probs
73+
Game.reveal
74+
Game.sort_infosets
7475

7576

7677
Transforming game components

src/games/file.cc

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@
2828
#include <algorithm>
2929

3030
#include "gambit.h"
31-
// for explicit access to turning off canonicalization
32-
#include "gametree.h"
3331

3432
namespace {
3533
// This anonymous namespace encapsulates the file-parsing code
@@ -732,7 +730,6 @@ Game ReadEfgFile(std::istream &p_stream)
732730

733731
TreeData treeData;
734732
Game game = NewTree();
735-
dynamic_cast<GameTreeRep &>(*game).SetCanonicalization(false);
736733
game->SetTitle(parser.GetLastText());
737734
ReadPlayers(parser, game, treeData);
738735
if (parser.GetNextToken() == TOKEN_TEXT) {
@@ -741,7 +738,7 @@ Game ReadEfgFile(std::istream &p_stream)
741738
parser.GetNextToken();
742739
}
743740
ParseNode(parser, game, game->GetRoot(), treeData);
744-
dynamic_cast<GameTreeRep &>(*game).SetCanonicalization(true);
741+
game->SortInfosets();
745742
return game;
746743
}
747744

src/games/game.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,8 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
862862
virtual GameInfoset GetInfoset(int iset) const { throw UndefinedException(); }
863863
/// Returns the set of information sets in the game
864864
virtual std::vector<GameInfoset> GetInfosets() const { throw UndefinedException(); }
865+
/// Sort the information sets for each player in a canonical order
866+
virtual void SortInfosets() {}
865867
//@}
866868

867869
/// @name Outcomes

src/games/gametree.cc

Lines changed: 29 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ void GameTreeRep::DeleteAction(GameAction p_action)
145145
member->m_children.erase(it);
146146
}
147147
ClearComputedValues();
148-
Canonicalize();
149148
}
150149

151150
GameInfoset GameActionRep::GetInfoset() const { return m_infoset->shared_from_this(); }
@@ -193,7 +192,6 @@ void GameTreeRep::SetPlayer(GameInfoset p_infoset, GamePlayer p_player)
193192
p_player->m_infosets.push_back(p_infoset);
194193

195194
ClearComputedValues();
196-
Canonicalize();
197195
}
198196

199197
bool GameInfosetRep::Precedes(GameNode p_node) const
@@ -237,7 +235,6 @@ GameAction GameTreeRep::InsertAction(GameInfoset p_infoset, GameAction p_action
237235
m_numNodes += p_infoset->m_members.size();
238236
// m_numNonterminalNodes stays unchanged when an action is appended to an information set
239237
ClearComputedValues();
240-
Canonicalize();
241238
return action;
242239
}
243240

@@ -251,10 +248,7 @@ void GameTreeRep::RemoveMember(GameInfosetRep *p_infoset, GameNodeRep *p_node)
251248
p_infoset->Invalidate();
252249
p_infoset->m_player->m_infosets.erase(std::find(
253250
player->m_infosets.begin(), player->m_infosets.end(), p_infoset->shared_from_this()));
254-
int iset = 1;
255-
for (auto &infoset : player->m_infosets) {
256-
infoset->m_number = iset++;
257-
}
251+
RenumberInfosets(player);
258252
}
259253
}
260254

@@ -284,7 +278,6 @@ void GameTreeRep::Reveal(GameInfoset p_atInfoset, GamePlayer p_player)
284278
}
285279

286280
ClearComputedValues();
287-
Canonicalize();
288281
}
289282

290283
//========================================================================
@@ -422,7 +415,6 @@ void GameTreeRep::DeleteParent(GameNode p_node)
422415

423416
oldParent->Invalidate();
424417
ClearComputedValues();
425-
Canonicalize();
426418
}
427419

428420
void GameTreeRep::DeleteTree(GameNode p_node)
@@ -449,7 +441,6 @@ void GameTreeRep::DeleteTree(GameNode p_node)
449441
node->m_label = "";
450442

451443
ClearComputedValues();
452-
Canonicalize();
453444
}
454445

455446
void GameTreeRep::CopySubtree(GameNodeRep *dest, GameNodeRep *src, GameNodeRep *stop)
@@ -491,7 +482,6 @@ void GameTreeRep::CopyTree(GameNode p_dest, GameNode p_src)
491482
CopySubtree(dest_child->get(), src_child->get(), dest);
492483
}
493484
ClearComputedValues();
494-
Canonicalize();
495485
}
496486
}
497487

@@ -515,7 +505,6 @@ void GameTreeRep::MoveTree(GameNode p_dest, GameNode p_src)
515505
dest->m_outcome = nullptr;
516506

517507
ClearComputedValues();
518-
Canonicalize();
519508
}
520509

521510
Game GameTreeRep::CopySubgame(GameNode p_root) const
@@ -547,7 +536,6 @@ void GameTreeRep::SetInfoset(GameNode p_node, GameInfoset p_infoset)
547536
node->m_infoset = p_infoset.get();
548537

549538
ClearComputedValues();
550-
Canonicalize();
551539
}
552540

553541
GameInfoset GameTreeRep::LeaveInfoset(GameNode p_node)
@@ -578,7 +566,6 @@ GameInfoset GameTreeRep::LeaveInfoset(GameNode p_node)
578566
(*new_act)->SetLabel((*old_act)->GetLabel());
579567
}
580568
ClearComputedValues();
581-
Canonicalize();
582569
return node->m_infoset->shared_from_this();
583570
}
584571

@@ -619,7 +606,6 @@ GameInfoset GameTreeRep::AppendMove(GameNode p_node, GameInfoset p_infoset)
619606
});
620607
m_numNonterminalNodes++;
621608
ClearComputedValues();
622-
Canonicalize();
623609
return node->m_infoset->shared_from_this();
624610
}
625611

@@ -671,7 +657,6 @@ GameInfoset GameTreeRep::InsertMove(GameNode p_node, GameInfoset p_infoset)
671657
m_numNodes += newNode->m_infoset->m_actions.size();
672658
m_numNonterminalNodes++;
673659
ClearComputedValues();
674-
Canonicalize();
675660
return p_infoset;
676661
}
677662

@@ -769,64 +754,39 @@ bool GameTreeRep::IsPerfectRecall() const
769754
// GameTreeRep: Managing the representation
770755
//------------------------------------------------------------------------
771756

772-
void GameTreeRep::NumberNodes(GameNodeRep *n, int &index)
757+
void GameTreeRep::SortInfosets(GamePlayerRep *p_player)
773758
{
774-
n->m_number = index++;
775-
for (auto &child : n->m_children) {
776-
NumberNodes(child.get(), index);
759+
// Sort nodes within information sets according to ID.
760+
for (auto &infoset : p_player->m_infosets) {
761+
std::sort(infoset->m_members.begin(), infoset->m_members.end(),
762+
[](const std::shared_ptr<GameNodeRep> &a, const std::shared_ptr<GameNodeRep> &b) {
763+
return a->m_number < b->m_number;
764+
});
777765
}
766+
// Sort information sets by the smallest ID among their members
767+
std::sort(
768+
p_player->m_infosets.begin(), p_player->m_infosets.end(),
769+
[](const std::shared_ptr<GameInfosetRep> &a, const std::shared_ptr<GameInfosetRep> &b) {
770+
return a->m_members.front()->m_number < b->m_members.front()->m_number;
771+
});
772+
RenumberInfosets(p_player);
773+
}
774+
void GameTreeRep::RenumberInfosets(GamePlayerRep *p_player)
775+
{
776+
std::for_each(
777+
p_player->m_infosets.begin(), p_player->m_infosets.end(),
778+
[iset = 1](const std::shared_ptr<GameInfosetRep> &s) mutable { s->m_number = iset++; });
778779
}
779780

780-
void GameTreeRep::Canonicalize()
781+
void GameTreeRep::SortInfosets()
781782
{
782-
if (!m_doCanon) {
783-
return;
784-
}
785783
int nodeindex = 1;
786-
NumberNodes(m_root.get(), nodeindex);
787-
788-
for (size_t pl = 0; pl <= m_players.size(); pl++) {
789-
auto player = (pl) ? m_players[pl - 1].get() : m_chance.get();
790-
791-
// Sort nodes within information sets according to ID.
792-
// Coded using a bubble sort for simplicity; large games might
793-
// find a quicksort worthwhile.
794-
for (auto &infoset : player->m_infosets) {
795-
for (size_t i = 1; i < infoset->m_members.size(); i++) {
796-
for (size_t j = 1; j < infoset->m_members.size() - i; j++) {
797-
if (infoset->m_members[j]->m_number < infoset->m_members[j - 1]->m_number) {
798-
auto tmp = infoset->m_members[j - 1];
799-
infoset->m_members[j - 1] = infoset->m_members[j];
800-
infoset->m_members[j] = tmp;
801-
}
802-
}
803-
}
804-
}
805-
806-
// Sort information sets by the smallest ID among their members
807-
// Coded using a bubble sort for simplicity; large games might
808-
// find a quicksort worthwhile.
809-
for (size_t i = 1; i < player->m_infosets.size(); i++) {
810-
for (size_t j = 1; j < player->m_infosets.size() - i; j++) {
811-
const int a = ((player->m_infosets[j]->m_members.size())
812-
? player->m_infosets[j]->m_members[0]->m_number
813-
: 0);
814-
const int b = ((player->m_infosets[j - 1]->m_members.size())
815-
? player->m_infosets[j - 1]->m_members[0]->m_number
816-
: 0);
817-
818-
if (a < b || b == 0) {
819-
auto tmp = player->m_infosets[j - 1];
820-
player->m_infosets[j - 1] = player->m_infosets[j];
821-
player->m_infosets[j] = tmp;
822-
}
823-
}
824-
}
825-
826-
// Reassign information set IDs
827-
std::for_each(
828-
player->m_infosets.begin(), player->m_infosets.end(),
829-
[iset = 1](const std::shared_ptr<GameInfosetRep> &s) mutable { s->m_number = iset++; });
784+
for (const auto &node : GetNodes()) {
785+
node->m_number = nodeindex++;
786+
}
787+
SortInfosets(m_chance.get());
788+
for (auto player : m_players) {
789+
SortInfosets(player.get());
830790
}
831791
}
832792

@@ -848,7 +808,7 @@ void GameTreeRep::BuildComputedValues() const
848808
if (m_computedValues) {
849809
return;
850810
}
851-
const_cast<GameTreeRep *>(this)->Canonicalize();
811+
const_cast<GameTreeRep *>(this)->SortInfosets();
852812
for (const auto &player : m_players) {
853813
std::map<GameInfosetRep *, int> behav;
854814
std::map<GameNodeRep *, GameNodeRep *> ptr, whichbranch;

src/games/gametree.h

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class GameTreeRep : public GameExplicitRep {
3333
friend class GameActionRep;
3434

3535
protected:
36-
mutable bool m_computedValues{false}, m_doCanon{true};
36+
mutable bool m_computedValues{false};
3737
std::shared_ptr<GameNodeRep> m_root;
3838
std::shared_ptr<GamePlayerRep> m_chance;
3939
std::size_t m_numNodes = 1;
@@ -43,14 +43,14 @@ class GameTreeRep : public GameExplicitRep {
4343

4444
/// @name Private auxiliary functions
4545
//@{
46-
void NumberNodes(GameNodeRep *, int &);
46+
void SortInfosets(GamePlayerRep *);
47+
static void RenumberInfosets(GamePlayerRep *);
4748
/// Normalize the probability distribution of actions at a chance node
4849
Game NormalizeChanceProbs(GameInfosetRep *);
4950
//@}
5051

5152
/// @name Managing the representation
5253
//@{
53-
void Canonicalize();
5454
void BuildComputedValues() const override;
5555
void BuildConsistentPlays();
5656
void ClearComputedValues() const;
@@ -74,14 +74,6 @@ class GameTreeRep : public GameExplicitRep {
7474
bool IsTree() const override { return true; }
7575
bool IsConstSum() const override;
7676
bool IsPerfectRecall() const override;
77-
/// Turn on or off automatic canonicalization of the game
78-
void SetCanonicalization(bool p_doCanon) const
79-
{
80-
m_doCanon = p_doCanon;
81-
if (m_doCanon) {
82-
const_cast<GameTreeRep *>(this)->Canonicalize();
83-
}
84-
}
8577
//@}
8678

8779
/// @name Players
@@ -122,6 +114,8 @@ class GameTreeRep : public GameExplicitRep {
122114
GameInfoset GetInfoset(int iset) const override;
123115
/// Returns the set of information sets in the game
124116
std::vector<GameInfoset> GetInfosets() const override;
117+
/// Sort the information sets for each player in a canonical order
118+
void SortInfosets() override;
125119
//@}
126120

127121
/// @name Modification

0 commit comments

Comments
 (0)