Skip to content

Commit 059c316

Browse files
authored
Refactor tree layout from GUI and expose in Python (#631)
* Refactors the layout algorithm for trees to be independent of UI library, move to `games/` * Simple exposure of output of the algorithm in Python. (For the moment, this is undocumented.) Closes #596
1 parent 7a94914 commit 059c316

9 files changed

Lines changed: 233 additions & 101 deletions

File tree

Makefile.am

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,8 @@ game_SOURCES = \
313313
src/games/file.cc \
314314
src/games/writer.cc \
315315
src/games/writer.h \
316+
src/games/layout.cc \
317+
src/games/layout.h \
316318
${agg_SOURCES} \
317319
src/games/nash.h
318320

src/games/layout.cc

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//
2+
// This file is part of Gambit
3+
// Copyright (c) 1994-2025, The Gambit Project (https://www.gambit-project.org)
4+
//
5+
// FILE: src/gui/efglayout.cc
6+
// Implementation of tree layout representation
7+
//
8+
// This program is free software; you can redistribute it and/or modify
9+
// it under the terms of the GNU General Public License as published by
10+
// the Free Software Foundation; either version 2 of the License, or
11+
// (at your option) any later version.
12+
//
13+
// This program is distributed in the hope that it will be useful,
14+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
// GNU General Public License for more details.
17+
//
18+
// You should have received a copy of the GNU General Public License
19+
// along with this program; if not, write to the Free Software
20+
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
21+
//
22+
23+
#include "layout.h"
24+
25+
namespace Gambit {
26+
27+
void Layout::LayoutSubtree(const GameNode &p_node, const BehaviorSupportProfile &p_support,
28+
int p_level, double &p_offset)
29+
{
30+
const auto entry = std::make_shared<LayoutEntry>(p_level);
31+
m_nodeMap[p_node] = entry;
32+
try {
33+
entry->m_sublevel = m_infosetSublevels.at({entry->m_level, p_node->GetInfoset()});
34+
}
35+
catch (std::out_of_range &) {
36+
if (p_level - 1 < static_cast<int>(m_numSublevels.size())) {
37+
m_numSublevels.push_back(0);
38+
}
39+
entry->m_sublevel = ++m_numSublevels[p_level];
40+
m_infosetSublevels[{entry->m_level, p_node->GetInfoset()}] = entry->m_sublevel;
41+
}
42+
43+
if (p_node->IsTerminal()) {
44+
entry->m_offset = p_offset;
45+
p_offset += 1;
46+
return;
47+
}
48+
if (p_node->GetInfoset() && !p_node->GetInfoset()->GetPlayer()->IsChance()) {
49+
const auto actions = p_support.GetActions(p_node->GetInfoset());
50+
for (const auto &action : actions) {
51+
LayoutSubtree(p_node->GetChild(action), p_support, p_level + 1, p_offset);
52+
}
53+
entry->m_offset = (m_nodeMap.at(p_node->GetChild(actions.front()))->m_offset +
54+
m_nodeMap.at(p_node->GetChild(actions.back()))->m_offset) /
55+
2;
56+
}
57+
else {
58+
for (const auto &child : p_node->GetChildren()) {
59+
LayoutSubtree(child, p_support, p_level + 1, p_offset);
60+
}
61+
entry->m_offset = (m_nodeMap.at(p_node->GetChildren().front())->m_offset +
62+
m_nodeMap.at(p_node->GetChildren().back())->m_offset) /
63+
2;
64+
}
65+
}
66+
67+
void Layout::LayoutTree(const BehaviorSupportProfile &p_support)
68+
{
69+
m_nodeMap.clear();
70+
m_numSublevels.clear();
71+
m_infosetSublevels.clear();
72+
73+
m_maxOffset = 0;
74+
LayoutSubtree(m_game->GetRoot(), p_support, 0, m_maxOffset);
75+
}
76+
77+
} // namespace Gambit

src/games/layout.h

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//
2+
// This file is part of Gambit
3+
// Copyright (c) 1994-2025, The Gambit Project (https://www.gambit-project.org)
4+
//
5+
// FILE: src/games/layout.h
6+
// Interface to generic tree layout representation
7+
//
8+
// This program is free software; you can redistribute it and/or modify
9+
// it under the terms of the GNU General Public License as published by
10+
// the Free Software Foundation; either version 2 of the License, or
11+
// (at your option) any later version.
12+
//
13+
// This program is distributed in the hope that it will be useful,
14+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
// GNU General Public License for more details.
17+
//
18+
// You should have received a copy of the GNU General Public License
19+
// along with this program; if not, write to the Free Software
20+
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
21+
//
22+
23+
#ifndef GAMBIT_GAMES_TREELAYOUT_H
24+
#define GAMBIT_GAMES_TREELAYOUT_H
25+
26+
#include <map>
27+
28+
#include "gambit.h"
29+
#include "game.h"
30+
31+
namespace Gambit {
32+
33+
struct LayoutEntry {
34+
friend class Layout;
35+
double m_offset{-1}; // Cartesian coordinates of node
36+
int m_level, m_sublevel{0}; // depth of the node in tree
37+
bool m_inSupport{true};
38+
39+
explicit LayoutEntry(int p_level) : m_level(p_level) {}
40+
};
41+
42+
class Layout {
43+
Game m_game;
44+
std::map<GameNode, std::shared_ptr<LayoutEntry>> m_nodeMap;
45+
std::vector<int> m_numSublevels;
46+
std::map<std::pair<int, GameInfoset>, int> m_infosetSublevels;
47+
48+
double m_maxOffset{0};
49+
50+
void LayoutSubtree(const GameNode &, const BehaviorSupportProfile &, int, double &);
51+
52+
public:
53+
explicit Layout(const Game &p_game) : m_game(p_game) {}
54+
~Layout() = default;
55+
56+
void LayoutTree(const BehaviorSupportProfile &);
57+
58+
const std::map<GameNode, std::shared_ptr<LayoutEntry>> &GetNodeMap() const { return m_nodeMap; }
59+
int GetNodeLevel(const GameNode &p_node) const { return m_nodeMap.at(p_node)->m_level; }
60+
int GetNodeSublevel(const GameNode &p_node) const { return m_nodeMap.at(p_node)->m_sublevel; }
61+
double GetNodeOffset(const GameNode &p_node) const { return m_nodeMap.at(p_node)->m_offset; }
62+
63+
const std::vector<int> &GetNumSublevels() const { return m_numSublevels; }
64+
double GetMinOffset() const { return 0; }
65+
double GetMaxOffset() const { return m_maxOffset; }
66+
};
67+
68+
inline std::shared_ptr<Layout> CreateLayout(const Game &p_game)
69+
{
70+
auto layout = std::make_shared<Layout>(p_game);
71+
layout->LayoutTree(BehaviorSupportProfile(p_game));
72+
return layout;
73+
}
74+
75+
} // namespace Gambit
76+
77+
#endif // GAMBIT_GAMES_TREELAYOUT_H

src/gui/dlefglayout.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ LayoutNodesPanel::LayoutNodesPanel(wxWindow *p_parent, const TreeRenderConfig &p
115115
constexpr int Y_SPACING_MAX = 60;
116116

117117
m_terminalSpacing = new wxSpinCtrl(
118-
this, wxID_ANY, wxString::Format(_T("%d"), p_settings.TerminalSpacing()), wxDefaultPosition,
119-
wxDefaultSize, wxSP_ARROW_KEYS, Y_SPACING_MIN, Y_SPACING_MAX);
118+
this, wxID_ANY, wxString::Format(_T("%d"), p_settings.GetTerminalSpacing()),
119+
wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, Y_SPACING_MIN, Y_SPACING_MAX);
120120
gridSizer->Add(m_terminalSpacing, 1, wxEXPAND | wxALL, 5);
121121

122122
sizeSizer->Add(gridSizer, 1, wxALL | wxEXPAND, 5);

src/gui/efglayout.cc

Lines changed: 31 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@
2929
#include <wx/wx.h>
3030
#endif // WX_PRECOMP
3131

32-
#include "gambit.h"
33-
#include "efgdisplay.h"
32+
#include "efglayout.h"
3433

3534
namespace Gambit::GUI {
3635
namespace {
@@ -375,14 +374,15 @@ GameNode TreeLayout::BranchBelowHitTest(int p_x, int p_y) const
375374
bool TreeLayout::InfosetHitTest(const std::shared_ptr<NodeEntry> &p_entry, const int p_x,
376375
const int p_y) const
377376
{
378-
if (p_entry->GetNextMember() && p_entry->GetNode()->GetInfoset()) {
377+
auto nextMember = ComputeNextInInfoset(p_entry);
378+
if (nextMember && p_entry->GetNode()->GetInfoset()) {
379379
if (p_x > p_entry->m_x + p_entry->GetSublevel() * m_infosetSpacing - 2 &&
380380
p_x < p_entry->m_x + p_entry->GetSublevel() * m_infosetSpacing + 2) {
381-
if (p_y > p_entry->m_y && p_y < p_entry->GetNextMember()->m_y) {
381+
if (p_y > p_entry->m_y && p_y < nextMember->m_y) {
382382
// next infoset is below this one
383383
return true;
384384
}
385-
if (p_y > p_entry->GetNextMember()->m_y && p_y < p_entry->m_y) {
385+
if (p_y > nextMember->m_y && p_y < p_entry->m_y) {
386386
// next infoset is above this one
387387
return true;
388388
}
@@ -557,43 +557,8 @@ GameNode TreeLayout::NextSameLevel(const GameNode &p_node) const
557557
return nullptr;
558558
}
559559

560-
void TreeLayout::ComputeOffsets(const GameNode &p_node, const BehaviorSupportProfile &p_support,
561-
int &p_ycoord)
562-
{
563-
const auto &settings = m_doc->GetStyle();
564-
565-
const auto entry = GetNodeEntry(p_node);
566-
if (m_doc->GetStyle().RootReachable() && p_node->GetInfoset() &&
567-
!p_node->GetInfoset()->GetPlayer()->IsChance()) {
568-
const auto actions = p_support.GetActions(p_node->GetInfoset());
569-
for (const auto &action : actions) {
570-
ComputeOffsets(p_node->GetChild(action), p_support, p_ycoord);
571-
}
572-
entry->m_y = (GetNodeEntry(p_node->GetChild(actions.front()))->m_y +
573-
GetNodeEntry(p_node->GetChild(actions.back()))->m_y) /
574-
2;
575-
}
576-
else if (!p_node->IsTerminal()) {
577-
const auto actions = p_node->GetInfoset()->GetActions();
578-
for (const auto &action : actions) {
579-
const auto child = p_node->GetChild(action);
580-
ComputeOffsets(child, p_support, p_ycoord);
581-
if (!p_node->GetPlayer()->IsChance() && !p_support.Contains(action)) {
582-
GetNodeEntry(child)->m_inSupport = false;
583-
}
584-
}
585-
entry->m_y = (GetNodeEntry(p_node->GetChild(actions.front()))->m_y +
586-
GetNodeEntry(p_node->GetChild(actions.back()))->m_y) /
587-
2;
588-
}
589-
else {
590-
entry->m_y = p_ycoord;
591-
p_ycoord += settings.TerminalSpacing();
592-
}
593-
}
594-
595560
std::shared_ptr<NodeEntry>
596-
TreeLayout::ComputeNextInInfoset(const std::shared_ptr<NodeEntry> &p_entry)
561+
TreeLayout::ComputeNextInInfoset(const std::shared_ptr<NodeEntry> &p_entry) const
597562
{
598563
const auto infoset = p_entry->m_node->GetInfoset();
599564
if (!infoset) {
@@ -620,22 +585,11 @@ TreeLayout::ComputeNextInInfoset(const std::shared_ptr<NodeEntry> &p_entry)
620585
return nullptr;
621586
}
622587

623-
void TreeLayout::ComputeSublevel(const std::shared_ptr<NodeEntry> &p_entry)
624-
{
625-
try {
626-
p_entry->m_sublevel = m_infosetSublevels.at({p_entry->m_level, p_entry->m_node->GetInfoset()});
627-
}
628-
catch (std::out_of_range &) {
629-
p_entry->m_sublevel = ++m_numSublevels[p_entry->m_level];
630-
m_infosetSublevels[{p_entry->m_level, p_entry->m_node->GetInfoset()}] = p_entry->m_sublevel;
631-
}
632-
p_entry->m_nextMember = ComputeNextInInfoset(p_entry);
633-
}
634-
635-
void TreeLayout::ComputeNodeDepths() const
588+
void TreeLayout::ComputeNodeDepths(const Gambit::Layout &p_layout) const
636589
{
637-
std::vector<int> aggregateSublevels(m_maxLevel + 1);
638-
std::partial_sum(m_numSublevels.cbegin(), m_numSublevels.cend(), aggregateSublevels.begin());
590+
std::vector<int> aggregateSublevels;
591+
std::partial_sum(p_layout.GetNumSublevels().cbegin(), p_layout.GetNumSublevels().cend(),
592+
std::back_inserter(aggregateSublevels));
639593
m_maxX = 0;
640594
for (const auto &entry : m_nodeList) {
641595
entry->m_x = c_leftMargin + entry->m_level * m_doc->GetStyle().GetNodeLevelLength();
@@ -654,43 +608,39 @@ void TreeLayout::ComputeRenderedParents() const
654608
}
655609
}
656610

657-
void TreeLayout::BuildNodeList(const GameNode &p_node, const BehaviorSupportProfile &p_support,
658-
const int p_level)
611+
void TreeLayout::BuildNodeList(const GameNode &p_node, const BehaviorSupportProfile &p_support)
659612
{
660613
const auto entry = std::make_shared<NodeEntry>(p_node);
661614
m_nodeList.push_back(entry);
662615
m_nodeMap[p_node] = entry;
663616
entry->m_size = m_doc->GetStyle().GetNodeSize();
664617
entry->m_branchLength = m_doc->GetStyle().GetBranchLength();
665-
entry->m_level = p_level;
666618
if (m_doc->GetStyle().RootReachable()) {
667619
if (const GameInfoset infoset = p_node->GetInfoset()) {
668620
if (infoset->GetPlayer()->IsChance()) {
669621
for (const auto &child : p_node->GetChildren()) {
670-
BuildNodeList(child, p_support, p_level + 1);
622+
BuildNodeList(child, p_support);
671623
}
672624
}
673625
else {
674626
for (const auto &action : p_support.GetActions(infoset)) {
675-
BuildNodeList(p_node->GetChild(action), p_support, p_level + 1);
627+
BuildNodeList(p_node->GetChild(action), p_support);
676628
}
677629
}
678630
}
679631
}
680632
else {
681633
for (const auto &child : p_node->GetChildren()) {
682-
BuildNodeList(child, p_support, p_level + 1);
634+
BuildNodeList(child, p_support);
683635
}
684636
}
685-
m_maxLevel = std::max(p_level, m_maxLevel);
686637
}
687638

688639
void TreeLayout::BuildNodeList(const BehaviorSupportProfile &p_support)
689640
{
690641
m_nodeList.clear();
691642
m_nodeMap.clear();
692-
m_maxLevel = 0;
693-
BuildNodeList(m_doc->GetGame()->GetRoot(), p_support, 0);
643+
BuildNodeList(m_doc->GetGame()->GetRoot(), p_support);
694644
}
695645

696646
void TreeLayout::Layout(const BehaviorSupportProfile &p_support)
@@ -703,16 +653,18 @@ void TreeLayout::Layout(const BehaviorSupportProfile &p_support)
703653
BuildNodeList(p_support);
704654
}
705655

706-
int maxy = c_topMargin;
707-
ComputeOffsets(m_doc->GetGame()->GetRoot(), p_support, maxy);
708-
m_maxY = maxy + c_bottomMargin;
656+
auto layout = Gambit::Layout(m_doc->GetGame());
657+
layout.LayoutTree(p_support);
709658

710-
m_infosetSublevels.clear();
711-
m_numSublevels = std::vector<int>(m_maxLevel + 1);
712-
for (auto entry : m_nodeList) {
713-
ComputeSublevel(entry);
659+
const auto spacing = m_doc->GetStyle().GetTerminalSpacing();
660+
for (auto [node, entry] : layout.GetNodeMap()) {
661+
m_nodeMap[node]->m_level = entry->m_level;
662+
m_nodeMap[node]->m_sublevel = entry->m_sublevel;
663+
m_nodeMap[node]->m_y = entry->m_offset * spacing + c_topMargin;
714664
}
715-
ComputeNodeDepths();
665+
m_maxY =
666+
c_topMargin + c_bottomMargin + spacing * (layout.GetMaxOffset() - layout.GetMinOffset());
667+
ComputeNodeDepths(layout);
716668

717669
ComputeRenderedParents();
718670
GenerateLabels();
@@ -773,9 +725,9 @@ void TreeLayout::RenderSubtree(wxDC &p_dc, bool p_noHints) const
773725
if (entry->GetChildNumber() == 1) {
774726
DrawNode(p_dc, parentEntry, m_doc->GetSelectNode(), p_noHints);
775727

776-
if (parentEntry->GetNextMember()) {
777-
const int nextX = parentEntry->GetNextMember()->m_x;
778-
const int nextY = parentEntry->GetNextMember()->m_y;
728+
if (auto nextMember = ComputeNextInInfoset(parentEntry)) {
729+
const int nextX = nextMember->m_x;
730+
const int nextY = nextMember->m_y;
779731

780732
if (parentEntry->m_x == nextX) {
781733
#ifdef __WXGTK__
@@ -792,17 +744,15 @@ void TreeLayout::RenderSubtree(wxDC &p_dc, bool p_noHints) const
792744
parentEntry->m_x + parentEntry->GetSize(), nextY);
793745
}
794746

795-
if (parentEntry->GetNextMember()->m_x != parentEntry->m_x) {
747+
if (nextMember->m_x != parentEntry->m_x) {
796748
// Draw a little arrow in the direction of the iset.
797749
int startX, endX;
798750
if (settings.GetInfosetJoin() == GBT_INFOSET_JOIN_LINES) {
799751
startX = parentEntry->m_x;
800-
endX =
801-
(startX + m_infosetSpacing *
802-
((parentEntry->GetNextMember()->m_x > parentEntry->m_x) ? 1 : -1));
752+
endX = (startX + m_infosetSpacing * ((nextMember->m_x > parentEntry->m_x) ? 1 : -1));
803753
}
804754
else {
805-
if (parentEntry->GetNextMember()->m_x < parentEntry->m_x) {
755+
if (nextMember->m_x < parentEntry->m_x) {
806756
// information set is continued to the left
807757
startX = parentEntry->m_x + parentEntry->GetSize();
808758
endX = parentEntry->m_x - m_infosetSpacing;

0 commit comments

Comments
 (0)