diff --git a/Makefile.am b/Makefile.am index 4325353ef..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 diff --git a/src/games/layout.cc b/src/games/layout.cc new file mode 100644 index 000000000..74910f610 --- /dev/null +++ b/src/games/layout.cc @@ -0,0 +1,77 @@ +// +// 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 "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 &) { + 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; + } + + 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_numSublevels.clear(); + m_infosetSublevels.clear(); + + m_maxOffset = 0; + LayoutSubtree(m_game->GetRoot(), p_support, 0, m_maxOffset); +} + +} // namespace Gambit diff --git a/src/games/layout.h b/src/games/layout.h new file mode 100644 index 000000000..9fa4ad53f --- /dev/null +++ b/src/games/layout.h @@ -0,0 +1,77 @@ +// +// This file is part of Gambit +// Copyright (c) 1994-2025, The Gambit Project (https://www.gambit-project.org) +// +// 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 +// 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_GAMES_TREELAYOUT_H +#define GAMBIT_GAMES_TREELAYOUT_H + +#include + +#include "gambit.h" +#include "game.h" + +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; + + double m_maxOffset{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; } + 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/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 8e1dd105d..c876756f0 100644 --- a/src/gui/efglayout.cc +++ b/src/gui/efglayout.cc @@ -29,8 +29,7 @@ #include #endif // WX_PRECOMP -#include "gambit.h" -#include "efgdisplay.h" +#include "efglayout.h" namespace Gambit::GUI { namespace { @@ -375,14 +374,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; } @@ -557,43 +557,8 @@ 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) +TreeLayout::ComputeNextInInfoset(const std::shared_ptr &p_entry) const { const auto infoset = p_entry->m_node->GetInfoset(); if (!infoset) { @@ -620,22 +585,11 @@ TreeLayout::ComputeNextInInfoset(const std::shared_ptr &p_entry) return nullptr; } -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 +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::vector aggregateSublevels; + std::partial_sum(p_layout.GetNumSublevels().cbegin(), p_layout.GetNumSublevels().cend(), + 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(); @@ -654,43 +608,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) @@ -703,16 +653,18 @@ 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); - m_infosetSublevels.clear(); - m_numSublevels = std::vector(m_maxLevel + 1); - for (auto entry : m_nodeList) { - ComputeSublevel(entry); + 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; } - ComputeNodeDepths(); + m_maxY = + c_topMargin + c_bottomMargin + spacing * (layout.GetMaxOffset() - layout.GetMinOffset()); + ComputeNodeDepths(layout); ComputeRenderedParents(); GenerateLabels(); @@ -773,9 +725,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__ @@ -792,17 +744,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 2873fc5a8..823482365 100644 --- a/src/gui/efglayout.h +++ b/src/gui/efglayout.h @@ -26,15 +26,16 @@ #include "gambit.h" #include "gamedoc.h" +#include "games/layout.h" + 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; @@ -58,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; @@ -107,24 +106,19 @@ 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}; + mutable int m_maxX{0}, m_maxY{0}; int m_infosetSpacing{40}; 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 &, int); + 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; + void ComputeNodeDepths(const Layout &) const; void ComputeRenderedParents() const; wxString CreateNodeLabel(const std::shared_ptr &, int) const; 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; } 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..4dd1de5b8 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -19,10 +19,12 @@ # 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 +import cython import numpy as np import scipy.stats @@ -2030,3 +2032,25 @@ 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) + + +@dataclasses.dataclass +class NodeCoordinates: + level: int + sublevel: int + offset: float + + +@cython.cfunc +def _layout_tree(game: Game) -> dict[GameNode, NodeCoordinates]: + layout = CreateLayout(game.game) + data = {} + for node in game.nodes: + 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 + + +def layout_tree(game: Game) -> dict[GameNode, dict]: + return _layout_tree(game)