From 7b3442dd092a556acc486547f7022b77e0d7cb68 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 27 Oct 2025 16:20:26 +0000 Subject: [PATCH 01/11] Wrote simple layout abstraction that at least compiles... --- Makefile.am | 2 ++ src/gui/layout.cc | 78 +++++++++++++++++++++++++++++++++++++++++++++++ src/gui/layout.h | 62 +++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 src/gui/layout.cc create mode 100644 src/gui/layout.h diff --git a/Makefile.am b/Makefile.am index 2a19386a9..c1c823596 100644 --- a/Makefile.am +++ b/Makefile.am @@ -589,6 +589,8 @@ gambit_SOURCES = \ src/gui/gamedoc.h \ src/gui/gameframe.cc \ src/gui/gameframe.h \ + src/gui/layout.cc \ + src/gui/layout.h \ src/gui/menuconst.h \ src/gui/nfgpanel.cc \ src/gui/nfgpanel.h \ diff --git a/src/gui/layout.cc b/src/gui/layout.cc new file mode 100644 index 000000000..5328c432b --- /dev/null +++ b/src/gui/layout.cc @@ -0,0 +1,78 @@ +// +// This file is part of Gambit +// Copyright (c) 1994-2025, The Gambit Project (https://www.gambit-project.org) +// +// FILE: src/gui/efglayout.cc +// Implementation of tree layout representation +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include // for std::min, std::max +#include // for std::partial_sum + +#include "layout.h" + +namespace Gambit { + +void Layout::LayoutSubtree(const GameNode &p_node, const BehaviorSupportProfile &p_support, + int p_level, double &p_offset) +{ + const auto entry = std::make_shared(p_level); + m_nodeMap[p_node] = entry; + try { + entry->m_sublevel = m_infosetSublevels.at({entry->m_level, p_node->GetInfoset()}); + } + catch (std::out_of_range &) { + entry->m_sublevel = ++m_numSublevels[entry->m_level]; + m_infosetSublevels[{entry->m_level, p_node->GetInfoset()}] = entry->m_sublevel; + } + + if (p_node->IsTerminal()) { + entry->m_offset = p_offset; + p_offset += 1; + return; + } + if (p_node->GetInfoset() && !p_node->GetInfoset()->GetPlayer()->IsChance()) { + const auto actions = p_support.GetActions(p_node->GetInfoset()); + for (const auto &action : actions) { + LayoutSubtree(p_node->GetChild(action), p_support, p_level + 1, p_offset); + } + entry->m_offset = (m_nodeMap.at(p_node->GetChild(actions.front()))->m_offset + + m_nodeMap.at(p_node->GetChild(actions.back()))->m_offset) / + 2; + } + else { + for (const auto &child : p_node->GetChildren()) { + LayoutSubtree(child, p_support, p_level + 1, p_offset); + } + entry->m_offset = (m_nodeMap.at(p_node->GetChildren().front())->m_offset + + m_nodeMap.at(p_node->GetChildren().back())->m_offset) / + 2; + } +} + +void Layout::LayoutTree(const BehaviorSupportProfile &p_support) +{ + m_nodeMap.clear(); + m_maxLevel = 0; + m_numSublevels.clear(); + m_infosetSublevels.clear(); + + double ycoord = 0; + LayoutSubtree(m_game->GetRoot(), p_support, 0, ycoord); +} + +} // namespace Gambit diff --git a/src/gui/layout.h b/src/gui/layout.h new file mode 100644 index 000000000..cacf85347 --- /dev/null +++ b/src/gui/layout.h @@ -0,0 +1,62 @@ +// +// This file is part of Gambit +// Copyright (c) 1994-2025, The Gambit Project (https://www.gambit-project.org) +// +// FILE: src/gui/efglayout.h +// Interface to tree layout representation +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef GAMBIT_LAYOUT_H +#define GAMBIT_LAYOUT_H + +#include "gambit.h" + +#include + +namespace Gambit { +struct LayoutEntry { + friend class Layout; + double m_offset{-1}; // Cartesian coordinates of node + int m_level, m_sublevel{0}; // depth of the node in tree + bool m_inSupport{true}; + + explicit LayoutEntry(int p_level) : m_level(p_level) {} +}; + +class Layout { + Game m_game; + std::map> m_nodeMap; + std::vector m_numSublevels; + std::map, int> m_infosetSublevels; + + float m_maxOffset{0}; + int m_maxLevel{0}; + + void LayoutSubtree(const GameNode &, const BehaviorSupportProfile &, int, double &); + +public: + explicit Layout(const Game &p_game) : m_game(p_game) {} + ~Layout() = default; + + void LayoutTree(const BehaviorSupportProfile &); + + const std::map> &GetNodeMap() const { return m_nodeMap; } + float GetMinOffset() const { return 0; } + float GetMaxOffset() const { return m_maxOffset; } +}; +} // namespace Gambit +#endif // GAMBIT_LAYOUT_H From cd567192a2a9e5b6470f55895ef020056c238e52 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 27 Oct 2025 16:50:43 +0000 Subject: [PATCH 02/11] Layout of levels using new abstraction seems to work. --- src/gui/efglayout.cc | 19 ++++++++++++++++--- src/gui/efglayout.h | 4 +++- src/gui/layout.cc | 7 ++++++- src/gui/layout.h | 1 + 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/gui/efglayout.cc b/src/gui/efglayout.cc index 8e1dd105d..2f693fb80 100644 --- a/src/gui/efglayout.cc +++ b/src/gui/efglayout.cc @@ -32,6 +32,8 @@ #include "gambit.h" #include "efgdisplay.h" +#include "layout.h" + namespace Gambit::GUI { namespace { @@ -622,6 +624,7 @@ TreeLayout::ComputeNextInInfoset(const std::shared_ptr &p_entry) void TreeLayout::ComputeSublevel(const std::shared_ptr &p_entry) { + /* try { p_entry->m_sublevel = m_infosetSublevels.at({p_entry->m_level, p_entry->m_node->GetInfoset()}); } @@ -629,13 +632,15 @@ void TreeLayout::ComputeSublevel(const std::shared_ptr &p_entry) p_entry->m_sublevel = ++m_numSublevels[p_entry->m_level]; m_infosetSublevels[{p_entry->m_level, p_entry->m_node->GetInfoset()}] = p_entry->m_sublevel; } + */ p_entry->m_nextMember = ComputeNextInInfoset(p_entry); } -void TreeLayout::ComputeNodeDepths() const +void TreeLayout::ComputeNodeDepths(const Gambit::Layout &p_layout) const { std::vector aggregateSublevels(m_maxLevel + 1); - std::partial_sum(m_numSublevels.cbegin(), m_numSublevels.cend(), aggregateSublevels.begin()); + std::partial_sum(p_layout.GetNumSublevels().cbegin(), p_layout.GetNumSublevels().cend(), + aggregateSublevels.begin()); m_maxX = 0; for (const auto &entry : m_nodeList) { entry->m_x = c_leftMargin + entry->m_level * m_doc->GetStyle().GetNodeLevelLength(); @@ -707,12 +712,20 @@ void TreeLayout::Layout(const BehaviorSupportProfile &p_support) ComputeOffsets(m_doc->GetGame()->GetRoot(), p_support, maxy); m_maxY = maxy + c_bottomMargin; + auto layout = Gambit::Layout(m_doc->GetGame()); + layout.LayoutTree(p_support); + + for (auto [node, entry] : layout.GetNodeMap()) { + m_nodeMap[node]->m_level = entry->m_level; + m_nodeMap[node]->m_sublevel = entry->m_sublevel; + } + m_infosetSublevels.clear(); m_numSublevels = std::vector(m_maxLevel + 1); for (auto entry : m_nodeList) { ComputeSublevel(entry); } - ComputeNodeDepths(); + ComputeNodeDepths(layout); ComputeRenderedParents(); GenerateLabels(); diff --git a/src/gui/efglayout.h b/src/gui/efglayout.h index 2873fc5a8..42a148232 100644 --- a/src/gui/efglayout.h +++ b/src/gui/efglayout.h @@ -26,6 +26,8 @@ #include "gambit.h" #include "gamedoc.h" +#include "layout.h" + namespace Gambit::GUI { class NodeEntry { friend class TreeLayout; @@ -124,7 +126,7 @@ class TreeLayout final : public GameView { void ComputeOffsets(const GameNode &, const BehaviorSupportProfile &, int &); /// Based on node levels and information set sublevels, compute the depth /// (X coordinate) of all nodes - void ComputeNodeDepths() const; + void ComputeNodeDepths(const Gambit::Layout &) const; void ComputeRenderedParents() const; wxString CreateNodeLabel(const std::shared_ptr &, int) const; diff --git a/src/gui/layout.cc b/src/gui/layout.cc index 5328c432b..cc11432b9 100644 --- a/src/gui/layout.cc +++ b/src/gui/layout.cc @@ -25,6 +25,8 @@ #include "layout.h" +#include <__ostream/basic_ostream.h> + namespace Gambit { void Layout::LayoutSubtree(const GameNode &p_node, const BehaviorSupportProfile &p_support, @@ -36,7 +38,10 @@ void Layout::LayoutSubtree(const GameNode &p_node, const BehaviorSupportProfile entry->m_sublevel = m_infosetSublevels.at({entry->m_level, p_node->GetInfoset()}); } catch (std::out_of_range &) { - entry->m_sublevel = ++m_numSublevels[entry->m_level]; + if (p_level - 1 < static_cast(m_numSublevels.size())) { + m_numSublevels.push_back(0); + } + entry->m_sublevel = ++m_numSublevels[p_level]; m_infosetSublevels[{entry->m_level, p_node->GetInfoset()}] = entry->m_sublevel; } diff --git a/src/gui/layout.h b/src/gui/layout.h index cacf85347..9cc7ee7eb 100644 --- a/src/gui/layout.h +++ b/src/gui/layout.h @@ -55,6 +55,7 @@ class Layout { void LayoutTree(const BehaviorSupportProfile &); const std::map> &GetNodeMap() const { return m_nodeMap; } + const std::vector &GetNumSublevels() const { return m_numSublevels; } float GetMinOffset() const { return 0; } float GetMaxOffset() const { return m_maxOffset; } }; From 2b9e8721b6e3f4fbebbf6166edd880350cae41e1 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 27 Oct 2025 16:51:30 +0000 Subject: [PATCH 03/11] Remove infoset depths from GUI class. --- src/gui/efglayout.cc | 2 -- src/gui/efglayout.h | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/gui/efglayout.cc b/src/gui/efglayout.cc index 2f693fb80..578d03df3 100644 --- a/src/gui/efglayout.cc +++ b/src/gui/efglayout.cc @@ -720,8 +720,6 @@ void TreeLayout::Layout(const BehaviorSupportProfile &p_support) m_nodeMap[node]->m_sublevel = entry->m_sublevel; } - m_infosetSublevels.clear(); - m_numSublevels = std::vector(m_maxLevel + 1); for (auto entry : m_nodeList) { ComputeSublevel(entry); } diff --git a/src/gui/efglayout.h b/src/gui/efglayout.h index 42a148232..d4f773930 100644 --- a/src/gui/efglayout.h +++ b/src/gui/efglayout.h @@ -109,8 +109,6 @@ class NodeEntry { class TreeLayout final : public GameView { std::list> m_nodeList; std::map> m_nodeMap; - std::vector m_numSublevels; - std::map, int> m_infosetSublevels; mutable int m_maxX{0}, m_maxY{0}, m_maxLevel{0}; int m_infosetSpacing{40}; From 7e3d2e584534b98087db1aadfdffd0c90cee567e Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 27 Oct 2025 16:56:42 +0000 Subject: [PATCH 04/11] Remove max level from GUI class. --- src/gui/efglayout.cc | 27 +++++++-------------------- src/gui/efglayout.h | 4 ++-- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/gui/efglayout.cc b/src/gui/efglayout.cc index 578d03df3..46e746f9f 100644 --- a/src/gui/efglayout.cc +++ b/src/gui/efglayout.cc @@ -624,23 +624,14 @@ TreeLayout::ComputeNextInInfoset(const std::shared_ptr &p_entry) void TreeLayout::ComputeSublevel(const std::shared_ptr &p_entry) { - /* - try { - p_entry->m_sublevel = m_infosetSublevels.at({p_entry->m_level, p_entry->m_node->GetInfoset()}); - } - catch (std::out_of_range &) { - p_entry->m_sublevel = ++m_numSublevels[p_entry->m_level]; - m_infosetSublevels[{p_entry->m_level, p_entry->m_node->GetInfoset()}] = p_entry->m_sublevel; - } - */ p_entry->m_nextMember = ComputeNextInInfoset(p_entry); } void TreeLayout::ComputeNodeDepths(const Gambit::Layout &p_layout) const { - std::vector aggregateSublevels(m_maxLevel + 1); + std::vector aggregateSublevels; std::partial_sum(p_layout.GetNumSublevels().cbegin(), p_layout.GetNumSublevels().cend(), - aggregateSublevels.begin()); + std::back_inserter(aggregateSublevels)); m_maxX = 0; for (const auto &entry : m_nodeList) { entry->m_x = c_leftMargin + entry->m_level * m_doc->GetStyle().GetNodeLevelLength(); @@ -659,43 +650,39 @@ void TreeLayout::ComputeRenderedParents() const } } -void TreeLayout::BuildNodeList(const GameNode &p_node, const BehaviorSupportProfile &p_support, - const int p_level) +void TreeLayout::BuildNodeList(const GameNode &p_node, const BehaviorSupportProfile &p_support) { const auto entry = std::make_shared(p_node); m_nodeList.push_back(entry); m_nodeMap[p_node] = entry; entry->m_size = m_doc->GetStyle().GetNodeSize(); entry->m_branchLength = m_doc->GetStyle().GetBranchLength(); - entry->m_level = p_level; if (m_doc->GetStyle().RootReachable()) { if (const GameInfoset infoset = p_node->GetInfoset()) { if (infoset->GetPlayer()->IsChance()) { for (const auto &child : p_node->GetChildren()) { - BuildNodeList(child, p_support, p_level + 1); + BuildNodeList(child, p_support); } } else { for (const auto &action : p_support.GetActions(infoset)) { - BuildNodeList(p_node->GetChild(action), p_support, p_level + 1); + BuildNodeList(p_node->GetChild(action), p_support); } } } } else { for (const auto &child : p_node->GetChildren()) { - BuildNodeList(child, p_support, p_level + 1); + BuildNodeList(child, p_support); } } - m_maxLevel = std::max(p_level, m_maxLevel); } void TreeLayout::BuildNodeList(const BehaviorSupportProfile &p_support) { m_nodeList.clear(); m_nodeMap.clear(); - m_maxLevel = 0; - BuildNodeList(m_doc->GetGame()->GetRoot(), p_support, 0); + BuildNodeList(m_doc->GetGame()->GetRoot(), p_support); } void TreeLayout::Layout(const BehaviorSupportProfile &p_support) diff --git a/src/gui/efglayout.h b/src/gui/efglayout.h index d4f773930..ae76783db 100644 --- a/src/gui/efglayout.h +++ b/src/gui/efglayout.h @@ -110,7 +110,7 @@ class TreeLayout final : public GameView { std::list> m_nodeList; std::map> m_nodeMap; - mutable int m_maxX{0}, m_maxY{0}, m_maxLevel{0}; + mutable int m_maxX{0}, m_maxY{0}; int m_infosetSpacing{40}; const int c_leftMargin{20}, c_topMargin{40}, c_bottomMargin{25}; @@ -118,7 +118,7 @@ class TreeLayout final : public GameView { std::shared_ptr ComputeNextInInfoset(const std::shared_ptr &); void ComputeSublevel(const std::shared_ptr &); - void BuildNodeList(const GameNode &, const BehaviorSupportProfile &, int); + void BuildNodeList(const GameNode &, const BehaviorSupportProfile &); /// (Recursively) compute the y-offsets of all nodes void ComputeOffsets(const GameNode &, const BehaviorSupportProfile &, int &); From 7233788fd14208f846ec3a286293bf5e2bfab793 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 27 Oct 2025 17:06:25 +0000 Subject: [PATCH 05/11] Compute next member on demand for now --- src/gui/efglayout.cc | 32 +++++++++++--------------------- src/gui/efglayout.h | 16 ++++++---------- 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/src/gui/efglayout.cc b/src/gui/efglayout.cc index 46e746f9f..c5aaf8122 100644 --- a/src/gui/efglayout.cc +++ b/src/gui/efglayout.cc @@ -377,14 +377,15 @@ GameNode TreeLayout::BranchBelowHitTest(int p_x, int p_y) const bool TreeLayout::InfosetHitTest(const std::shared_ptr &p_entry, const int p_x, const int p_y) const { - if (p_entry->GetNextMember() && p_entry->GetNode()->GetInfoset()) { + auto nextMember = ComputeNextInInfoset(p_entry); + if (nextMember && p_entry->GetNode()->GetInfoset()) { if (p_x > p_entry->m_x + p_entry->GetSublevel() * m_infosetSpacing - 2 && p_x < p_entry->m_x + p_entry->GetSublevel() * m_infosetSpacing + 2) { - if (p_y > p_entry->m_y && p_y < p_entry->GetNextMember()->m_y) { + if (p_y > p_entry->m_y && p_y < nextMember->m_y) { // next infoset is below this one return true; } - if (p_y > p_entry->GetNextMember()->m_y && p_y < p_entry->m_y) { + if (p_y > nextMember->m_y && p_y < p_entry->m_y) { // next infoset is above this one return true; } @@ -595,7 +596,7 @@ void TreeLayout::ComputeOffsets(const GameNode &p_node, const BehaviorSupportPro } std::shared_ptr -TreeLayout::ComputeNextInInfoset(const std::shared_ptr &p_entry) +TreeLayout::ComputeNextInInfoset(const std::shared_ptr &p_entry) const { const auto infoset = p_entry->m_node->GetInfoset(); if (!infoset) { @@ -622,11 +623,6 @@ TreeLayout::ComputeNextInInfoset(const std::shared_ptr &p_entry) return nullptr; } -void TreeLayout::ComputeSublevel(const std::shared_ptr &p_entry) -{ - p_entry->m_nextMember = ComputeNextInInfoset(p_entry); -} - void TreeLayout::ComputeNodeDepths(const Gambit::Layout &p_layout) const { std::vector aggregateSublevels; @@ -706,10 +702,6 @@ void TreeLayout::Layout(const BehaviorSupportProfile &p_support) m_nodeMap[node]->m_level = entry->m_level; m_nodeMap[node]->m_sublevel = entry->m_sublevel; } - - for (auto entry : m_nodeList) { - ComputeSublevel(entry); - } ComputeNodeDepths(layout); ComputeRenderedParents(); @@ -771,9 +763,9 @@ void TreeLayout::RenderSubtree(wxDC &p_dc, bool p_noHints) const if (entry->GetChildNumber() == 1) { DrawNode(p_dc, parentEntry, m_doc->GetSelectNode(), p_noHints); - if (parentEntry->GetNextMember()) { - const int nextX = parentEntry->GetNextMember()->m_x; - const int nextY = parentEntry->GetNextMember()->m_y; + if (auto nextMember = ComputeNextInInfoset(parentEntry)) { + const int nextX = nextMember->m_x; + const int nextY = nextMember->m_y; if (parentEntry->m_x == nextX) { #ifdef __WXGTK__ @@ -790,17 +782,15 @@ void TreeLayout::RenderSubtree(wxDC &p_dc, bool p_noHints) const parentEntry->m_x + parentEntry->GetSize(), nextY); } - if (parentEntry->GetNextMember()->m_x != parentEntry->m_x) { + if (nextMember->m_x != parentEntry->m_x) { // Draw a little arrow in the direction of the iset. int startX, endX; if (settings.GetInfosetJoin() == GBT_INFOSET_JOIN_LINES) { startX = parentEntry->m_x; - endX = - (startX + m_infosetSpacing * - ((parentEntry->GetNextMember()->m_x > parentEntry->m_x) ? 1 : -1)); + endX = (startX + m_infosetSpacing * ((nextMember->m_x > parentEntry->m_x) ? 1 : -1)); } else { - if (parentEntry->GetNextMember()->m_x < parentEntry->m_x) { + if (nextMember->m_x < parentEntry->m_x) { // information set is continued to the left startX = parentEntry->m_x + parentEntry->GetSize(); endX = parentEntry->m_x - m_infosetSpacing; diff --git a/src/gui/efglayout.h b/src/gui/efglayout.h index ae76783db..65bfb8605 100644 --- a/src/gui/efglayout.h +++ b/src/gui/efglayout.h @@ -31,12 +31,11 @@ namespace Gambit::GUI { class NodeEntry { friend class TreeLayout; - GameNode m_node; // the corresponding node in the game - std::shared_ptr m_parent; // parent node - int m_x{-1}, m_y{-1}; // Cartesian coordinates of node - std::shared_ptr m_nextMember; // entry of next information set member - bool m_inSupport{true}; // true if node reachable in current support - int m_size{20}; // horizontal size of the node + GameNode m_node; // the corresponding node in the game + std::shared_ptr m_parent; // parent node + int m_x{-1}, m_y{-1}; // Cartesian coordinates of node + bool m_inSupport{true}; // true if node reachable in current support + int m_size{20}; // horizontal size of the node mutable wxRect m_outcomeRect; mutable Array m_payoffRect; mutable wxRect m_branchAboveRect, m_branchBelowRect; @@ -60,8 +59,6 @@ class NodeEntry { int GetX() const { return m_x; } int GetY() const { return m_y; } - std::shared_ptr GetNextMember() const { return m_nextMember; } - int GetChildNumber() const { return (m_node->GetParent()) ? m_node->GetPriorAction()->GetNumber() : 0; @@ -115,8 +112,7 @@ class TreeLayout final : public GameView { const int c_leftMargin{20}, c_topMargin{40}, c_bottomMargin{25}; - std::shared_ptr ComputeNextInInfoset(const std::shared_ptr &); - void ComputeSublevel(const std::shared_ptr &); + std::shared_ptr ComputeNextInInfoset(const std::shared_ptr &) const; void BuildNodeList(const GameNode &, const BehaviorSupportProfile &); From 99faaa2c68252f520a465f41aac011dda3d18997 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 27 Oct 2025 17:07:40 +0000 Subject: [PATCH 06/11] Remove unused maxlevel from new layout --- src/gui/layout.cc | 1 - src/gui/layout.h | 1 - 2 files changed, 2 deletions(-) diff --git a/src/gui/layout.cc b/src/gui/layout.cc index cc11432b9..23202bcdb 100644 --- a/src/gui/layout.cc +++ b/src/gui/layout.cc @@ -72,7 +72,6 @@ void Layout::LayoutSubtree(const GameNode &p_node, const BehaviorSupportProfile void Layout::LayoutTree(const BehaviorSupportProfile &p_support) { m_nodeMap.clear(); - m_maxLevel = 0; m_numSublevels.clear(); m_infosetSublevels.clear(); diff --git a/src/gui/layout.h b/src/gui/layout.h index 9cc7ee7eb..c01002374 100644 --- a/src/gui/layout.h +++ b/src/gui/layout.h @@ -44,7 +44,6 @@ class Layout { std::map, int> m_infosetSublevels; float m_maxOffset{0}; - int m_maxLevel{0}; void LayoutSubtree(const GameNode &, const BehaviorSupportProfile &, int, double &); From 3f400f769123201cfc4351faccd4b1f7cab43011 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 27 Oct 2025 17:28:25 +0000 Subject: [PATCH 07/11] Offsets now being computed from the generic layout. --- src/gui/dlefglayout.cc | 4 ++-- src/gui/efglayout.cc | 43 ++++-------------------------------------- src/gui/efglayout.h | 2 -- src/gui/layout.cc | 4 ++-- src/gui/layout.h | 6 +++--- src/gui/style.h | 2 +- 6 files changed, 12 insertions(+), 49 deletions(-) diff --git a/src/gui/dlefglayout.cc b/src/gui/dlefglayout.cc index aa0adf7a8..b4802680b 100644 --- a/src/gui/dlefglayout.cc +++ b/src/gui/dlefglayout.cc @@ -115,8 +115,8 @@ LayoutNodesPanel::LayoutNodesPanel(wxWindow *p_parent, const TreeRenderConfig &p constexpr int Y_SPACING_MAX = 60; m_terminalSpacing = new wxSpinCtrl( - this, wxID_ANY, wxString::Format(_T("%d"), p_settings.TerminalSpacing()), wxDefaultPosition, - wxDefaultSize, wxSP_ARROW_KEYS, Y_SPACING_MIN, Y_SPACING_MAX); + this, wxID_ANY, wxString::Format(_T("%d"), p_settings.GetTerminalSpacing()), + wxDefaultPosition, wxDefaultSize, wxSP_ARROW_KEYS, Y_SPACING_MIN, Y_SPACING_MAX); gridSizer->Add(m_terminalSpacing, 1, wxEXPAND | wxALL, 5); sizeSizer->Add(gridSizer, 1, wxALL | wxEXPAND, 5); diff --git a/src/gui/efglayout.cc b/src/gui/efglayout.cc index c5aaf8122..af9a595d9 100644 --- a/src/gui/efglayout.cc +++ b/src/gui/efglayout.cc @@ -560,41 +560,6 @@ GameNode TreeLayout::NextSameLevel(const GameNode &p_node) const return nullptr; } -void TreeLayout::ComputeOffsets(const GameNode &p_node, const BehaviorSupportProfile &p_support, - int &p_ycoord) -{ - const auto &settings = m_doc->GetStyle(); - - const auto entry = GetNodeEntry(p_node); - if (m_doc->GetStyle().RootReachable() && p_node->GetInfoset() && - !p_node->GetInfoset()->GetPlayer()->IsChance()) { - const auto actions = p_support.GetActions(p_node->GetInfoset()); - for (const auto &action : actions) { - ComputeOffsets(p_node->GetChild(action), p_support, p_ycoord); - } - entry->m_y = (GetNodeEntry(p_node->GetChild(actions.front()))->m_y + - GetNodeEntry(p_node->GetChild(actions.back()))->m_y) / - 2; - } - else if (!p_node->IsTerminal()) { - const auto actions = p_node->GetInfoset()->GetActions(); - for (const auto &action : actions) { - const auto child = p_node->GetChild(action); - ComputeOffsets(child, p_support, p_ycoord); - if (!p_node->GetPlayer()->IsChance() && !p_support.Contains(action)) { - GetNodeEntry(child)->m_inSupport = false; - } - } - entry->m_y = (GetNodeEntry(p_node->GetChild(actions.front()))->m_y + - GetNodeEntry(p_node->GetChild(actions.back()))->m_y) / - 2; - } - else { - entry->m_y = p_ycoord; - p_ycoord += settings.TerminalSpacing(); - } -} - std::shared_ptr TreeLayout::ComputeNextInInfoset(const std::shared_ptr &p_entry) const { @@ -691,17 +656,17 @@ void TreeLayout::Layout(const BehaviorSupportProfile &p_support) BuildNodeList(p_support); } - int maxy = c_topMargin; - ComputeOffsets(m_doc->GetGame()->GetRoot(), p_support, maxy); - m_maxY = maxy + c_bottomMargin; - auto layout = Gambit::Layout(m_doc->GetGame()); layout.LayoutTree(p_support); + const auto spacing = m_doc->GetStyle().GetTerminalSpacing(); for (auto [node, entry] : layout.GetNodeMap()) { m_nodeMap[node]->m_level = entry->m_level; m_nodeMap[node]->m_sublevel = entry->m_sublevel; + m_nodeMap[node]->m_y = entry->m_offset * spacing + c_topMargin; } + m_maxY = + c_topMargin + c_bottomMargin + spacing * (layout.GetMaxOffset() - layout.GetMinOffset()); ComputeNodeDepths(layout); ComputeRenderedParents(); diff --git a/src/gui/efglayout.h b/src/gui/efglayout.h index 65bfb8605..365861fc9 100644 --- a/src/gui/efglayout.h +++ b/src/gui/efglayout.h @@ -116,8 +116,6 @@ class TreeLayout final : public GameView { void BuildNodeList(const GameNode &, const BehaviorSupportProfile &); - /// (Recursively) compute the y-offsets of all nodes - void ComputeOffsets(const GameNode &, const BehaviorSupportProfile &, int &); /// Based on node levels and information set sublevels, compute the depth /// (X coordinate) of all nodes void ComputeNodeDepths(const Gambit::Layout &) const; diff --git a/src/gui/layout.cc b/src/gui/layout.cc index 23202bcdb..258294b71 100644 --- a/src/gui/layout.cc +++ b/src/gui/layout.cc @@ -75,8 +75,8 @@ void Layout::LayoutTree(const BehaviorSupportProfile &p_support) m_numSublevels.clear(); m_infosetSublevels.clear(); - double ycoord = 0; - LayoutSubtree(m_game->GetRoot(), p_support, 0, ycoord); + m_maxOffset = 0; + LayoutSubtree(m_game->GetRoot(), p_support, 0, m_maxOffset); } } // namespace Gambit diff --git a/src/gui/layout.h b/src/gui/layout.h index c01002374..477705637 100644 --- a/src/gui/layout.h +++ b/src/gui/layout.h @@ -43,7 +43,7 @@ class Layout { std::vector m_numSublevels; std::map, int> m_infosetSublevels; - float m_maxOffset{0}; + double m_maxOffset{0}; void LayoutSubtree(const GameNode &, const BehaviorSupportProfile &, int, double &); @@ -55,8 +55,8 @@ class Layout { const std::map> &GetNodeMap() const { return m_nodeMap; } const std::vector &GetNumSublevels() const { return m_numSublevels; } - float GetMinOffset() const { return 0; } - float GetMaxOffset() const { return m_maxOffset; } + double GetMinOffset() const { return 0; } + double GetMaxOffset() const { return m_maxOffset; } }; } // namespace Gambit #endif // GAMBIT_LAYOUT_H diff --git a/src/gui/style.h b/src/gui/style.h index 6823c700c..61edcdb25 100644 --- a/src/gui/style.h +++ b/src/gui/style.h @@ -109,7 +109,7 @@ class TreeRenderConfig { int GetNodeSize() const { return m_nodeSize; } void SetNodeSize(int p_nodeSize) { m_nodeSize = p_nodeSize; } - int TerminalSpacing() const { return m_terminalSpacing; } + int GetTerminalSpacing() const { return m_terminalSpacing; } void SetTerminalSpacing(int p_spacing) { m_terminalSpacing = p_spacing; } NodeTokenStyle GetChanceToken() const { return m_chanceToken; } From d53c89a25fabd349526d406d7fd62a37529f12ee Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 27 Oct 2025 19:15:19 +0000 Subject: [PATCH 08/11] Removing unused (and non-existent) includes --- src/gui/layout.cc | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/gui/layout.cc b/src/gui/layout.cc index 258294b71..74910f610 100644 --- a/src/gui/layout.cc +++ b/src/gui/layout.cc @@ -20,13 +20,8 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // -#include // for std::min, std::max -#include // for std::partial_sum - #include "layout.h" -#include <__ostream/basic_ostream.h> - namespace Gambit { void Layout::LayoutSubtree(const GameNode &p_node, const BehaviorSupportProfile &p_support, From 3185b1188a05ad07b5eaabfc9a0e0bd9f01b3feb Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 19 Nov 2025 10:51:08 +0000 Subject: [PATCH 09/11] Move UI-independent tree layout to game --- Makefile.am | 4 ++-- src/{gui => games}/layout.cc | 0 src/{gui => games}/layout.h | 15 ++++++++------- src/gui/efglayout.cc | 5 +---- src/gui/efglayout.h | 4 ++-- 5 files changed, 13 insertions(+), 15 deletions(-) rename src/{gui => games}/layout.cc (100%) rename src/{gui => games}/layout.h (91%) diff --git a/Makefile.am b/Makefile.am index 86040249e..681edc9ca 100644 --- a/Makefile.am +++ b/Makefile.am @@ -313,6 +313,8 @@ game_SOURCES = \ src/games/file.cc \ src/games/writer.cc \ src/games/writer.h \ + src/games/layout.cc \ + src/games/layout.h \ ${agg_SOURCES} \ src/games/nash.h @@ -589,8 +591,6 @@ gambit_SOURCES = \ src/gui/gamedoc.h \ src/gui/gameframe.cc \ src/gui/gameframe.h \ - src/gui/layout.cc \ - src/gui/layout.h \ src/gui/menuconst.h \ src/gui/nfgpanel.cc \ src/gui/nfgpanel.h \ diff --git a/src/gui/layout.cc b/src/games/layout.cc similarity index 100% rename from src/gui/layout.cc rename to src/games/layout.cc diff --git a/src/gui/layout.h b/src/games/layout.h similarity index 91% rename from src/gui/layout.h rename to src/games/layout.h index 477705637..a0f59c98a 100644 --- a/src/gui/layout.h +++ b/src/games/layout.h @@ -2,8 +2,8 @@ // This file is part of Gambit // Copyright (c) 1994-2025, The Gambit Project (https://www.gambit-project.org) // -// FILE: src/gui/efglayout.h -// Interface to tree layout representation +// FILE: src/games/layout.h +// Interface to generic tree layout representation // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -20,13 +20,13 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // -#ifndef GAMBIT_LAYOUT_H -#define GAMBIT_LAYOUT_H - -#include "gambit.h" +#ifndef GAMBIT_GAMES_TREELAYOUT_H +#define GAMBIT_GAMES_TREELAYOUT_H #include +#include "gambit.h" + namespace Gambit { struct LayoutEntry { friend class Layout; @@ -59,4 +59,5 @@ class Layout { double GetMaxOffset() const { return m_maxOffset; } }; } // namespace Gambit -#endif // GAMBIT_LAYOUT_H + +#endif // GAMBIT_GAMES_TREELAYOUT_H diff --git a/src/gui/efglayout.cc b/src/gui/efglayout.cc index af9a595d9..c876756f0 100644 --- a/src/gui/efglayout.cc +++ b/src/gui/efglayout.cc @@ -29,10 +29,7 @@ #include #endif // WX_PRECOMP -#include "gambit.h" -#include "efgdisplay.h" - -#include "layout.h" +#include "efglayout.h" namespace Gambit::GUI { namespace { diff --git a/src/gui/efglayout.h b/src/gui/efglayout.h index 365861fc9..823482365 100644 --- a/src/gui/efglayout.h +++ b/src/gui/efglayout.h @@ -26,7 +26,7 @@ #include "gambit.h" #include "gamedoc.h" -#include "layout.h" +#include "games/layout.h" namespace Gambit::GUI { class NodeEntry { @@ -118,7 +118,7 @@ class TreeLayout final : public GameView { /// Based on node levels and information set sublevels, compute the depth /// (X coordinate) of all nodes - void ComputeNodeDepths(const Gambit::Layout &) const; + void ComputeNodeDepths(const Layout &) const; void ComputeRenderedParents() const; wxString CreateNodeLabel(const std::shared_ptr &, int) const; From 2cc3997350234796400ebd1888f13939867a8b8a Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 19 Nov 2025 15:20:35 +0000 Subject: [PATCH 10/11] Rudimentary exposure of tree layout in Python --- src/games/layout.h | 14 ++++++++++++++ src/pygambit/gambit.pxd | 8 ++++++++ src/pygambit/game.pxi | 16 ++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/games/layout.h b/src/games/layout.h index a0f59c98a..9fa4ad53f 100644 --- a/src/games/layout.h +++ b/src/games/layout.h @@ -26,8 +26,10 @@ #include #include "gambit.h" +#include "game.h" namespace Gambit { + struct LayoutEntry { friend class Layout; double m_offset{-1}; // Cartesian coordinates of node @@ -54,10 +56,22 @@ class Layout { void LayoutTree(const BehaviorSupportProfile &); const std::map> &GetNodeMap() const { return m_nodeMap; } + int GetNodeLevel(const GameNode &p_node) const { return m_nodeMap.at(p_node)->m_level; } + int GetNodeSublevel(const GameNode &p_node) const { return m_nodeMap.at(p_node)->m_sublevel; } + double GetNodeOffset(const GameNode &p_node) const { return m_nodeMap.at(p_node)->m_offset; } + const std::vector &GetNumSublevels() const { return m_numSublevels; } double GetMinOffset() const { return 0; } double GetMaxOffset() const { return m_maxOffset; } }; + +inline std::shared_ptr CreateLayout(const Game &p_game) +{ + auto layout = std::make_shared(p_game); + layout->LayoutTree(BehaviorSupportProfile(p_game)); + return layout; +} + } // namespace Gambit #endif // GAMBIT_GAMES_TREELAYOUT_H diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index e492d9c00..a6c6bb40b 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -417,6 +417,14 @@ cdef extern from "games/behavspt.h": c_BehaviorSupportProfile(c_Game) except + +cdef extern from "games/layout.h": + cdef cppclass c_Layout "Layout": + int GetNodeLevel(c_GameNode) except + + int GetNodeSublevel(c_GameNode) except + + double GetNodeOffset(c_GameNode) except + + shared_ptr[c_Layout] CreateLayout(c_Game) except + + + cdef extern from "util.h": c_Game ParseGbtGame(string) except +IOError c_Game ParseEfgGame(string) except +IOError diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 082b08b62..347e85c91 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -23,6 +23,7 @@ import io import itertools import pathlib +import cython import numpy as np import scipy.stats @@ -2030,3 +2031,18 @@ class Game: if len(resolved_strategy.player.strategies) == 1: raise UndefinedOperationError("Cannot delete the only strategy for a player") self.game.deref().DeleteStrategy(resolved_strategy.strategy) + + +@cython.cfunc +def _layout_tree(game: Game) -> dict[GameNode, dict]: + layout = CreateLayout(game.game) + data = {} + for node in game.nodes: + data[node] = {"level": deref(layout).GetNodeLevel(cython.cast(Node, node).node), + "sublevel": deref(layout).GetNodeSublevel(cython.cast(Node, node).node), + "offset": deref(layout).GetNodeOffset(cython.cast(Node, node).node)} + return data + + +def layout_tree(game: Game) -> dict[GameNode, dict]: + return _layout_tree(game) From 442188b12d846d9f41e3cbdf5e47e57c6e1422eb Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 19 Nov 2025 15:24:35 +0000 Subject: [PATCH 11/11] Rudimentary exposure of tree layout in Python. Closes #596. --- src/pygambit/game.pxi | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 347e85c91..4dd1de5b8 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -19,6 +19,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # +import dataclasses import io import itertools import pathlib @@ -2033,14 +2034,21 @@ class Game: self.game.deref().DeleteStrategy(resolved_strategy.strategy) +@dataclasses.dataclass +class NodeCoordinates: + level: int + sublevel: int + offset: float + + @cython.cfunc -def _layout_tree(game: Game) -> dict[GameNode, dict]: +def _layout_tree(game: Game) -> dict[GameNode, NodeCoordinates]: layout = CreateLayout(game.game) data = {} for node in game.nodes: - data[node] = {"level": deref(layout).GetNodeLevel(cython.cast(Node, node).node), - "sublevel": deref(layout).GetNodeSublevel(cython.cast(Node, node).node), - "offset": deref(layout).GetNodeOffset(cython.cast(Node, node).node)} + data[node] = NodeCoordinates(deref(layout).GetNodeLevel(cython.cast(Node, node).node), + deref(layout).GetNodeSublevel(cython.cast(Node, node).node), + deref(layout).GetNodeOffset(cython.cast(Node, node).node)) return data