diff --git a/doc/gui.efg.rst b/doc/gui.efg.rst index a863bdb1e..4878ec077 100644 --- a/doc/gui.efg.rst +++ b/doc/gui.efg.rst @@ -417,7 +417,7 @@ To select the font used to draw the labels in the tree, select :menuselection:`Format --> Font`. The standard font selection dialog for the operating system is displayed, showing the fonts available on the system. Since -available fonts vary across systems, when opening a workbook on a +available fonts vary across systems, when opening a workspace on a system different from the system on which it was saved, Gambit tries to match the font style as closely as possible when the original font is not available. diff --git a/doc/gui.general.rst b/doc/gui.general.rst index b42165f31..e96baa055 100644 --- a/doc/gui.general.rst +++ b/doc/gui.general.rst @@ -80,30 +80,14 @@ versions dating back to release 0.94 in 1995. (Users interested in the details of these file formats can consult :ref:`file-formats` for more information.) -Beginning with release 2005.12.xx, the graphical interface now reads -and writes a new file format, which is referred to as a"Gambit -workbook." This extended file format stores not only the +The graphical interface reads and writes an extended format, referred to +as a "Gambit workspace". +This stores not only the representation of the game, but also additional information, including parameters for laying out the game tree, the colors assigned to players, any equilibria or other analysis done on the game, and so -forth. So, for example, the workbook file can be used to store the +forth. So, for example, the workspace file can be used to store the analysis of a game and then return to it. These files by convention end in the extension .gbt. - The graphical interface will read files in all three formats: .gbt, -.efg, and .nfg. The "Save" and "Save as" commands, however, always -save in the Gambit workbook (.gbt) format. To save the game itself as -an extensive (.efg) or strategic (.nfg) game, use the items on the -"Export" submenu of the "File" menu. This is useful in interfacing -with older versions of Gambit, with other tools which read and write -those formats, and in using the underlying Gambit analysis command- -line tools directly, as those programs accept .efg or .nfg game files. -Users primarily interested in using Gambit solely via the graphical -interface are encouraged to use the workbook (.gbt) format. - - - -As it is a new format, the Gambit workbook format is still under -development and may change in details. It is intended that newer -versions of the graphical interface will still be able to read -workbook files written in older formats. +.efg, and .nfg. diff --git a/src/gui/analysis.cc b/src/gui/analysis.cc index 2ded2c819..84f1d3a7f 100644 --- a/src/gui/analysis.cc +++ b/src/gui/analysis.cc @@ -100,7 +100,7 @@ template void AnalysisProfileList::AddOutput(const wxString &p_outp auto profile = std::make_shared>(OutputToMixedProfile(m_doc, p_output)); m_mixedProfiles.push_back(profile); - if (m_doc->IsTree()) { + if (m_doc->GetGame()->IsTree()) { m_behavProfiles.push_back(std::make_shared>(*profile)); } m_current = m_mixedProfiles.size(); @@ -120,7 +120,7 @@ template void AnalysisProfileList::BuildNfg() template int AnalysisProfileList::NumProfiles() const { - return (m_doc->IsTree()) ? m_behavProfiles.size() : m_mixedProfiles.size(); + return (m_doc->GetGame()->IsTree()) ? m_behavProfiles.size() : m_mixedProfiles.size(); } template void AnalysisProfileList::Clear() @@ -202,7 +202,7 @@ template std::string AnalysisProfileList::GetPayoff(int pl, int p_i const int index = (p_index == -1) ? m_current : p_index; try { - if (m_doc->IsTree()) { + if (m_doc->GetGame()->IsTree()) { return lexical_cast( m_behavProfiles[index]->GetPayoff(m_doc->GetGame()->GetPlayer(pl)), m_doc->GetStyle().NumDecimals()); @@ -417,7 +417,7 @@ template void AnalysisProfileList::Save(std::ostream &p_file) const p_file << static_cast(m_description.mb_str()) << "\n"; p_file << "\n"; - if (m_doc->IsTree()) { + if (m_doc->GetGame()->IsTree()) { for (int j = 1; j <= NumProfiles(); j++) { const MixedBehaviorProfile &behav = *m_behavProfiles[j]; p_file << "\n"; diff --git a/src/gui/app.cc b/src/gui/app.cc index ce23208da..1cb7bf574 100644 --- a/src/gui/app.cc +++ b/src/gui/app.cc @@ -130,7 +130,7 @@ AppLoadResult Application::LoadFile(const wxString &p_filename, wxWindow *p_pare } auto *doc = new GameDocument(NewTree()); - if (doc->LoadDocument(p_filename)) { + if (doc->LoadWorkspace(p_filename)) { doc->SetFilename(p_filename); m_fileHistory.AddFileToHistory(p_filename); m_fileHistory.Save(*wxConfigBase::Get()); @@ -167,12 +167,6 @@ void Application::SetCurrentDir(const wxString &p_dir) wxConfigBase::Get()->Write(_T("/General/CurrentDirectory"), p_dir); } -bool Application::AreDocumentsModified() const -{ - return std::any_of(m_documents.begin(), m_documents.end(), - std::mem_fn(&GameDocument::IsModified)); -} - } // namespace Gambit::GUI IMPLEMENT_APP(Gambit::GUI::Application) diff --git a/src/gui/app.h b/src/gui/app.h index da5b48994..bdb02b64b 100644 --- a/src/gui/app.h +++ b/src/gui/app.h @@ -83,7 +83,6 @@ class Application final : public wxApp { { m_documents.erase(std::find(m_documents.begin(), m_documents.end(), p_doc)); } - bool AreDocumentsModified() const; //@} }; diff --git a/src/gui/dlgameprop.cc b/src/gui/dlgameprop.cc index d2debad6d..8cca9d609 100644 --- a/src/gui/dlgameprop.cc +++ b/src/gui/dlgameprop.cc @@ -77,7 +77,7 @@ GamePropertiesDialog::GamePropertiesDialog(wxWindow *p_parent, GameDocument *p_d wxALL, 5); } - if (m_doc->IsTree()) { + if (game->IsTree()) { if (game->IsPerfectRecall()) { boxSizer->Add(new wxStaticText(this, wxID_STATIC, _("This is a game of perfect recall")), 0, wxALL, 5); diff --git a/src/gui/dlinsertmove.cc b/src/gui/dlinsertmove.cc index 320bbe179..71c16a9e7 100644 --- a/src/gui/dlinsertmove.cc +++ b/src/gui/dlinsertmove.cc @@ -118,7 +118,7 @@ void InsertMoveDialog::OnPlayer(wxCommandEvent &) if (playerNumber == 0) { player = m_doc->GetGame()->GetChance(); } - else if (playerNumber <= static_cast(m_doc->NumPlayers())) { + else if (playerNumber <= static_cast(m_doc->GetGame()->NumPlayers())) { player = m_doc->GetGame()->GetPlayer(playerNumber); } @@ -187,17 +187,17 @@ GamePlayer InsertMoveDialog::GetPlayer() const if (playerNumber == 0) { return m_doc->GetGame()->GetChance(); } - if (playerNumber <= static_cast(m_doc->NumPlayers())) { + if (playerNumber <= static_cast(m_doc->GetGame()->NumPlayers())) { return m_doc->GetGame()->GetPlayer(playerNumber); } const GamePlayer player = m_doc->GetGame()->NewPlayer(); - player->SetLabel("Player " + lexical_cast(m_doc->NumPlayers())); + player->SetLabel("Player " + lexical_cast(m_doc->GetGame()->NumPlayers())); return player; } GameInfoset InsertMoveDialog::GetInfoset() const { - if (m_playerItem->GetSelection() <= static_cast(m_doc->NumPlayers())) { + if (m_playerItem->GetSelection() <= static_cast(m_doc->GetGame()->NumPlayers())) { const GamePlayer player = GetPlayer(); const int infosetNumber = m_infosetItem->GetSelection(); diff --git a/src/gui/dlnash.cc b/src/gui/dlnash.cc index a5c201594..afc6b84ef 100644 --- a/src/gui/dlnash.cc +++ b/src/gui/dlnash.cc @@ -66,7 +66,7 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, GameDocument *p_doc) wxCommandEventHandler(NashChoiceDialog::OnCount)); topSizer->Add(m_countChoice, 0, wxALL | wxEXPAND, 5); - if (p_doc->NumPlayers() == 2 && m_doc->IsConstSum()) { + if (p_doc->GetGame()->NumPlayers() == 2 && m_doc->GetGame()->IsConstSum()) { wxString methodChoices[] = {s_recommended, s_lp, s_simpdiv, s_logit, s_enumpoly}; m_methodChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 5, methodChoices); @@ -79,7 +79,7 @@ NashChoiceDialog::NashChoiceDialog(wxWindow *p_parent, GameDocument *p_doc) m_methodChoice->SetSelection(0); topSizer->Add(m_methodChoice, 0, wxALL | wxEXPAND, 5); - if (m_doc->IsTree()) { + if (m_doc->GetGame()->IsTree()) { wxString repChoices[] = {wxT("using the extensive game"), wxT("using the strategic game")}; m_repChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 2, repChoices); m_repChoice->SetSelection(0); @@ -114,14 +114,14 @@ void NashChoiceDialog::OnCount(wxCommandEvent &p_event) m_methodChoice->Append(s_recommended); if (p_event.GetSelection() == 0) { - if (m_doc->NumPlayers() == 2 && m_doc->IsConstSum()) { + if (m_doc->GetGame()->NumPlayers() == 2 && m_doc->GetGame()->IsConstSum()) { m_methodChoice->Append(s_lp); } m_methodChoice->Append(s_simpdiv); m_methodChoice->Append(s_logit); } else if (p_event.GetSelection() == 1) { - if (m_doc->NumPlayers() == 2) { + if (m_doc->GetGame()->NumPlayers() == 2) { m_methodChoice->Append(s_lcp); } m_methodChoice->Append(s_enumpure); @@ -131,7 +131,7 @@ void NashChoiceDialog::OnCount(wxCommandEvent &p_event) m_methodChoice->Append(s_enumpoly); } else { - if (m_doc->NumPlayers() == 2) { + if (m_doc->GetGame()->NumPlayers() == 2) { m_methodChoice->Append(s_enummixed); } } @@ -195,7 +195,7 @@ std::shared_ptr NashChoiceDialog::GetCommand() const if (method == s_recommended) { if (m_countChoice->GetSelection() == 0) { - if (m_doc->NumPlayers() == 2 && m_doc->IsConstSum()) { + if (m_doc->GetGame()->NumPlayers() == 2 && m_doc->GetGame()->IsConstSum()) { cmd = std::make_shared>(m_doc, useEfg); cmd->SetCommand(prefix + wxT("lp") + options); cmd->SetDescription(wxT("One equilibrium by solving a linear program ") + game); @@ -207,7 +207,7 @@ std::shared_ptr NashChoiceDialog::GetCommand() const } } else if (m_countChoice->GetSelection() == 1) { - if (m_doc->NumPlayers() == 2) { + if (m_doc->GetGame()->NumPlayers() == 2) { cmd = std::make_shared>(m_doc, useEfg); cmd->SetCommand(prefix + wxT("lcp") + options); cmd->SetDescription(wxT("Some equilibria by solving a linear complementarity program ") + @@ -220,7 +220,7 @@ std::shared_ptr NashChoiceDialog::GetCommand() const } } else { - if (m_doc->NumPlayers() == 2) { + if (m_doc->GetGame()->NumPlayers() == 2) { cmd = std::make_shared>(m_doc, false); cmd->SetCommand(prefix + wxT("enummixed")); cmd->SetDescription( diff --git a/src/gui/dlnashmon.cc b/src/gui/dlnashmon.cc index a07ab5a16..07e385831 100644 --- a/src/gui/dlnashmon.cc +++ b/src/gui/dlnashmon.cc @@ -100,7 +100,7 @@ void NashMonitorDialog::Start(std::shared_ptr p_command) m_doc->BuildNfg(); } - m_doc->AddProfileList(p_command); + m_doc->DoAddEquilibriumOutput(p_command); m_process = new wxProcess(this, GBT_ID_PROCESS); m_process->Redirect(); diff --git a/src/gui/efgdisplay.cc b/src/gui/efgdisplay.cc index 5ab177bc7..658a6cccb 100644 --- a/src/gui/efgdisplay.cc +++ b/src/gui/efgdisplay.cc @@ -468,7 +468,7 @@ void EfgDisplay::OnKeyEvent(wxKeyEvent &p_event) PrepareDC(dc); OnDraw(dc); - if (player < static_cast(m_doc->NumPlayers())) { + if (player < static_cast(m_doc->GetGame()->NumPlayers())) { auto entry = m_layout.GetNodeEntry(node); const wxRect rect = entry->GetPayoffExtent(player + 1); int xx, yy; @@ -919,7 +919,7 @@ void EfgDisplay::OnLeftDoubleClick(wxMouseEvent &p_event) // Editing an existing outcome auto entry = m_layout.GetNodeEntry(node); - for (size_t pl = 1; pl <= m_doc->NumPlayers(); pl++) { + for (size_t pl = 1; pl <= m_doc->GetGame()->NumPlayers(); pl++) { const wxRect rect = entry->GetPayoffExtent(pl); if (rect.Contains(x, y)) { int xx, yy; diff --git a/src/gui/efglayout.cc b/src/gui/efglayout.cc index 6e41cf11d..a81f8599e 100644 --- a/src/gui/efglayout.cc +++ b/src/gui/efglayout.cc @@ -481,12 +481,15 @@ wxString TreeLayout::CreateNodeLabel(const std::shared_ptr &p_entry, return _T(""); } case GBT_NODE_LABEL_REALIZPROB: - return {m_doc->GetProfiles().GetRealizProb(n).c_str(), *wxConvCurrent}; + return {m_doc->GetWorkspace().GetProfiles().GetRealizProb(n).c_str(), *wxConvCurrent}; case GBT_NODE_LABEL_BELIEFPROB: - return {m_doc->GetProfiles().GetBeliefProb(n).c_str(), *wxConvCurrent}; + return {m_doc->GetWorkspace().GetProfiles().GetBeliefProb(n).c_str(), *wxConvCurrent}; case GBT_NODE_LABEL_VALUE: if (n->GetInfoset() && n->GetPlayer()->GetNumber() > 0) { - return {m_doc->GetProfiles().GetNodeValue(n, n->GetPlayer()->GetNumber()).c_str(), + return {m_doc->GetWorkspace() + .GetProfiles() + .GetNodeValue(n, n->GetPlayer()->GetNumber()) + .c_str(), *wxConvCurrent}; } else { @@ -521,19 +524,25 @@ wxString TreeLayout::CreateBranchLabel(const std::shared_ptr &p_entry .c_str(), *wxConvCurrent}; } - else if (m_doc->NumProfileLists() == 0) { + else if (m_doc->GetWorkspace().NumProfileLists() == 0) { return wxT(""); } else { - return {m_doc->GetProfiles().GetActionProb(parent, p_entry->GetChildNumber()).c_str(), + return {m_doc->GetWorkspace() + .GetProfiles() + .GetActionProb(parent, p_entry->GetChildNumber()) + .c_str(), *wxConvCurrent}; } case GBT_BRANCH_LABEL_VALUE: - if (m_doc->NumProfileLists() == 0) { + if (m_doc->GetWorkspace().NumProfileLists() == 0) { return wxT(""); } else { - return {m_doc->GetProfiles().GetActionValue(parent, p_entry->GetChildNumber()).c_str(), + return {m_doc->GetWorkspace() + .GetProfiles() + .GetActionValue(parent, p_entry->GetChildNumber()) + .c_str(), *wxConvCurrent}; } default: @@ -712,11 +721,12 @@ void TreeLayout::GenerateLabels() const parent->GetInfoset()->GetAction(entry->GetChildNumber())))); } else { - const int profile = m_doc->GetCurrentProfile(); + const int profile = m_doc->GetWorkspace().GetCurrentProfile(); if (profile > 0) { try { - entry->SetActionProb((double)lexical_cast( - m_doc->GetProfiles().GetActionProb(parent, entry->GetChildNumber()))); + entry->SetActionProb( + (double)lexical_cast(m_doc->GetWorkspace().GetProfiles().GetActionProb( + parent, entry->GetChildNumber()))); } catch (ValueException &) { // This occurs when the probability is undefined diff --git a/src/gui/efgpanel.cc b/src/gui/efgpanel.cc index 4767fb47b..784c9ad0d 100644 --- a/src/gui/efgpanel.cc +++ b/src/gui/efgpanel.cc @@ -188,7 +188,7 @@ gbtTreePlayerPanel::gbtTreePlayerPanel(wxWindow *p_parent, GameDocument *p_doc, void gbtTreePlayerPanel::OnUpdate() { - if (!m_doc->IsTree()) { + if (!m_doc->GetGame()->IsTree()) { return; } @@ -199,36 +199,36 @@ void gbtTreePlayerPanel::OnUpdate() wxString(m_doc->GetGame()->GetPlayer(m_player)->GetLabel().c_str(), *wxConvCurrent)); m_payoff->SetForegroundColour(color); - if (m_doc->GetCurrentProfile() > 0) { - const std::string pay = m_doc->GetProfiles().GetPayoff(m_player); + if (m_doc->GetWorkspace().GetCurrentProfile() > 0) { + const std::string pay = m_doc->GetWorkspace().GetProfiles().GetPayoff(m_player); m_payoff->SetLabel(wxT("Payoff: ") + wxString(pay.c_str(), *wxConvCurrent)); GetSizer()->Show(m_payoff, true); if (const GameNode node = m_doc->GetSelectNode()) { m_nodeValue->SetForegroundColour(color); - std::string value = m_doc->GetProfiles().GetNodeValue(node, m_player); + std::string value = m_doc->GetWorkspace().GetProfiles().GetNodeValue(node, m_player); m_nodeValue->SetLabel(wxT("Node value: ") + wxString(value.c_str(), *wxConvCurrent)); GetSizer()->Show(m_nodeValue, true); if (node->GetInfoset() && node->GetPlayer()->GetNumber() == m_player) { m_nodeProb->SetForegroundColour(color); - value = m_doc->GetProfiles().GetRealizProb(node); + value = m_doc->GetWorkspace().GetProfiles().GetRealizProb(node); m_nodeProb->SetLabel(wxT("Node reached: ") + wxString(value.c_str(), *wxConvCurrent)); GetSizer()->Show(m_nodeProb, true); m_infosetValue->SetForegroundColour(color); - value = m_doc->GetProfiles().GetInfosetValue(node); + value = m_doc->GetWorkspace().GetProfiles().GetInfosetValue(node); m_infosetValue->SetLabel(wxT("Infoset value: ") + wxString(value.c_str(), *wxConvCurrent)); GetSizer()->Show(m_infosetValue, true); m_infosetProb->SetForegroundColour(color); - value = m_doc->GetProfiles().GetInfosetProb(node); + value = m_doc->GetWorkspace().GetProfiles().GetInfosetProb(node); m_infosetProb->SetLabel(wxT("Infoset reached: ") + wxString(value.c_str(), *wxConvCurrent)); GetSizer()->Show(m_infosetProb, true); m_belief->SetForegroundColour(color); - value = m_doc->GetProfiles().GetBeliefProb(node); + value = m_doc->GetWorkspace().GetProfiles().GetBeliefProb(node); m_belief->SetLabel(wxT("Belief: ") + wxString(value.c_str(), *wxConvCurrent)); GetSizer()->Show(m_belief, true); } @@ -441,7 +441,7 @@ gbtTreePlayerToolbar::gbtTreePlayerToolbar(wxWindow *p_parent, GameDocument *p_d topSizer->Add(m_chancePanel, 0, wxALL | wxEXPAND, 5); - for (size_t pl = 1; pl <= m_doc->NumPlayers(); pl++) { + for (size_t pl = 1; pl <= m_doc->GetGame()->NumPlayers(); pl++) { m_playerPanels.push_back(new gbtTreePlayerPanel(this, m_doc, pl)); topSizer->Add(m_playerPanels[pl], 0, wxALL | wxEXPAND, 5); } @@ -452,13 +452,13 @@ gbtTreePlayerToolbar::gbtTreePlayerToolbar(wxWindow *p_parent, GameDocument *p_d void gbtTreePlayerToolbar::OnUpdate() { - while (m_playerPanels.size() < m_doc->NumPlayers()) { + while (m_playerPanels.size() < m_doc->GetGame()->NumPlayers()) { auto *panel = new gbtTreePlayerPanel(this, m_doc, m_playerPanels.size() + 1); m_playerPanels.push_back(panel); GetSizer()->Add(panel, 0, wxALL | wxEXPAND, 5); } - while (m_playerPanels.size() > m_doc->NumPlayers()) { + while (m_playerPanels.size() > m_doc->GetGame()->NumPlayers()) { gbtTreePlayerPanel *panel = m_playerPanels.back(); GetSizer()->Detach(panel); panel->Destroy(); diff --git a/src/gui/efgprofile.cc b/src/gui/efgprofile.cc index 73ba19805..df6be712e 100644 --- a/src/gui/efgprofile.cc +++ b/src/gui/efgprofile.cc @@ -56,7 +56,7 @@ BehaviorProfileList::~BehaviorProfileList() = default; void BehaviorProfileList::OnLabelClick(wxSheetEvent &p_event) { if (p_event.GetCol() == -1) { - m_doc->SetCurrentProfile(p_event.GetRow() + 1); + m_doc->DoSelectProfile(p_event.GetRow() + 1); } else { // Clicking on an action column sets the selected node to the first @@ -68,7 +68,7 @@ void BehaviorProfileList::OnLabelClick(wxSheetEvent &p_event) void BehaviorProfileList::OnCellClick(wxSheetEvent &p_event) { - m_doc->SetCurrentProfile(p_event.GetRow() + 1); + m_doc->DoSelectProfile(p_event.GetRow() + 1); } wxString BehaviorProfileList::GetCellValue(const wxSheetCoords &p_coords) @@ -88,7 +88,10 @@ wxString BehaviorProfileList::GetCellValue(const wxSheetCoords &p_coords) return wxT("#"); } - return {m_doc->GetProfiles().GetActionProb(p_coords.GetCol() + 1, p_coords.GetRow() + 1).c_str(), + return {m_doc->GetWorkspace() + .GetProfiles() + .GetActionProb(p_coords.GetCol() + 1, p_coords.GetRow() + 1) + .c_str(), *wxConvCurrent}; } @@ -100,7 +103,7 @@ static wxColour GetPlayerColor(const GameDocument *p_doc, int p_index) wxSheetCellAttr BehaviorProfileList::GetAttr(const wxSheetCoords &p_coords, wxSheetAttr_Type) const { - const int currentProfile = m_doc->GetCurrentProfile(); + const int currentProfile = m_doc->GetWorkspace().GetCurrentProfile(); if (IsRowLabelCell(p_coords)) { wxSheetCellAttr attr(GetSheetRefData()->m_defaultRowLabelAttr); @@ -161,12 +164,12 @@ wxSheetCellAttr BehaviorProfileList::GetAttr(const wxSheetCoords &p_coords, wxSh void BehaviorProfileList::OnUpdate() { - if (!m_doc->GetGame() || m_doc->NumProfileLists() == 0) { + if (!m_doc->GetGame() || m_doc->GetWorkspace().NumProfileLists() == 0) { DeleteRows(0, GetNumberRows()); return; } - const AnalysisOutput &profiles = m_doc->GetProfiles(); + const AnalysisOutput &profiles = m_doc->GetWorkspace().GetProfiles(); const int profileLength = m_doc->GetGame()->BehavProfileLength(); BeginBatch(); diff --git a/src/gui/gamedoc.cc b/src/gui/gamedoc.cc index 4d7d5b8d7..536b967c9 100644 --- a/src/gui/gamedoc.cc +++ b/src/gui/gamedoc.cc @@ -23,8 +23,6 @@ #include #include -#include - #include "gambit.h" #include "core/tinyxml.h" // for XML parser for LoadDocument() @@ -94,23 +92,134 @@ bool StrategyDominanceStack::PreviousLevel() } } +//========================================================================= +// class AnalysisWorkspace +//========================================================================= + +AnalysisWorkspace::AnalysisWorkspace(GameDocument *p_doc) + : m_doc(p_doc), m_stratSupports(p_doc, true), m_currentProfileList(0) +{ +} + +void AnalysisWorkspace::Clear() +{ + m_stratSupports.Reset(); + m_profiles.clear(); + m_currentProfileList = 0; +} + +void AnalysisWorkspace::ResetForGameChange() +{ + m_stratSupports.Reset(); + + // Even though modifications only to payoffs doesn't make the + // computed profiles invalid for the edited game, it does mean + // that, in general, they won't be Nash. For now, to avoid confusion, + // we will wipe them out. + m_profiles.clear(); + m_currentProfileList = 0; +} + +void AnalysisWorkspace::BuildNfg() +{ + m_stratSupports.Reset(); + std::for_each(m_profiles.begin(), m_profiles.end(), std::mem_fn(&AnalysisOutput::BuildNfg)); +} + +void AnalysisWorkspace::AddEquilibriumOutput(std::shared_ptr p_profs) +{ + m_profiles.push_back(p_profs); + m_currentProfileList = m_profiles.size(); +} + +void AnalysisWorkspace::SelectEquilibriumOutput(int p_index) { m_currentProfileList = p_index; } + +void AnalysisWorkspace::SelectProfile(int p_profile) +{ + m_profiles[m_currentProfileList]->SetCurrent(p_profile); +} + +void AnalysisWorkspace::SetDominanceStrictness(bool p_strict) +{ + m_stratSupports.SetStrict(p_strict); +} + +bool AnalysisWorkspace::GetStrategyElimStrength() const { return m_stratSupports.GetStrict(); } + +bool AnalysisWorkspace::NextDominanceLevel() { return m_stratSupports.NextLevel(); } + +void AnalysisWorkspace::PreviousDominanceLevel() { m_stratSupports.PreviousLevel(); } + +void AnalysisWorkspace::TopDominanceLevel() { m_stratSupports.TopLevel(); } + +bool AnalysisWorkspace::CanStrategyElim() const { return m_stratSupports.CanEliminate(); } + +int AnalysisWorkspace::GetStrategyElimLevel() const { return m_stratSupports.GetLevel(); } + +void AnalysisWorkspace::Save(std::ostream &p_file) const +{ + std::for_each(m_profiles.begin(), m_profiles.end(), + [&p_file](std::shared_ptr a) { a->Save(p_file); }); +} + +bool AnalysisWorkspace::Load(TiXmlNode *p_game) +{ + m_stratSupports.Reset(); + + m_profiles.clear(); + + for (TiXmlNode *analysis = p_game->FirstChild("analysis"); analysis; + analysis = analysis->NextSibling()) { + const char *type = analysis->ToElement()->Attribute("type"); + // const char *rep = analysis->ToElement()->Attribute("rep"); + if (type && !strcmp(type, "list")) { + // Read in a list of profiles + // We need to try to guess whether the profiles are float or rational + bool isFloat = false; + for (TiXmlNode *profile = analysis->FirstChild("profile"); profile; + profile = profile->NextSiblingElement()) { + if (std::string(profile->FirstChild()->Value()).find('.') != std::string::npos || + std::string(profile->FirstChild()->Value()).find('e') != std::string::npos) { + isFloat = true; + break; + } + } + + if (isFloat) { + auto plist = std::make_shared>(m_doc, false); + plist->Load(analysis); + m_profiles.push_back(plist); + } + else { + auto plist = std::make_shared>(m_doc, false); + plist->Load(analysis); + m_profiles.push_back(plist); + } + } + } + + m_currentProfileList = m_profiles.size(); + + return true; +} + //========================================================================= // class GameDocument //========================================================================= GameDocument::GameDocument(Game p_game) - : m_game(p_game), m_selectNode(nullptr), m_gameModified(false), m_unsavedResults(false), - m_stratSupports(this, true), m_currentProfileList(0) + : m_game(p_game), m_selectNode(nullptr), m_gameModified(false), m_workspaceModified(false), + m_workspace(this) { wxGetApp().AddDocument(this); std::ostringstream s; - SaveDocument(s); + SaveWorkspace(s); } GameDocument::~GameDocument() { wxGetApp().RemoveDocument(this); } -bool GameDocument::LoadDocument(const wxString &p_filename) +bool GameDocument::LoadWorkspace(const wxString &p_filename) { TiXmlDocument doc(p_filename.mb_str()); if (!doc.LoadFile()) { @@ -158,42 +267,10 @@ bool GameDocument::LoadDocument(const wxString &p_filename) return false; } - m_stratSupports.Reset(); - - m_profiles.clear(); - - for (TiXmlNode *analysis = game->FirstChild("analysis"); analysis; - analysis = analysis->NextSibling()) { - const char *type = analysis->ToElement()->Attribute("type"); - // const char *rep = analysis->ToElement()->Attribute("rep"); - if (type && !strcmp(type, "list")) { - // Read in a list of profiles - // We need to try to guess whether the profiles are float or rational - bool isFloat = false; - for (TiXmlNode *profile = analysis->FirstChild("profile"); profile; - profile = profile->NextSiblingElement()) { - if (std::string(profile->FirstChild()->Value()).find('.') != std::string::npos || - std::string(profile->FirstChild()->Value()).find('e') != std::string::npos) { - isFloat = true; - break; - } - } - - if (isFloat) { - auto plist = std::make_shared>(this, false); - plist->Load(analysis); - m_profiles.push_back(plist); - } - else { - auto plist = std::make_shared>(this, false); - plist->Load(analysis); - m_profiles.push_back(plist); - } - } + if (!m_workspace.Load(game)) { + return false; } - m_currentProfileList = m_profiles.size(); - TiXmlNode *colors = docroot->FirstChild("colors"); if (colors) { m_style.SetColorXML(colors); @@ -220,7 +297,7 @@ bool GameDocument::LoadDocument(const wxString &p_filename) return true; } -void GameDocument::SaveDocument(std::ostream &p_file) const +void GameDocument::SaveWorkspace(std::ostream &p_file) const { p_file << "\n"; @@ -249,31 +326,28 @@ void GameDocument::SaveDocument(std::ostream &p_file) const p_file << "\n"; } - std::for_each(m_profiles.begin(), m_profiles.end(), - [&p_file](std::shared_ptr a) { a->Save(p_file); }); + m_workspace.Save(p_file); p_file << "\n"; p_file << "\n"; } -void GameDocument::UpdateViews(GameModificationType p_modifications) +void GameDocument::NotifyChanged(GameModificationType p_modifications) { - if (p_modifications == GBT_DOC_MODIFIED_GAME || p_modifications == GBT_DOC_MODIFIED_PAYOFFS || - p_modifications == GBT_DOC_MODIFIED_LABELS) { - m_gameModified = true; - } - if (p_modifications == GBT_DOC_MODIFIED_GAME || p_modifications == GBT_DOC_MODIFIED_PAYOFFS) { - m_stratSupports.Reset(); - - // Even though modifications only to payoffs doesn't make the - // computed profiles invalid for the edited game, it does mean - // that, in general, they won't be Nash. For now, to avoid confusion, - // we will wipe them out. - m_profiles.clear(); - m_currentProfileList = 0; + m_gameModified |= HasModification(p_modifications, GameModificationType::GameForm | + GameModificationType::GamePayoffs | + GameModificationType::GameLabels); + m_workspaceModified |= HasModification(p_modifications, GameModificationType::Workspace); + if (HasModification(p_modifications, + GameModificationType::GameForm | GameModificationType::GamePayoffs)) { + m_workspace.ResetForGameChange(); } + UpdateViews(); +} +void GameDocument::UpdateViews() +{ std::for_each(m_views.begin(), m_views.end(), std::mem_fn(&GameView::OnUpdate)); } @@ -285,8 +359,7 @@ void GameDocument::PostPendingChanges() void GameDocument::BuildNfg() { if (m_game->IsTree()) { - m_stratSupports.Reset(); - std::for_each(m_profiles.begin(), m_profiles.end(), std::mem_fn(&AnalysisOutput::BuildNfg)); + m_workspace.BuildNfg(); } } @@ -308,63 +381,62 @@ GameAction GameDocument::GetAction(int p_index) const void GameDocument::SetStyle(const TreeRenderConfig &p_style) { m_style = p_style; - UpdateViews(GBT_DOC_MODIFIED_VIEWS); + NotifyChanged(GameModificationType::Presentation); } -void GameDocument::SetCurrentProfile(int p_profile) +void GameDocument::DoSelectProfile(int p_profile) { - m_profiles[m_currentProfileList]->SetCurrent(p_profile); - UpdateViews(GBT_DOC_MODIFIED_VIEWS); + m_workspace.SelectProfile(p_profile); + UpdateViews(); } -void GameDocument::AddProfileList(std::shared_ptr p_profs) +void GameDocument::DoAddEquilibriumOutput(std::shared_ptr p_profs) { - m_profiles.push_back(p_profs); - m_currentProfileList = m_profiles.size(); - UpdateViews(GBT_DOC_MODIFIED_VIEWS); + m_workspace.AddEquilibriumOutput(p_profs); + NotifyChanged(GameModificationType::Workspace); } -void GameDocument::SetProfileList(int p_index) +void GameDocument::DoAddOutput(AnalysisOutput &p_list, const wxString &p_output) { - m_currentProfileList = p_index; - UpdateViews(GBT_DOC_MODIFIED_VIEWS); + p_list.AddOutput(p_output); + NotifyChanged(GameModificationType::Workspace); } -void GameDocument::SetStrategyElimStrength(bool p_strict) +void GameDocument::DoSelectEquilibriumOutput(int p_index) { - m_stratSupports.SetStrict(p_strict); - UpdateViews(GBT_DOC_MODIFIED_VIEWS); + m_workspace.SelectEquilibriumOutput(p_index); + UpdateViews(); } -bool GameDocument::GetStrategyElimStrength() const { return m_stratSupports.GetStrict(); } +void GameDocument::DoSetDominanceStrictness(bool p_strict) +{ + m_workspace.SetDominanceStrictness(p_strict); + UpdateViews(); +} -bool GameDocument::NextStrategyElimLevel() +bool GameDocument::DoNextDominanceLevel() { - const bool ret = m_stratSupports.NextLevel(); - UpdateViews(GBT_DOC_MODIFIED_VIEWS); + const bool ret = m_workspace.NextDominanceLevel(); + UpdateViews(); return ret; } -void GameDocument::PreviousStrategyElimLevel() +void GameDocument::DoPreviousDominanceLevel() { - m_stratSupports.PreviousLevel(); - UpdateViews(GBT_DOC_MODIFIED_VIEWS); + m_workspace.PreviousDominanceLevel(); + UpdateViews(); } -void GameDocument::TopStrategyElimLevel() +void GameDocument::DoTopDominanceLevel() { - m_stratSupports.TopLevel(); - UpdateViews(GBT_DOC_MODIFIED_VIEWS); + m_workspace.TopDominanceLevel(); + UpdateViews(); } -bool GameDocument::CanStrategyElim() const { return m_stratSupports.CanEliminate(); } - -int GameDocument::GetStrategyElimLevel() const { return m_stratSupports.GetLevel(); } - void GameDocument::SetSelectNode(GameNode p_node) { m_selectNode = p_node; - UpdateViews(GBT_DOC_MODIFIED_VIEWS); + UpdateViews(); } //====================================================================== @@ -379,11 +451,11 @@ void GameDocument::DoSave(const wxString &p_filename, GameSaveFormat p_format) static_cast(p_filename.mb_str())); } switch (p_format) { - case GameSaveFormat::Workbook: - SaveDocument(file); + case GameSaveFormat::Workspace: + SaveWorkspace(file); m_filename = p_filename; m_gameModified = false; - m_unsavedResults = false; + m_workspaceModified = false; break; case GameSaveFormat::Efg: @@ -397,14 +469,14 @@ void GameDocument::DoSave(const wxString &p_filename, GameSaveFormat p_format) m_gameModified = false; break; } - UpdateViews(GBT_DOC_MODIFIED_NONE); + UpdateViews(); } void GameDocument::DoSetTitle(const wxString &p_title, const wxString &p_comment) { m_game->SetTitle(static_cast(p_title.mb_str())); m_game->SetDescription(static_cast(p_comment.mb_str())); - UpdateViews(GBT_DOC_MODIFIED_LABELS); + NotifyChanged(GameModificationType::GameLabels); } void GameDocument::DoNewPlayer() @@ -414,67 +486,67 @@ void GameDocument::DoNewPlayer() if (!m_game->IsTree()) { player->GetStrategy(1)->SetLabel("1"); } - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } void GameDocument::DoSetPlayerLabel(GamePlayer p_player, const wxString &p_label) { p_player->SetLabel(p_label.ToStdString()); - UpdateViews(GBT_DOC_MODIFIED_LABELS); + NotifyChanged(GameModificationType::GameLabels); } void GameDocument::DoNewStrategy(GamePlayer p_player) { m_game->NewStrategy(p_player, std::to_string(p_player->GetStrategies().size() + 1)); - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } void GameDocument::DoDeleteStrategy(GameStrategy p_strategy) { m_game->DeleteStrategy(p_strategy); - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } void GameDocument::DoSetStrategyLabel(GameStrategy p_strategy, const wxString &p_label) { p_strategy->SetLabel(p_label.ToStdString()); - UpdateViews(GBT_DOC_MODIFIED_LABELS); + NotifyChanged(GameModificationType::GameLabels); } void GameDocument::DoSetInfosetLabel(GameInfoset p_infoset, const wxString &p_label) { p_infoset->SetLabel(p_label.ToStdString()); - UpdateViews(GBT_DOC_MODIFIED_LABELS); + NotifyChanged(GameModificationType::GameLabels); } void GameDocument::DoSetActionLabel(GameAction p_action, const wxString &p_label) { p_action->SetLabel(p_label.ToStdString()); - UpdateViews(GBT_DOC_MODIFIED_LABELS); + NotifyChanged(GameModificationType::GameLabels); } void GameDocument::DoSetActionProbs(GameInfoset p_infoset, const Array &p_probs) { m_game->SetChanceProbs(p_infoset, p_probs); - UpdateViews(GBT_DOC_MODIFIED_PAYOFFS); + NotifyChanged(GameModificationType::GamePayoffs); } void GameDocument::DoSetInfoset(GameNode p_node, GameInfoset p_infoset) { m_game->SetInfoset(p_node, p_infoset); - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } void GameDocument::DoLeaveInfoset(GameNode p_node) { m_game->LeaveInfoset(p_node); - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } void GameDocument::DoRevealAction(GameInfoset p_infoset, GamePlayer p_player) { m_game->Reveal(p_infoset, p_player); - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } void GameDocument::DoInsertAction(GameNode p_node) @@ -484,43 +556,43 @@ void GameDocument::DoInsertAction(GameNode p_node) } const GameAction action = m_game->InsertAction(p_node->GetInfoset()); action->SetLabel(std::to_string(action->GetNumber())); - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } void GameDocument::DoSetNodeLabel(GameNode p_node, const wxString &p_label) { p_node->SetLabel(p_label.ToStdString()); - UpdateViews(GBT_DOC_MODIFIED_LABELS); + NotifyChanged(GameModificationType::GameLabels); } void GameDocument::DoAppendMove(GameNode p_node, GameInfoset p_infoset) { m_game->AppendMove(p_node, p_infoset); - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } void GameDocument::DoInsertMove(GameNode p_node, GamePlayer p_player, unsigned int p_actions) { m_game->InsertMove(p_node, p_player, p_actions, true); - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } void GameDocument::DoInsertMove(GameNode p_node, GameInfoset p_infoset) { m_game->InsertMove(p_node, p_infoset); - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } void GameDocument::DoCopyTree(GameNode p_destNode, GameNode p_srcNode) { m_game->CopyTree(p_destNode, p_srcNode); - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } void GameDocument::DoMoveTree(GameNode p_destNode, GameNode p_srcNode) { m_game->MoveTree(p_destNode, p_srcNode); - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } void GameDocument::DoDeleteParent(GameNode p_node) @@ -529,13 +601,13 @@ void GameDocument::DoDeleteParent(GameNode p_node) return; } m_game->DeleteParent(p_node); - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } void GameDocument::DoDeleteTree(GameNode p_node) { m_game->DeleteTree(p_node); - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } void GameDocument::DoSetPlayer(GameInfoset p_infoset, GamePlayer p_player) @@ -543,7 +615,7 @@ void GameDocument::DoSetPlayer(GameInfoset p_infoset, GamePlayer p_player) if (!p_player->IsChance() && !p_infoset->GetPlayer()->IsChance()) { // Currently don't support switching nodes to/from chance player m_game->SetPlayer(p_infoset, p_player); - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } } @@ -552,26 +624,26 @@ void GameDocument::DoSetPlayer(GameNode p_node, GamePlayer p_player) if (!p_player->IsChance() && !p_node->GetPlayer()->IsChance()) { // Currently don't support switching nodes to/from chance player m_game->SetPlayer(p_node->GetInfoset(), p_player); - UpdateViews(GBT_DOC_MODIFIED_GAME); + NotifyChanged(GameModificationType::GameForm); } } void GameDocument::DoNewOutcome(GameNode p_node) { m_game->SetOutcome(p_node, m_game->NewOutcome()); - UpdateViews(GBT_DOC_MODIFIED_PAYOFFS); + NotifyChanged(GameModificationType::GamePayoffs); } void GameDocument::DoNewOutcome(const PureStrategyProfile &p_profile) { p_profile->SetOutcome(m_game->NewOutcome()); - UpdateViews(GBT_DOC_MODIFIED_PAYOFFS); + NotifyChanged(GameModificationType::GamePayoffs); } void GameDocument::DoSetOutcome(GameNode p_node, GameOutcome p_outcome) { m_game->SetOutcome(p_node, p_outcome); - UpdateViews(GBT_DOC_MODIFIED_PAYOFFS); + NotifyChanged(GameModificationType::GamePayoffs); } void GameDocument::DoRemoveOutcome(GameNode p_node) @@ -580,7 +652,7 @@ void GameDocument::DoRemoveOutcome(GameNode p_node) return; } m_game->SetOutcome(p_node, nullptr); - UpdateViews(GBT_DOC_MODIFIED_PAYOFFS); + NotifyChanged(GameModificationType::GamePayoffs); } void GameDocument::DoCopyOutcome(GameNode p_node, GameOutcome p_outcome) @@ -591,19 +663,13 @@ void GameDocument::DoCopyOutcome(GameNode p_node, GameOutcome p_outcome) outcome->SetPayoff(player, p_outcome->GetPayoff(player)); } m_game->SetOutcome(p_node, outcome); - UpdateViews(GBT_DOC_MODIFIED_PAYOFFS); + NotifyChanged(GameModificationType::GamePayoffs); } void GameDocument::DoSetPayoff(GameOutcome p_outcome, int p_player, const wxString &p_value) { p_outcome->SetPayoff(m_game->GetPlayer(p_player), Number(p_value.ToStdString())); - UpdateViews(GBT_DOC_MODIFIED_PAYOFFS); -} - -void GameDocument::DoAddOutput(AnalysisOutput &p_list, const wxString &p_output) -{ - p_list.AddOutput(p_output); - UpdateViews(GBT_DOC_MODIFIED_NONE); + NotifyChanged(GameModificationType::GamePayoffs); } } // namespace Gambit::GUI diff --git a/src/gui/gamedoc.h b/src/gui/gamedoc.h index 32f2e9e1e..904bb05a6 100644 --- a/src/gui/gamedoc.h +++ b/src/gui/gamedoc.h @@ -27,6 +27,8 @@ #include "style.h" #include "analysis.h" +class TiXmlNode; + namespace Gambit::GUI { class GameView; @@ -122,15 +124,84 @@ class StrategyDominanceStack { // GBT_DOC_MODIFIED_LABELS: Labeling of players, strategies, etc. has // changed. // -// GBT_DOC_MODIFIED_VIEWS: Information about how the document is viewed -// (e.g., player colors) has changed. +// GBT_DOC_MODIFIED_WORKSPACE: Stored analysis workspace data has changed. +// This affects workbook/workspace persistence, but does not modify the +// underlying game model. +// +// GBT_DOC_MODIFIED_PRESENTATION: Settings on how information is rendered +// (for example, colors) have changed. // -using GameModificationType = enum { - GBT_DOC_MODIFIED_NONE = 0x00, - GBT_DOC_MODIFIED_GAME = 0x01, - GBT_DOC_MODIFIED_PAYOFFS = 0x02, - GBT_DOC_MODIFIED_LABELS = 0x04, - GBT_DOC_MODIFIED_VIEWS = 0x08 +enum class GameModificationType : unsigned int { + None = 0x00, + GameForm = 0x01, + GamePayoffs = 0x02, + GameLabels = 0x04, + Workspace = 0x08, + Presentation = 0x10 +}; + +inline GameModificationType operator|(GameModificationType p_left, GameModificationType p_right) +{ + return static_cast(static_cast(p_left) | + static_cast(p_right)); +} + +inline GameModificationType operator&(GameModificationType p_left, GameModificationType p_right) +{ + return static_cast(static_cast(p_left) & + static_cast(p_right)); +} + +inline GameModificationType &operator|=(GameModificationType &p_left, GameModificationType p_right) +{ + p_left = p_left | p_right; + return p_left; +} + +inline bool HasModification(GameModificationType p_modifications, GameModificationType p_test) +{ + return static_cast(p_modifications & p_test) != 0; +} + +class AnalysisWorkspace { + GameDocument *m_doc; + + StrategyDominanceStack m_stratSupports; + + Array> m_profiles; + int m_currentProfileList; + +public: + explicit AnalysisWorkspace(GameDocument *p_doc); + + void Clear(); + void ResetForGameChange(); + void BuildNfg(); + + const AnalysisOutput &GetProfiles() const { return *m_profiles[m_currentProfileList]; } + const AnalysisOutput &GetProfiles(int p_index) const { return *m_profiles[p_index]; } + void AddEquilibriumOutput(std::shared_ptr p_profs); + void SelectEquilibriumOutput(int p_index); + int NumProfileLists() const { return m_profiles.size(); } + int GetCurrentProfileList() const { return m_currentProfileList; } + + int GetCurrentProfile() const + { + return (m_profiles.size() == 0) ? 0 : GetProfiles().GetCurrent(); + } + void SelectProfile(int p_profile); + + const StrategySupportProfile &GetNfgSupport() const { return m_stratSupports.GetCurrent(); } + void SetDominanceStrictness(bool p_strict); + bool GetStrategyElimStrength() const; + bool NextDominanceLevel(); + void PreviousDominanceLevel(); + void TopDominanceLevel(); + bool CanStrategyElim() const; + int GetStrategyElimLevel() const; + + void Save(std::ostream &) const; + bool Load(TiXmlNode *p_game); }; class GameDocument { @@ -152,27 +223,25 @@ class GameDocument { TreeRenderConfig m_style; GameNode m_selectNode; - bool m_gameModified, m_unsavedResults; - - StrategyDominanceStack m_stratSupports; + bool m_gameModified, m_workspaceModified; - Array> m_profiles; - int m_currentProfileList; + AnalysisWorkspace m_workspace; - void UpdateViews(GameModificationType p_modifications); + void NotifyChanged(GameModificationType p_modifications); + void UpdateViews(); public: explicit GameDocument(Game p_game); ~GameDocument(); //! - //! @name Reading and writing .gbt savefiles + //! @name Reading and writing savefiles //! //@{ - /// Load document from the specified file (which should be a .gbt file) + /// Load workspace from the specified file (which should be a .gbt file) /// Returns true if successful, false if error - bool LoadDocument(const wxString &p_filename); - void SaveDocument(std::ostream &) const; + bool LoadWorkspace(const wxString &p_filename); + void SaveWorkspace(std::ostream &) const; //@} Game GetGame() const { return m_game; } @@ -181,57 +250,30 @@ class GameDocument { const wxString &GetFilename() const { return m_filename; } void SetFilename(const wxString &p_filename) { m_filename = p_filename; } - bool IsModified() const { return m_gameModified || m_unsavedResults; } + bool IsModified() const { return m_gameModified || m_workspaceModified; } bool IsGameModified() const { return m_gameModified; } - bool AreResultsUnsaved() const { return m_unsavedResults; } + bool IsWorkspaceModified() const { return m_workspaceModified; } void SetGameModified(bool p_modified) { m_gameModified = p_modified; } - void SetUnsavedResults(bool p_unsaved) { m_unsavedResults = p_unsaved; } + void SetWorkspaceModified(bool p_unsaved) { m_workspaceModified = p_unsaved; } + + const AnalysisWorkspace &GetWorkspace() const { return m_workspace; } const TreeRenderConfig &GetStyle() const { return m_style; } void SetStyle(const TreeRenderConfig &p_style); - size_t NumPlayers() const { return m_game->NumPlayers(); } - bool IsConstSum() const { return m_game->IsConstSum(); } - bool IsTree() const { return m_game->IsTree(); } GameAction GetAction(int p_index) const; - //! - //! @name Handling of list of computed profiles - //! - //@{ - const AnalysisOutput &GetProfiles() const { return *m_profiles[m_currentProfileList]; } - const AnalysisOutput &GetProfiles(int p_index) const { return *m_profiles[p_index]; } - void AddProfileList(std::shared_ptr p_profs); - void SetProfileList(int p_index); - int NumProfileLists() const { return m_profiles.size(); } - int GetCurrentProfileList() const { return m_currentProfileList; } + void DoAddEquilibriumOutput(std::shared_ptr p_profs); + void DoSelectEquilibriumOutput(int p_index); + void DoSelectProfile(int p_profile); - int GetCurrentProfile() const - { - return (m_profiles.size() == 0) ? 0 : GetProfiles().GetCurrent(); - } - void SetCurrentProfile(int p_profile); - - //! - //! @name Handling of behavior supports - //! - //@{ BehaviorSupportProfile GetEfgSupport() const { return BehaviorSupportProfile(m_game); } - //@} + const StrategySupportProfile &GetNfgSupport() const { return m_workspace.GetNfgSupport(); } - //! - //! @name Handling of strategy supports - //! - //@{ - const StrategySupportProfile &GetNfgSupport() const { return m_stratSupports.GetCurrent(); } - void SetStrategyElimStrength(bool p_strict); - bool GetStrategyElimStrength() const; - bool NextStrategyElimLevel(); - void PreviousStrategyElimLevel(); - void TopStrategyElimLevel(); - bool CanStrategyElim() const; - int GetStrategyElimLevel() const; - //@} + void DoSetDominanceStrictness(bool p_strict); + bool DoNextDominanceLevel(); + void DoPreviousDominanceLevel(); + void DoTopDominanceLevel(); GameNode GetSelectNode() const { return m_selectNode; } void SetSelectNode(GameNode); @@ -240,7 +282,7 @@ class GameDocument { void PostPendingChanges(); /// Operations on game model - enum class GameSaveFormat { Efg, Nfg, Workbook }; + enum class GameSaveFormat { Efg, Nfg, Workspace }; GameSaveFormat GetCurrentSaveFormat() const { if (m_filename.EndsWith(".efg")) { @@ -249,7 +291,7 @@ class GameDocument { if (m_filename.EndsWith(".nfg")) { return GameSaveFormat::Nfg; } - return GameSaveFormat::Workbook; + return GameSaveFormat::Workspace; } void DoSave(const wxString &p_filename, GameSaveFormat p_format); void DoSetTitle(const wxString &p_title, const wxString &p_comment); diff --git a/src/gui/gameframe.cc b/src/gui/gameframe.cc index 260030245..7ecf24684 100644 --- a/src/gui/gameframe.cc +++ b/src/gui/gameframe.cc @@ -84,7 +84,7 @@ ProfileListPanel::ProfileListPanel(wxWindow *p_parent, GameDocument *p_doc) { auto *topSizer = new wxBoxSizer(wxHORIZONTAL); - if (p_doc->IsTree()) { + if (p_doc->GetGame()->IsTree()) { m_behavProfiles = new BehaviorProfileList(this, p_doc); m_behavProfiles->Show(false); topSizer->Add(m_behavProfiles, 1, wxEXPAND, 0); @@ -160,21 +160,21 @@ void AnalysisNotebook::ShowMixed(bool p_show) { m_profiles->ShowMixed(p_show); } void AnalysisNotebook::OnChoice(wxCommandEvent &p_event) { - m_doc->SetProfileList(p_event.GetSelection() + 1); + m_doc->DoSelectEquilibriumOutput(p_event.GetSelection() + 1); } void AnalysisNotebook::OnUpdate() { m_choices->Clear(); - for (int i = 1; i <= m_doc->NumProfileLists(); i++) { + for (int i = 1; i <= m_doc->GetWorkspace().NumProfileLists(); i++) { wxString label; label << wxT("Profiles ") << i; m_choices->Append(label); } - m_choices->SetSelection(m_doc->GetCurrentProfileList() - 1); + m_choices->SetSelection(m_doc->GetWorkspace().GetCurrentProfileList() - 1); - if (m_doc->GetCurrentProfileList() > 0) { - m_description->SetLabel(m_doc->GetProfiles().GetDescription()); + if (m_doc->GetWorkspace().GetCurrentProfileList() > 0) { + m_description->SetLabel(m_doc->GetWorkspace().GetProfiles().GetDescription()); } } @@ -264,7 +264,7 @@ GameFrame::GameFrame(wxWindow *p_parent, GameDocument *p_doc) wxWindowBase::SetAcceleratorTable(accel); m_splitter = new wxSplitterWindow(this, wxID_ANY); - if (p_doc->IsTree()) { + if (p_doc->GetGame()->IsTree()) { m_efgPanel = new EfgPanel(m_splitter, p_doc); m_efgPanel->Show(true); m_splitter->Initialize(m_efgPanel); @@ -291,7 +291,7 @@ GameFrame::GameFrame(wxWindow *p_parent, GameDocument *p_doc) SetSizer(topSizer); wxTopLevelWindowBase::Layout(); - if (p_doc->IsTree()) { + if (p_doc->GetGame()->IsTree()) { m_efgPanel->SetFocus(); } else { @@ -335,11 +335,11 @@ void GameFrame::OnUpdate() GetToolBar()->EnableTool(GBT_MENU_EDIT_NEWPLAYER, !m_efgPanel || m_efgPanel->IsShown()); - menuBar->Enable(GBT_MENU_VIEW_PROFILES, m_doc->NumProfileLists() > 0); - GetToolBar()->EnableTool(GBT_MENU_VIEW_PROFILES, m_doc->NumProfileLists() > 0); + menuBar->Enable(GBT_MENU_VIEW_PROFILES, m_doc->GetWorkspace().NumProfileLists() > 0); + GetToolBar()->EnableTool(GBT_MENU_VIEW_PROFILES, m_doc->GetWorkspace().NumProfileLists() > 0); GetToolBar()->EnableTool(GBT_MENU_FORMAT_DECIMALS_DELETE, m_doc->GetStyle().NumDecimals() > 1); - if (m_doc->NumProfileLists() == 0 && m_splitter->IsSplit()) { + if (m_doc->GetWorkspace().NumProfileLists() == 0 && m_splitter->IsSplit()) { m_splitter->Unsplit(m_analysisPanel); } menuBar->Check(GBT_MENU_VIEW_PROFILES, m_splitter->IsSplit()); @@ -568,7 +568,7 @@ void GameFrame::MakeToolbar() toolBar->AddTool(GBT_MENU_EDIT_NEWPLAYER, wxEmptyString, wxBitmap(newplayer_xpm), wxNullBitmap, wxITEM_NORMAL, _("Add a new player"), _("Add a new player to the game"), nullptr); - if (m_doc->IsTree()) { + if (m_doc->GetGame()->IsTree()) { toolBar->AddTool(GBT_MENU_VIEW_ZOOMIN, wxEmptyString, wxBitmap(zoomin_xpm), wxNullBitmap, wxITEM_NORMAL, _("Zoom in"), _("Increase magnification"), nullptr); toolBar->AddTool(GBT_MENU_VIEW_ZOOMOUT, wxEmptyString, wxBitmap(zoomout_xpm), wxNullBitmap, @@ -590,7 +590,7 @@ void GameFrame::MakeToolbar() toolBar->AddSeparator(); - if (m_doc->IsTree()) { + if (m_doc->GetGame()->IsTree()) { toolBar->AddTool(GBT_MENU_VIEW_STRATEGIC, wxEmptyString, wxBitmap(table_xpm), wxNullBitmap, wxITEM_CHECK, _("Display the reduced strategic representation of the game"), _("Display the reduced strategic representation of the game"), nullptr); @@ -630,7 +630,7 @@ void GameFrame::OnFileOpen(wxCommandEvent &) { wxFileDialog dialog( this, _("Choose file to open"), wxGetApp().GetCurrentDir(), _T(""), - wxT("Gambit workbooks (*.gbt)|*.gbt|") wxT("Gambit extensive games (*.efg)|*.efg|") + wxT("Gambit workspaces (*.gbt)|*.gbt|") wxT("Gambit extensive games (*.efg)|*.efg|") wxT("Gambit strategic games (*.nfg)|*.nfg|") wxT("All files (*.*)|*.*")); if (dialog.ShowModal() == wxID_OK) { @@ -664,7 +664,7 @@ void GameFrame::OnFileSave(wxCommandEvent &p_event) wxFileDialog dialog( this, _("Save game as"), wxPathOnly(currentFilename), wxFileNameFromPath(currentFilename), - wxT("Gambit workbooks (*.gbt)|*.gbt|") wxT("Gambit extensive games (*.efg)|*.efg|") + wxT("Gambit workspaces (*.gbt)|*.gbt|") wxT("Gambit extensive games (*.efg)|*.efg|") wxT("Gambit strategic games (*.nfg)|*.nfg|") wxT("All files (*.*)|*.*"), wxFD_SAVE | wxFD_OVERWRITE_PROMPT); @@ -689,7 +689,7 @@ void GameFrame::OnFileSave(wxCommandEvent &p_event) break; } } - GameDocument::GameSaveFormat format = GameDocument::GameSaveFormat::Workbook; + GameDocument::GameSaveFormat format = GameDocument::GameSaveFormat::Workspace; if (filename.GetExt() == wxT("efg")) { format = GameDocument::GameSaveFormat::Efg; } @@ -867,19 +867,19 @@ void GameFrame::OnFileExportSVG(wxCommandEvent &) } } -void GameFrame::OnFileExit(wxCommandEvent &p_event) +void GameFrame::OnFileExit(wxCommandEvent &) { - if (wxGetApp().AreDocumentsModified()) { - if (wxMessageBox(wxT("There are modified games.\n") wxT("Any unsaved changes will be lost!\n") - wxT("Close anyway?"), - _("Warning"), wxOK | wxCANCEL) == wxCANCEL) { + while (auto *topWindow = wxGetApp().GetTopWindow()) { + topWindow->Raise(); + const auto before = wxGetApp().GetTopWindow(); + topWindow->Close(); + + // The close was vetoed, or otherwise did not destroy/advance the top window. + // In that case, abort application exit. + if (wxGetApp().GetTopWindow() == before) { return; } } - - while (wxGetApp().GetTopWindow()) { - delete wxGetApp().GetTopWindow(); - } } void GameFrame::OnFileMRUFile(wxCommandEvent &p_event) @@ -1201,7 +1201,7 @@ void GameFrame::OnToolsDominance(wxCommandEvent &p_event) wxPostEvent(m_nfgPanel, p_event); } if (!p_event.IsChecked()) { - m_doc->TopStrategyElimLevel(); + m_doc->DoTopDominanceLevel(); } } @@ -1291,18 +1291,17 @@ namespace { wxString CloseWarningMessage(GameDocument *p_doc) { - if (p_doc->IsGameModified() && !p_doc->AreResultsUnsaved()) { + if (p_doc->IsGameModified() && !p_doc->IsWorkspaceModified()) { return _("This game has unsaved changes.\n\n" "Close without saving?"); } - if (!p_doc->IsGameModified() && p_doc->AreResultsUnsaved()) { + if (!p_doc->IsGameModified() && p_doc->IsWorkspaceModified()) { return _("There are unsaved computational results.\n\n" - "These changes are not saved in ordinary game files.\n" + "These can be saved in a Gambit workspace file.\n" "Close without saving?"); } - if (p_doc->IsGameModified() && p_doc->AreResultsUnsaved()) { - return _("This game has unsaved changes, and there are unsaved computational results " - "unsaved computational results or workspace changes.\n\n" + if (p_doc->IsGameModified() && p_doc->IsWorkspaceModified()) { + return _("This game has unsaved changes, and there are unsaved computational results.\n\n" "Close without saving?"); } return wxEmptyString; diff --git a/src/gui/nfgpanel.cc b/src/gui/nfgpanel.cc index c72df8620..f1919505a 100644 --- a/src/gui/nfgpanel.cc +++ b/src/gui/nfgpanel.cc @@ -123,7 +123,7 @@ TablePlayerPanel::TablePlayerPanel(wxWindow *p_parent, NfgPanel *p_nfgPanel, Gam wxStaticBitmap *playerIcon = new TablePlayerIcon(this, m_player); labelSizer->Add(playerIcon, 0, wxALL | wxALIGN_CENTER, 0); - if (!m_doc->IsTree()) { + if (!m_doc->GetGame()->IsTree()) { auto *addStrategyIcon = new wxBitmapButton(this, wxID_ANY, wxBitmap(newrow_xpm), wxDefaultPosition, wxDefaultSize, wxNO_BORDER); addStrategyIcon->SetToolTip(_("Add a strategy for this player")); @@ -172,10 +172,10 @@ void TablePlayerPanel::OnUpdate() m_playerLabel->SetValue( wxString(m_doc->GetGame()->GetPlayer(m_player)->GetLabel().c_str(), *wxConvCurrent)); - if (m_doc->GetCurrentProfile() > 0) { + if (m_doc->GetWorkspace().GetCurrentProfile() > 0) { m_payoff->SetForegroundColour(color); - const std::string pay = m_doc->GetProfiles().GetPayoff(m_player); + const std::string pay = m_doc->GetWorkspace().GetProfiles().GetPayoff(m_player); m_payoff->SetLabel(wxT("Payoff: ") + wxString(pay.c_str(), *wxConvCurrent)); GetSizer()->Show(m_payoff, true); } @@ -264,7 +264,7 @@ TablePlayerToolbar::TablePlayerToolbar(NfgPanel *p_parent, GameDocument *p_doc) { auto *topSizer = new wxBoxSizer(wxVERTICAL); - for (size_t pl = 1; pl <= m_doc->NumPlayers(); pl++) { + for (size_t pl = 1; pl <= m_doc->GetGame()->NumPlayers(); pl++) { m_playerPanels.push_back(new TablePlayerPanel(this, p_parent, m_doc, pl)); topSizer->Add(m_playerPanels[pl], 0, wxALL | wxEXPAND, 5); } @@ -275,13 +275,13 @@ TablePlayerToolbar::TablePlayerToolbar(NfgPanel *p_parent, GameDocument *p_doc) void TablePlayerToolbar::OnUpdate() { - while (m_playerPanels.size() < m_doc->NumPlayers()) { + while (m_playerPanels.size() < m_doc->GetGame()->NumPlayers()) { auto *panel = new TablePlayerPanel(this, m_nfgPanel, m_doc, m_playerPanels.size() + 1); m_playerPanels.push_back(panel); GetSizer()->Add(panel, 0, wxALL | wxEXPAND, 5); } - while (m_playerPanels.size() > m_doc->NumPlayers()) { + while (m_playerPanels.size() > m_doc->GetGame()->NumPlayers()) { TablePlayerPanel *panel = m_playerPanels.back(); GetSizer()->Detach(panel); panel->Destroy(); @@ -380,39 +380,39 @@ StrategyDominanceToolbar::StrategyDominanceToolbar(wxWindow *p_parent, GameDocum void StrategyDominanceToolbar::OnStrength(wxCommandEvent &p_event) { - m_doc->SetStrategyElimStrength(p_event.GetSelection() == 0); + m_doc->DoSetDominanceStrictness(p_event.GetSelection() == 0); } -void StrategyDominanceToolbar::OnTopLevel(wxCommandEvent &) { m_doc->TopStrategyElimLevel(); } +void StrategyDominanceToolbar::OnTopLevel(wxCommandEvent &) { m_doc->DoTopDominanceLevel(); } void StrategyDominanceToolbar::OnPreviousLevel(wxCommandEvent &) { - m_doc->PreviousStrategyElimLevel(); + m_doc->DoPreviousDominanceLevel(); } -void StrategyDominanceToolbar::OnNextLevel(wxCommandEvent &) { m_doc->NextStrategyElimLevel(); } +void StrategyDominanceToolbar::OnNextLevel(wxCommandEvent &) { m_doc->DoNextDominanceLevel(); } void StrategyDominanceToolbar::OnLastLevel(wxCommandEvent &) { - while (m_doc->NextStrategyElimLevel()) + while (m_doc->DoNextDominanceLevel()) ; } void StrategyDominanceToolbar::OnUpdate() { - m_topButton->Enable(m_doc->GetStrategyElimLevel() > 1); - m_prevButton->Enable(m_doc->GetStrategyElimLevel() > 1); - m_nextButton->Enable(m_doc->CanStrategyElim()); - m_allButton->Enable(m_doc->CanStrategyElim()); - if (m_doc->GetStrategyElimLevel() == 1) { + m_topButton->Enable(m_doc->GetWorkspace().GetStrategyElimLevel() > 1); + m_prevButton->Enable(m_doc->GetWorkspace().GetStrategyElimLevel() > 1); + m_nextButton->Enable(m_doc->GetWorkspace().CanStrategyElim()); + m_allButton->Enable(m_doc->GetWorkspace().CanStrategyElim()); + if (m_doc->GetWorkspace().GetStrategyElimLevel() == 1) { m_level->SetLabel(wxT("All strategies shown")); } - else if (m_doc->GetStrategyElimLevel() == 2) { + else if (m_doc->GetWorkspace().GetStrategyElimLevel() == 2) { m_level->SetLabel(wxT("Eliminated 1 level")); } else { wxString label; - label << "Eliminated " << (m_doc->GetStrategyElimLevel() - 1) << " levels"; + label << "Eliminated " << (m_doc->GetWorkspace().GetStrategyElimLevel() - 1) << " levels"; m_level->SetLabel(label); } GetSizer()->Layout(); diff --git a/src/gui/nfgprofile.cc b/src/gui/nfgprofile.cc index 71f125afd..cebfac488 100644 --- a/src/gui/nfgprofile.cc +++ b/src/gui/nfgprofile.cc @@ -56,13 +56,13 @@ MixedProfileList::~MixedProfileList() = default; void MixedProfileList::OnLabelClick(wxSheetEvent &p_event) { if (p_event.GetCol() == -1) { - m_doc->SetCurrentProfile(RowToProfile(p_event.GetRow())); + m_doc->DoSelectProfile(RowToProfile(p_event.GetRow())); } } void MixedProfileList::OnCellClick(wxSheetEvent &p_event) { - m_doc->SetCurrentProfile(RowToProfile(p_event.GetRow())); + m_doc->DoSelectProfile(RowToProfile(p_event.GetRow())); } #ifdef UNUSED @@ -108,11 +108,17 @@ wxString MixedProfileList::GetCellValue(const wxSheetCoords &p_coords) const int profile = RowToProfile(p_coords.GetRow()); if (IsProbabilityRow(p_coords.GetRow())) { - return {m_doc->GetProfiles().GetStrategyProb(p_coords.GetCol() + 1, profile).c_str(), + return {m_doc->GetWorkspace() + .GetProfiles() + .GetStrategyProb(p_coords.GetCol() + 1, profile) + .c_str(), *wxConvCurrent}; } else { - return {m_doc->GetProfiles().GetStrategyValue(p_coords.GetCol() + 1, profile).c_str(), + return {m_doc->GetWorkspace() + .GetProfiles() + .GetStrategyValue(p_coords.GetCol() + 1, profile) + .c_str(), *wxConvCurrent}; } } @@ -132,7 +138,7 @@ static wxColour GetPlayerColor(const GameDocument *p_doc, int p_index) wxSheetCellAttr MixedProfileList::GetAttr(const wxSheetCoords &p_coords, wxSheetAttr_Type) const { - const int currentProfile = m_doc->GetCurrentProfile(); + const int currentProfile = m_doc->GetWorkspace().GetCurrentProfile(); if (IsRowLabelCell(p_coords)) { wxSheetCellAttr attr(GetSheetRefData()->m_defaultRowLabelAttr); @@ -178,12 +184,12 @@ wxSheetCellAttr MixedProfileList::GetAttr(const wxSheetCoords &p_coords, wxSheet void MixedProfileList::OnUpdate() { - if (m_doc->NumProfileLists() == 0) { + if (m_doc->GetWorkspace().NumProfileLists() == 0) { DeleteRows(0, GetNumberRows()); return; } - const AnalysisOutput &profiles = m_doc->GetProfiles(); + const AnalysisOutput &profiles = m_doc->GetWorkspace().GetProfiles(); BeginBatch(); diff --git a/src/gui/nfgtable.cc b/src/gui/nfgtable.cc index add5f6df4..97e50a7d0 100644 --- a/src/gui/nfgtable.cc +++ b/src/gui/nfgtable.cc @@ -660,7 +660,7 @@ void PayoffsWidget::OnUpdate() Refresh(); } -bool TableWidget::IsReadOnly() const { return m_doc->IsTree(); } +bool TableWidget::IsReadOnly() const { return m_doc->GetGame()->IsTree(); } wxColour TableWidget::GetPlayerColor(int player) const { diff --git a/src/gui/nfgtable.h b/src/gui/nfgtable.h index 34bc459b8..3fb177b02 100644 --- a/src/gui/nfgtable.h +++ b/src/gui/nfgtable.h @@ -47,13 +47,13 @@ class StrategicTableLayout { public: explicit StrategicTableLayout(GameDocument *doc) : m_doc(doc) { - if (m_doc->NumPlayers() >= 1) { + if (m_doc->GetGame()->NumPlayers() >= 1) { m_rowPlayers.push_back(1); } - if (m_doc->NumPlayers() >= 2) { + if (m_doc->GetGame()->NumPlayers() >= 2) { m_colPlayers.push_back(2); } - for (int pl = 3; pl <= m_doc->NumPlayers(); ++pl) { + for (int pl = 3; pl <= m_doc->GetGame()->NumPlayers(); ++pl) { m_rowPlayers.push_back(pl); } } @@ -74,7 +74,7 @@ class StrategicTableLayout { void ReconcilePlayers() { - const int maxPlayer = static_cast(m_doc->NumPlayers()); + const int maxPlayer = static_cast(m_doc->GetGame()->NumPlayers()); m_rowPlayers.erase(std::remove_if(m_rowPlayers.begin(), m_rowPlayers.end(), [maxPlayer](int pl) { return pl > maxPlayer; }), @@ -175,7 +175,7 @@ class StrategicTableLayout { int ColToStrategy(int player, int col) const { - const int strat = col / m_doc->NumPlayers() / NumColsSpanned(player); + const int strat = col / m_doc->GetGame()->NumPlayers() / NumColsSpanned(player); return (strat % NumStrategies(m_doc->GetNfgSupport(), GetColPlayer(player)) + 1); } @@ -183,10 +183,13 @@ class StrategicTableLayout { int GetRowHeaderColCount() const { return NumRowPlayers(); } int GetColHeaderRowCount() const { return NumColPlayers(); } - int GetColHeaderColCount() const { return NumColContingencies() * m_doc->NumPlayers(); } + int GetColHeaderColCount() const + { + return NumColContingencies() * m_doc->GetGame()->NumPlayers(); + } int GetPayoffRowCount() const { return NumRowContingencies(); } - int GetPayoffColCount() const { return NumColContingencies() * m_doc->NumPlayers(); } + int GetPayoffColCount() const { return NumColContingencies() * m_doc->GetGame()->NumPlayers(); } int GetRowHeaderPlayer(int headerCol) const { return GetRowPlayer(headerCol + 1); } @@ -206,12 +209,12 @@ class StrategicTableLayout { int GetColHeaderColSpan(int headerRow) const { - return NumColsSpanned(headerRow + 1) * m_doc->NumPlayers(); + return NumColsSpanned(headerRow + 1) * m_doc->GetGame()->NumPlayers(); } int GetPayoffPlayerForColumn(int payoffCol) const { - const int index = payoffCol % m_doc->NumPlayers() + 1; + const int index = payoffCol % m_doc->GetGame()->NumPlayers() + 1; if (index <= NumRowPlayers()) { return GetRowPlayer(index); }