Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
77 changes: 77 additions & 0 deletions src/games/layout.cc
Original file line number Diff line number Diff line change
@@ -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<LayoutEntry>(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<int>(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
77 changes: 77 additions & 0 deletions src/games/layout.h
Original file line number Diff line number Diff line change
@@ -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 <map>

#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<GameNode, std::shared_ptr<LayoutEntry>> m_nodeMap;
std::vector<int> m_numSublevels;
std::map<std::pair<int, GameInfoset>, 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<GameNode, std::shared_ptr<LayoutEntry>> &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<int> &GetNumSublevels() const { return m_numSublevels; }
double GetMinOffset() const { return 0; }
double GetMaxOffset() const { return m_maxOffset; }
};

inline std::shared_ptr<Layout> CreateLayout(const Game &p_game)
{
auto layout = std::make_shared<Layout>(p_game);
layout->LayoutTree(BehaviorSupportProfile(p_game));
return layout;
}

} // namespace Gambit

#endif // GAMBIT_GAMES_TREELAYOUT_H
4 changes: 2 additions & 2 deletions src/gui/dlefglayout.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
112 changes: 31 additions & 81 deletions src/gui/efglayout.cc
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@
#include <wx/wx.h>
#endif // WX_PRECOMP

#include "gambit.h"
#include "efgdisplay.h"
#include "efglayout.h"

namespace Gambit::GUI {
namespace {
Expand Down Expand Up @@ -375,14 +374,15 @@ GameNode TreeLayout::BranchBelowHitTest(int p_x, int p_y) const
bool TreeLayout::InfosetHitTest(const std::shared_ptr<NodeEntry> &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;
}
Expand Down Expand Up @@ -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<NodeEntry>
TreeLayout::ComputeNextInInfoset(const std::shared_ptr<NodeEntry> &p_entry)
TreeLayout::ComputeNextInInfoset(const std::shared_ptr<NodeEntry> &p_entry) const
{
const auto infoset = p_entry->m_node->GetInfoset();
if (!infoset) {
Expand All @@ -620,22 +585,11 @@ TreeLayout::ComputeNextInInfoset(const std::shared_ptr<NodeEntry> &p_entry)
return nullptr;
}

void TreeLayout::ComputeSublevel(const std::shared_ptr<NodeEntry> &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<int> aggregateSublevels(m_maxLevel + 1);
std::partial_sum(m_numSublevels.cbegin(), m_numSublevels.cend(), aggregateSublevels.begin());
std::vector<int> 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();
Expand All @@ -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<NodeEntry>(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)
Expand All @@ -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<int>(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();
Expand Down Expand Up @@ -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__
Expand All @@ -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;
Expand Down
Loading
Loading