Skip to content

Commit d1b8dca

Browse files
authored
Correct validation of action probabilities. (#798)
This fixes a problem first observed in the graphical interface (#788), in which validation of chance probabilities was not being handled correctly. The slightly deeper issue was that validation was not exposed separately. The solution is to refactor and expose a validation function, so the validation logic is available both in setting the probabilities, but also in validating the dialog. This also adds a few small UX and wxWidgets modernisations. Fixes #788.
1 parent 42995dd commit d1b8dca

4 files changed

Lines changed: 47 additions & 61 deletions

File tree

src/games/game.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,21 @@ inline GameNodeRep::Actions::iterator::iterator(GameInfosetRep::Actions::iterato
593593

594594
inline GameNode GameNodeRep::Actions::iterator::GetOwner() const { return m_child_it.GetOwner(); }
595595

596+
inline void ValidateDistribution(const Array<Number> &p_probs, const bool p_normalized = true)
597+
{
598+
if (std::any_of(p_probs.begin(), p_probs.end(),
599+
[](const Number &x) { return static_cast<Rational>(x) < Rational(0); })) {
600+
throw ValueException("Probabilities must be non-negative numbers");
601+
}
602+
if (!p_normalized) {
603+
return;
604+
}
605+
if (sum_function(p_probs, [](const Number &n) { return static_cast<Rational>(n); }) !=
606+
Rational(1)) {
607+
throw ValueException("Probabilities must sum to exactly one");
608+
}
609+
}
610+
596611
enum class TraversalOrder { Preorder, Postorder };
597612

598613
class CartesianProductSpace {

src/games/gametree.cc

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,17 +1324,8 @@ Game GameTreeRep::SetChanceProbs(const GameInfoset &p_infoset, const Array<Numbe
13241324
if (p_infoset->m_actions.size() != p_probs.size()) {
13251325
throw DimensionException("The number of probabilities given must match the number of actions");
13261326
}
1327+
ValidateDistribution(p_probs);
13271328
IncrementVersion();
1328-
if (std::any_of(p_probs.begin(), p_probs.end(),
1329-
[](const Number &x) { return static_cast<Rational>(x) < Rational(0); })) {
1330-
throw ValueException("Probabilities must be non-negative numbers");
1331-
}
1332-
auto sum = std::accumulate(
1333-
p_probs.begin(), p_probs.end(), Rational(0),
1334-
[](const Rational &r, const Number &n) { return r + static_cast<Rational>(n); });
1335-
if (sum != Rational(1)) {
1336-
throw ValueException("Probabilities must sum to exactly one");
1337-
}
13381329
std::copy(p_probs.begin(), p_probs.end(), p_infoset->m_probs.begin());
13391330
ClearComputedValues();
13401331
return shared_from_this();

src/gui/dleditmove.cc

Lines changed: 31 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@
3232
#include "renratio.h"
3333

3434
namespace Gambit::GUI {
35+
3536
class ActionSheet final : public wxSheet {
36-
GameInfoset m_infoset;
37+
GameInfoset m_infoset{nullptr};
38+
wxSheetCellAttr m_labelAttr;
39+
wxFont m_labelFont, m_cellFont;
3740

3841
// Overriding wxSheet members
3942
wxSheetCellAttr GetAttr(const wxSheetCoords &p_coords, wxSheetAttr_Type) const override;
@@ -48,8 +51,17 @@ class ActionSheet final : public wxSheet {
4851
};
4952

5053
ActionSheet::ActionSheet(wxWindow *p_parent, const GameInfoset &p_infoset)
51-
: wxSheet(p_parent, wxID_ANY), m_infoset(p_infoset)
54+
: wxSheet(p_parent, wxID_ANY), m_infoset(p_infoset), m_labelFont(GetFont()),
55+
m_cellFont(GetFont())
5256
{
57+
m_labelFont.MakeBold();
58+
59+
m_labelAttr = GetSheetRefData()->m_defaultRowLabelAttr;
60+
m_labelAttr.SetFont(m_labelFont);
61+
m_labelAttr.SetAlignment(wxALIGN_CENTER, wxALIGN_CENTER);
62+
m_labelAttr.SetOrientation(wxHORIZONTAL);
63+
m_labelAttr.SetReadOnly(true);
64+
5365
CreateGrid(p_infoset->GetActions().size(), (p_infoset->IsChanceInfoset()) ? 2 : 1);
5466
SetRowLabelWidth(40);
5567
SetColLabelHeight(25);
@@ -103,29 +115,12 @@ Array<Number> ActionSheet::GetActionProbs()
103115

104116
wxSheetCellAttr ActionSheet::GetAttr(const wxSheetCoords &p_coords, wxSheetAttr_Type) const
105117
{
106-
if (IsRowLabelCell(p_coords)) {
107-
wxSheetCellAttr attr(GetSheetRefData()->m_defaultRowLabelAttr);
108-
attr.SetFont(wxFont(10, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD));
109-
attr.SetAlignment(wxALIGN_CENTER, wxALIGN_CENTER);
110-
attr.SetOrientation(wxHORIZONTAL);
111-
attr.SetReadOnly(true);
112-
return attr;
113-
}
114-
else if (IsColLabelCell(p_coords)) {
115-
wxSheetCellAttr attr(GetSheetRefData()->m_defaultColLabelAttr);
116-
attr.SetFont(wxFont(10, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD));
117-
attr.SetAlignment(wxALIGN_CENTER, wxALIGN_CENTER);
118-
attr.SetOrientation(wxHORIZONTAL);
119-
attr.SetReadOnly(true);
120-
return attr;
121-
}
122-
else if (IsCornerLabelCell(p_coords)) {
123-
const wxSheetCellAttr attr(GetSheetRefData()->m_defaultCornerLabelAttr);
124-
return attr;
118+
if (IsLabelCell(p_coords)) {
119+
return m_labelAttr;
125120
}
126121

127122
wxSheetCellAttr attr(GetSheetRefData()->m_defaultGridCellAttr);
128-
attr.SetFont(wxFont(10, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL));
123+
attr.SetFont(m_cellFont);
129124
attr.SetAlignment(wxALIGN_CENTER, wxALIGN_CENTER);
130125
attr.SetOrientation(wxHORIZONTAL);
131126
attr.SetReadOnly(false);
@@ -143,9 +138,7 @@ wxSheetCellAttr ActionSheet::GetAttr(const wxSheetCoords &p_coords, wxSheetAttr_
143138
// class EditMoveDialog
144139
//======================================================================
145140

146-
wxBEGIN_EVENT_TABLE(EditMoveDialog, wxDialog) EVT_BUTTON(wxID_OK, EditMoveDialog::OnOK)
147-
wxEND_EVENT_TABLE() EditMoveDialog::EditMoveDialog(wxWindow *p_parent,
148-
const GameInfoset &p_infoset)
141+
EditMoveDialog::EditMoveDialog(wxWindow *p_parent, const GameInfoset &p_infoset)
149142
: wxDialog(p_parent, wxID_ANY, _("Move properties"), wxDefaultPosition), m_infoset(p_infoset)
150143
{
151144
auto *topSizer = new wxBoxSizer(wxVERTICAL);
@@ -171,6 +164,7 @@ wxBEGIN_EVENT_TABLE(EditMoveDialog, wxDialog) EVT_BUTTON(wxID_OK, EditMoveDialog
171164
if (p_infoset->IsChanceInfoset()) {
172165
m_player->Append(_("Chance"));
173166
m_player->SetSelection(0);
167+
m_player->Disable();
174168
}
175169
else {
176170
for (const auto &player : p_infoset->GetGame()->GetPlayers()) {
@@ -189,18 +183,13 @@ wxBEGIN_EVENT_TABLE(EditMoveDialog, wxDialog) EVT_BUTTON(wxID_OK, EditMoveDialog
189183
actionBoxSizer->Add(m_actionSheet, 1, wxALL | wxEXPAND, 5);
190184
topSizer->Add(actionBoxSizer, 0, wxALL | wxEXPAND, 5);
191185

192-
auto *buttonSizer = new wxBoxSizer(wxHORIZONTAL);
193-
buttonSizer->Add(new wxButton(this, wxID_CANCEL, _("Cancel")), 0, wxALL, 5);
194-
auto *okButton = new wxButton(this, wxID_OK, _("OK"));
195-
okButton->SetDefault();
196-
buttonSizer->Add(okButton, 0, wxALL, 5);
197-
topSizer->Add(buttonSizer, 0, wxALL | wxALIGN_RIGHT, 5);
198-
199-
SetSizer(topSizer);
200-
topSizer->Fit(this);
201-
topSizer->SetSizeHints(this);
202-
wxTopLevelWindowBase::Layout();
186+
if (auto *buttons = CreateSeparatedButtonSizer(wxOK | wxCANCEL)) {
187+
topSizer->Add(buttons, 0, wxALL | wxEXPAND, 5);
188+
}
189+
SetSizerAndFit(topSizer);
203190
CenterOnParent();
191+
192+
Bind(wxEVT_BUTTON, &EditMoveDialog::OnOK, this, wxID_OK);
204193
}
205194

206195
void EditMoveDialog::OnOK(wxCommandEvent &p_event)
@@ -209,23 +198,16 @@ void EditMoveDialog::OnOK(wxCommandEvent &p_event)
209198
p_event.Skip();
210199
return;
211200
}
212-
auto probs = m_actionSheet->GetActionProbs();
213-
if (std::any_of(probs.begin(), probs.end(),
214-
[](const Number &p) { return static_cast<Rational>(p) < Rational(0); })) {
215-
wxMessageBox("Action probabilities must be non-negative numbers.", "Error");
216-
return;
217-
}
218-
if (std::accumulate(probs.begin(), probs.end(), Rational(0),
219-
[](const Rational &s, const Number &p) {
220-
return s + static_cast<Rational>(p);
221-
}) != Rational(1)) {
222-
p_event.Skip();
201+
try {
202+
ValidateDistribution(m_actionSheet->GetActionProbs());
223203
}
224-
else {
225-
wxRichMessageDialog(this, "Action probabilities must sum to exactly one", "Error",
204+
catch (ValueException &) {
205+
wxRichMessageDialog(this, "Probabilities must be nonnegative numbers summing to one.", "Error",
226206
wxOK | wxCENTRE | wxICON_ERROR)
227207
.ShowModal();
208+
return;
228209
}
210+
p_event.Skip();
229211
}
230212

231213
int EditMoveDialog::NumActions() const { return m_actionSheet->NumActions(); }

src/gui/dleditmove.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ class EditMoveDialog final : public wxDialog {
4545
int NumActions() const;
4646
wxString GetActionName(int p_act) const;
4747
Array<Number> GetActionProbs() const;
48-
49-
wxDECLARE_EVENT_TABLE();
5048
};
5149
} // namespace Gambit::GUI
5250

0 commit comments

Comments
 (0)