diff --git a/ChangeLog b/ChangeLog index 843ce768cc..f172052f4b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,6 +3,8 @@ ### Added - Added `Game.has_perfect_recall(player)`, and `GameRep::HasPerfectRecall(const GamePlayer &)` in C++, reporting whether an individual player has perfect recall. (#1107) +- Added `MixedBehaviorProfile.event_probs`, returning the probability with which each of + the chance player's events is reached, as an `EventProbVector`. (#1109) - Added `Game.get_outcomes()`, returning a materialized list of outcome labels, replacing `Game.outcomes`. - Added `Game.get_outcome_payoffs(label)`, returning the payoff to each player at the outcome @@ -15,6 +17,8 @@ `Game.relabel_strategies`. Added `GameRep::RelabelOutcomes` in C++. ### Changed +- `MixedBehaviorProfile.infoset_probs` now covers only the personal players' information sets, + as `infoset_values` and `infoset_regrets`; the chance player's events moved to `event_probs`. (#1109) - `Game.make_outcome` now returns `None` instead of the `Outcome` created; use the `label` already passed in, together with `Game.get_outcome_payoffs`/`Game.relabel_outcomes`, to refer to it afterward. diff --git a/doc/formats.efg.rst b/doc/formats.efg.rst index 1bc78c3628..cb0abf61a6 100644 --- a/doc/formats.efg.rst +++ b/doc/formats.efg.rst @@ -105,18 +105,21 @@ generates these numbers as 1, 2, etc. as they appear first in the file, but there are no requirements other than uniqueness. The same integer may be used to specify information sets for different players; this is not ambiguous since the player number appears as well. +The chance player's events are numbered in the same way, uniquely among +the events; since chance nodes carry no player number, an event number +cannot be confused with an information set number. Finally, outcomes are also arbitrarily numbered in the file format in the same way in which information sets are, except for the special number 0 which is reserved to indicate the null outcome. Outcome 0 must not have a name or payoffs specified. -Information sets and outcomes may (and frequently will) appear +Information sets, events, and outcomes may (and frequently will) appear multiple times within a game. By convention, the second and subsequent -times an information set or outcome appears, the file may omit the -descriptive information for that information set or outcome. +times an information set, event, or outcome appears, the file may omit the +descriptive information for it. Alternatively, the file may specify the descriptive information again; -however, it must precisely match the original declaration of the -information set or outcome. Any mismatch in repeated declarations +however, it must precisely match the original declaration. +Any mismatch in repeated declarations is an error, and the file is not valid. If any part of the description is omitted, the whole description must be omitted. @@ -138,8 +141,8 @@ with the character c . Following this, in order, are + a text string, giving the name of the node -+ a positive integer specifying the information set number -+ (optional) the name of the information set and a list of actions at the information set with their ++ a positive integer specifying the event number ++ (optional) the name of the event and a list of actions at the event with their corresponding probabilities + a nonnegative integer specifying the outcome + (optional) the name of the outcome and the payoffs to each player for the outcome diff --git a/doc/gui.efg.building.rst b/doc/gui.efg.building.rst index c3f5d9e1ea..47900b4e87 100644 --- a/doc/gui.efg.building.rst +++ b/doc/gui.efg.building.rst @@ -86,13 +86,13 @@ Editing moves ------------- The properties of an existing move -- the label of its information -set, the player to which it belongs, and the actions available at it --- can be changed by right-clicking on any node belonging to the move +set or event, the player to which it belongs, and the actions available +at it -- can be changed by right-clicking on any node belonging to the move and choosing :guilabel:`Move properties`. This displays the :guilabel:`Move properties` dialog, which shows a dropdown for the player to which the move belongs (each player again shown with its colour), a text -field for the information set's label, and a row for each action, showing -its label and, for moves belonging to the chance player, its probability. +field for the information set's or event's label, and a row for each action, +showing its label and, for moves belonging to the chance player, its probability. Adding, removing, and reordering actions ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -205,7 +205,9 @@ A node can be placed in the same information set as another using the drag-and-drop idiom described in :ref:`copying-trees`: dragging one node onto another and choosing :guilabel:`Put node in same information set` (or, when dropping on a terminal node, :guilabel:`Insert move -using same information set`) from the popup menu. This is currently the +using same information set`) from the popup menu. When the node being +dragged belongs to the chance player, these read :guilabel:`Put node in +same event` and :guilabel:`Insert move using same event`. This is currently the only way to have a newly created move share an information set with an existing one; the :ref:`per-player submenu ` for adding a move always creates a fresh information set. @@ -225,7 +227,9 @@ a list of all other information sets in the game which are compatible with the node, that is, which have the same number of actions. Additionally, the node can be moved to a new, singleton information set by setting this dropdown to the :guilabel:`New -information set` entry. +information set` entry. For a node belonging to the chance player the +dropdown is labeled :guilabel:`Event`, lists the game's events, and +offers :guilabel:`New event` instead. diff --git a/doc/gui.nash.rst b/doc/gui.nash.rst index 5a07164af0..e139f46985 100644 --- a/doc/gui.nash.rst +++ b/doc/gui.nash.rst @@ -181,7 +181,8 @@ also names the player who moves there and shows the probability of reaching the node, the probability of reaching its information set, that player's belief about being at this particular node conditional on having reached the information set, and the information set's value to the -player who moves there. +player who moves there. For a node belonging to the chance player, the +popup refers to its event instead of an information set throughout. The computed profiles can also be viewed in the reduced strategic game. Clicking on the strategic game icon changes the view to the diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index eb679d381e..1b854da18f 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -234,6 +234,7 @@ Probability distributions over behavior MixedBehaviorProfile.node_values MixedBehaviorProfile.realiz_probs MixedBehaviorProfile.infoset_probs + MixedBehaviorProfile.event_probs MixedBehaviorProfile.beliefs MixedBehaviorProfile.agent_max_regret MixedBehaviorProfile.agent_liap_value @@ -255,6 +256,7 @@ Probability distributions over behavior InfosetValueVector InfosetRegretVector InfosetProbVector + EventProbVector ActionValueVector ActionRegretVector ActionValuesVector diff --git a/src/games/file.cc b/src/games/file.cc index 764ebd571d..5d1104e70a 100644 --- a/src/games/file.cc +++ b/src/games/file.cc @@ -762,7 +762,7 @@ void CheckChanceProbs(const GameFileLexer &p_state, const int p_infosetId, const GameInfoset &p_infoset, const Array &p_probs) { if (p_infoset->GetActions().size() != p_probs.size()) { - p_state.OnParseError("Chance infoset probability count mismatch " + p_state.OnParseError("Event probability count mismatch " "(infoset " + std::to_string(p_infosetId) + ")"); } @@ -770,7 +770,7 @@ void CheckChanceProbs(const GameFileLexer &p_state, const int p_infosetId, for (size_t i = 1; i <= p_probs.size(); ++i) { if (static_cast(p_infoset->GetActionProb(*action_it)) != static_cast(p_probs[i])) { - p_state.OnParseError("Chance infoset probabilities do not match previous definition " + p_state.OnParseError("Event probabilities do not match previous definition " "(infoset " + std::to_string(p_infosetId) + ")"); } diff --git a/src/gui/dleditmove.cc b/src/gui/dleditmove.cc index e1d6d1d89a..a22b39d1c0 100644 --- a/src/gui/dleditmove.cc +++ b/src/gui/dleditmove.cc @@ -48,7 +48,7 @@ const wxColour kNewLabelColour(0, 102, 0); // new action: bold, dark green const wxColour kRenamedLabelColour(0, 51, 204); // renamed action: italic, strong blue } // namespace -// An editable, variable-length list of the actions at an information set, colour-coded like +// An editable, variable-length list of the actions at an infoset, colour-coded like // a source-control diff (added/renamed/deleted). // // Each row's *stable label* (its label before this dialog touched it, or a placeholder for a @@ -459,7 +459,8 @@ wxString ActionPanel::ValidateLabels() if (invalid && message.empty()) { message = value.empty() ? _("Action labels cannot be empty.") - : _("Action labels must be unique within the information set."); + : m_isChance ? _("Action labels must be unique within the event.") + : _("Action labels must be unique within the infoset."); } } return message; @@ -528,8 +529,10 @@ EditMoveDialog::EditMoveDialog(wxWindow *p_parent, const std::shared_ptrAdd(new wxStaticText(m_headerPanel, wxID_STATIC, _("Information set label")), 0, - wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(7)); + labelSizer->Add(new wxStaticText(m_headerPanel, wxID_STATIC, + p_infoset->IsChanceInfoset() ? _("Event label") + : _("Information set label")), + 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(7)); m_infosetLabel = new LabelTextCtrl(m_headerPanel, wxID_ANY, wxString::FromUTF8(p_infoset->GetLabel())); m_infosetLabelDefaultBg = m_infosetLabel->GetBackgroundColour(); @@ -606,7 +609,9 @@ void EditMoveDialog::UpdateValidation() m_infosetLabel->SetBackgroundColour(infosetValid ? m_infosetLabelDefaultBg : kInvalidLabelBg); m_infosetLabel->Refresh(); if (!infosetValid) { - message = _("Information set label must be unique for the player."); + message = m_infoset->IsChanceInfoset() + ? _("Event label must be unique.") + : _("Information set label must be unique for the player."); } const wxString actionMessage = m_actionPanel->ValidateLabels(); diff --git a/src/gui/dleditmove.h b/src/gui/dleditmove.h index 4e237c9643..97f3a623f7 100644 --- a/src/gui/dleditmove.h +++ b/src/gui/dleditmove.h @@ -64,7 +64,7 @@ class EditMoveDialog final : public wxDialog { std::string GetStableLabel(int p_index) const; // The label as currently typed into the row at position `p_index`. wxString GetActionLabel(int p_index) const; - // The probability currently typed into the row at position `p_index` (chance infosets only). + // The probability currently typed into the row at position `p_index` (events only). Number GetActionProb(int p_index) const; }; } // namespace Gambit::GUI diff --git a/src/gui/dleditnode.cc b/src/gui/dleditnode.cc index f69378cf46..ed47d355d9 100644 --- a/src/gui/dleditnode.cc +++ b/src/gui/dleditnode.cc @@ -48,19 +48,20 @@ EditNodeDialog::EditNodeDialog(wxWindow *p_parent, const GameNode &p_node) labelSizer->Add(m_nodeLabel, 1, wxALL | wxCENTER | wxEXPAND, S); topSizer->Add(labelSizer, 0, wxALL | wxEXPAND, S); + const bool isChance = !p_node->IsTerminal() && p_node->GetInfoset()->IsChanceInfoset(); auto *infosetSizer = new wxBoxSizer(wxHORIZONTAL); - infosetSizer->Add(new wxStaticText(this, wxID_STATIC, _("Information set")), 0, wxALL | wxCENTER, - S); + infosetSizer->Add(new wxStaticText(this, wxID_STATIC, isChance ? _("Event") : _("Infoset")), 0, + wxALL | wxCENTER, S); m_infoset = new wxChoice(this, wxID_ANY); if (!p_node->IsTerminal()) { - m_infoset->Append(_("New information set")); - if (p_node->GetInfoset()->IsChanceInfoset()) { + m_infoset->Append(isChance ? _("New event") : _("New infoset")); + if (isChance) { int selection = 0; for (const auto &infoset : p_node->GetGame()->GetChance()->GetInfosets()) { if (infoset->GetActions().size() == p_node->GetChildren().size()) { m_infosetList.push_back(infoset); wxString label; - label << _("Chance infoset ") << infoset->GetNumber(); + label << _("Event ") << infoset->GetNumber(); m_infoset->Append(label); if (infoset == p_node->GetInfoset()) { selection = m_infosetList.size(); diff --git a/src/gui/efgdragdrop.cc b/src/gui/efgdragdrop.cc index a1191104ac..283b536ef8 100644 --- a/src/gui/efgdragdrop.cc +++ b/src/gui/efgdragdrop.cc @@ -258,11 +258,14 @@ bool EfgDisplay::ShowTreeDropMenu(const GameNode &p_targetNode, const GameNode & menu.AppendSeparator(); } + const bool isChance = p_sourceNode->GetPlayer()->IsChance(); if (p_targetNode->IsTerminal()) { - menu.Append(infosetId, _("Insert move using same information set")); + menu.Append(infosetId, isChance ? _("Insert move using same event") + : _("Insert move using same information set")); } else { - menu.Append(infosetId, _("Put node in same information set")); + menu.Append(infosetId, + isChance ? _("Put node in same event") : _("Put node in same information set")); } } diff --git a/src/gui/efgtooltip.cc b/src/gui/efgtooltip.cc index e6781dd19b..3217756165 100644 --- a/src/gui/efgtooltip.cc +++ b/src/gui/efgtooltip.cc @@ -1163,6 +1163,7 @@ wxString AppendMovePopup::ValidateLabels() if (invalid && message.empty()) { message = value.empty() ? _("Action labels cannot be empty.") + : m_isChance ? _("Action labels must be unique within the event.") : _("Action labels must be unique within the information set."); } } @@ -1509,11 +1510,13 @@ void NodeInfoPopup::ShowForNode(const GameNode &p_node) m_moveHeading->SetLabel(wxString::FromUTF8(mover->GetLabel()) + _(" to move")); m_nodeProbText->SetLabel(_("Pr(Node reached): ") + wxString::FromUTF8(profiles.GetRealizProb(p_node))); - m_infosetProbText->SetLabel(_("Pr(Infoset reached): ") + - wxString::FromUTF8(profiles.GetInfosetProb(p_node))); + m_infosetProbText->SetLabel( + (mover->IsChance() ? _("Pr(Event reached): ") : _("Pr(Information set reached): ")) + + wxString::FromUTF8(profiles.GetInfosetProb(p_node))); m_beliefText->SetLabel(_("Belief: ") + wxString::FromUTF8(profiles.GetBeliefProb(p_node))); - m_infosetValueText->SetLabel(_("Payoff | Infoset: ") + - wxString::FromUTF8(profiles.GetInfosetValue(p_node))); + m_infosetValueText->SetLabel( + (mover->IsChance() ? _("Payoff | Event: ") : _("Payoff | Information set: ")) + + wxString::FromUTF8(profiles.GetInfosetValue(p_node))); } m_contentPanel->GetSizer()->Show(m_moveSizer, showMove, true); diff --git a/src/pygambit/behavmixed.pxi b/src/pygambit/behavmixed.pxi index 6379facabb..8d979cd82b 100644 --- a/src/pygambit/behavmixed.pxi +++ b/src/pygambit/behavmixed.pxi @@ -25,8 +25,8 @@ from cython.operator cimport dereference as deref @cython.cclass class InfosetIndexedVector(_LabeledVector): - """A read-only mapping from an information set to a computed value, one entry per - information set. + """A read-only mapping from an information set or event to a computed value, one + entry per information set or event. Since information sets don't reliably have unique persistent labels, this is indexed by a `Selector` (an `H`-built expression) resolving to a single node belonging to the @@ -34,10 +34,13 @@ class InfosetIndexedVector(_LabeledVector): before lookup, so any member node is an equally valid key -- rather than by a label, unlike `NodeIndexedVector`. + Each subclass holds entries of one kind only, and overrides ``_label_kind`` so that + a failed lookup reports the kind it actually contains. + .. versionchanged:: 17.0.0 Indexed by a `Selector` rather than a `Node` object. """ - _label_kind = "information set" + _label_kind = "information set or event" _game = cython.declare(Game) def __init__(self, game: Game, values: collections.abc.Mapping) -> None: @@ -69,22 +72,33 @@ class InfosetIndexedVector(_LabeledVector): @cython.cclass class InfosetValueVector(InfosetIndexedVector): """The expected payoff to the player conditional on reaching each information set, - one entry per (non-chance) information set. + one entry per information set. Chance events are excluded. """ + _label_kind = "information set" @cython.cclass class InfosetRegretVector(InfosetIndexedVector): """The regret of playing the mixed action at each information set, one entry per - information set. + information set. Chance events are excluded. """ + _label_kind = "information set" @cython.cclass class InfosetProbVector(InfosetIndexedVector): """The probability with which each information set is reached, one entry per - information set. + information set. Chance events are excluded; see `EventProbVector`. + """ + _label_kind = "information set" + + +@cython.cclass +class EventProbVector(InfosetIndexedVector): + """The probability with which each chance event is reached, one entry per event. + Personal players' information sets are excluded; see `InfosetProbVector`. """ + _label_kind = "event" @cython.cclass @@ -103,15 +117,17 @@ class ActionRegretVector(StrategyIndexedVector): class ActionValuesVector(InfosetIndexedVector): """The expected payoff of playing each action, conditional on reaching it, grouped by information set; each value is an `ActionValueVector` for that information set's - actions. + actions. Chance events are excluded. """ + _label_kind = "information set" @cython.cclass class ActionRegretsVector(InfosetIndexedVector): """The regret of playing each action, grouped by information set; each value is an - `ActionValueVector` for that information set's actions. + `ActionValueVector` for that information set's actions. Chance events are excluded. """ + _label_kind = "information set" @cython.cclass @@ -135,8 +151,8 @@ class NodeValueVector(NodeIndexedVector): @cython.cclass class NodeValuesVector(PlayerIndexedVector): - """The expected payoff to each (non-chance) player conditional on reaching each - node, grouped by player; each value is a `NodeValueVector` for that player. + """The expected payoff to each personal player conditional on reaching each node, + grouped by player; each value is a `NodeValueVector` for that player. """ @@ -461,13 +477,6 @@ class MixedBehaviorProfile: f"profile index must be str or Selector, not {index.__class__.__name__}" ) - def _all_infosets(self) -> typing.Iterator[Node]: - """Iterates over a representative node of every information set and event in - the game.""" - for player in self.game.players: - yield from self.game._get_infosets(player) - yield from self.game._get_events() - def _personal_infosets(self) -> typing.Iterator[Node]: """Iterates over a representative node of every information set in the game belonging to a personal player, excluding the chance player's. @@ -475,6 +484,12 @@ class MixedBehaviorProfile: for player in self.game.players: yield from self.game._get_infosets(player) + def _events(self) -> typing.Iterator[Node]: + """Iterates over a representative node of every event of the chance player in + the game. + """ + yield from self.game._get_events() + # The public API above is implemented once here and dispatches to the hooks below, # each of which is implemented by a concrete dtype-specific subclass # (MixedBehaviorProfileDouble/MixedBehaviorProfileRational). @@ -823,14 +838,37 @@ class MixedBehaviorProfile: self._check_validity() return InfosetProbVector(self.game, { _canonical_history(node): self._infoset_prob(node) - for node in self._all_infosets() + for node in self._personal_infosets() + }) + + @property + def event_probs(self) -> EventProbVector: + """Returns the probability with which each chance event is reached, if all + players play according to the profile. + + This is the counterpart of `infoset_probs` for the chance player: the + probability that the event is reached *at least once* under the profile, i.e. + the realization probability of its upper frontier. For an event with a single + member node, which is the usual case, this is that node's realization + probability. + + .. versionadded:: 17.0.0 + + See Also + -------- + MixedBehaviorProfile.infoset_probs + MixedBehaviorProfile.beliefs + """ + self._check_validity() + return EventProbVector(self.game, { + _canonical_history(node): self._infoset_prob(node) for node in self._events() }) @property def beliefs(self) -> BeliefVector: """Returns, for each node, the conditional probability that the node is - reached, given that its information set is reached, if all players play - according to the profile. + reached, given that its information set (or, at a chance node, its event) is + reached, if all players play according to the profile. The conditioning event is that the information set is reached at least once, so beliefs are normalized by the upper-frontier probability returned by @@ -841,12 +879,14 @@ class MixedBehaviorProfile: If a node's information set is reached with zero probability under the profile, the belief is not well-defined and the corresponding entry is `None`. - This is the same reach probability returned by `infoset_probs`, so a `None` - belief corresponds exactly to `infoset_probs` being zero there. + This is the same reach probability returned by `infoset_probs` (or by + `event_probs`, for a chance node), so a `None` belief corresponds exactly to + that reach probability being zero. See Also -------- MixedBehaviorProfile.infoset_probs + MixedBehaviorProfile.event_probs .. versionchanged:: 17.0.0 Keyed by each node's History rather than a ``Node`` object. @@ -867,7 +907,7 @@ class MixedBehaviorProfile: always non-negative. Regret is not defined for the chance player, which takes no decisions; its - information sets are excluded. + events are excluded. See Also -------- @@ -894,7 +934,7 @@ class MixedBehaviorProfile: By convention, the regret is always non-negative. Regret is not defined for the chance player, which takes no decisions; its - information sets are excluded. + events are excluded. See Also -------- diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index bea1c63bde..41664f4d69 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -2578,7 +2578,7 @@ class Game: current = list(resolved_event.actions) _reconcile_labels( current, labels, add, drop, "set_event_actions", - "information set", "action", "actions", "the subtrees they lead to" + "event", "action", "actions", "the subtrees they lead to" ) c_labels = stdvector[string]() c_probs = stdvector[c_Number]() diff --git a/tests/test_behav.py b/tests/test_behav.py index a93311bda1..7e7b9fdb81 100644 --- a/tests/test_behav.py +++ b/tests/test_behav.py @@ -747,12 +747,31 @@ def test_absent_minded_infoset_prob( @pytest.mark.parametrize("rational_flag", [False, True]) def test_nature_rooted_game_root_reached_with_certainty(rational_flag: bool): - """The chance root infoset is reached with probability one.""" + """The chance event at the root is reached with probability one.""" game = gbt.catalog.load("journals/geb/gilboa1997/fig2") profile = game.mixed_behavior_profile(rational=rational_flag) one = gbt.Rational(1) if rational_flag else 1.0 assert profile.realiz_probs[()] == one - assert profile.infoset_probs[gbt.H.path()] == one + assert profile.event_probs[gbt.H.path()] == one + + +@pytest.mark.parametrize("rational_flag", [False, True]) +def test_infoset_probs_and_event_probs_partition_by_kind(rational_flag: bool): + """`infoset_probs` covers exactly the personal players' information sets and + `event_probs` exactly the chance player's events; each rejects the other kind.""" + game = games.read_from_file("stripped_down_poker.efg") + profile = game.mixed_behavior_profile(rational=rational_flag) + infoset_probs = profile.infoset_probs + event_probs = profile.event_probs + assert isinstance(event_probs, gbt.EventProbVector) + assert len(list(infoset_probs)) == len(games.all_infosets(game)) + assert len(list(event_probs)) == len(game.get_events()) + (event_node,) = game.get_events() + personal_node = games.all_infosets(game)[0] + with pytest.raises(KeyError, match="no information set for"): + infoset_probs[gbt.H.path(*event_node.actions)] + with pytest.raises(KeyError, match="no event for"): + event_probs[gbt.H.path(*personal_node.actions)] @pytest.mark.parametrize( @@ -892,9 +911,9 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): ) def test_vectorized_quantities_consistency(game: gbt.Game, rational_flag: bool): """The vectorized payoffs/node_values/infoset_values/infoset_regrets/action_values/ - action_regrets/realiz_probs/infoset_probs/beliefs properties are mathematically - consistent with each other (regret is the gap to the best response), and carry their - own type identity. + action_regrets/realiz_probs/infoset_probs/event_probs/beliefs properties are + mathematically consistent with each other (regret is the gap to the best response), + and carry their own type identity. """ profile = game.mixed_behavior_profile(rational=rational_flag) @@ -906,6 +925,7 @@ def test_vectorized_quantities_consistency(game: gbt.Game, rational_flag: bool): action_regrets = profile.action_regrets realiz_probs = profile.realiz_probs infoset_probs = profile.infoset_probs + event_probs = profile.event_probs beliefs = profile.beliefs assert isinstance(payoffs, gbt.PayoffVector) @@ -917,6 +937,7 @@ def test_vectorized_quantities_consistency(game: gbt.Game, rational_flag: bool): assert isinstance(action_regrets, gbt.ActionRegretsVector) assert isinstance(realiz_probs, gbt.RealizProbVector) assert isinstance(infoset_probs, gbt.InfosetProbVector) + assert isinstance(event_probs, gbt.EventProbVector) assert isinstance(beliefs, gbt.BeliefVector) for player in game.players: @@ -943,7 +964,12 @@ def test_vectorized_quantities_consistency(game: gbt.Game, rational_flag: bool): for history in game.get_histories(gbt.H.after()): if not game.get_actions(gbt.H.path(*history.actions)): continue - if infoset_probs[gbt.H.path(*history.actions)] == 0: + selector = gbt.H.path(*history.actions) + node = games.node_at_history(game, history.actions) + reach_prob = ( + infoset_probs[selector] if node.player in game.players else event_probs[selector] + ) + if reach_prob == 0: assert beliefs[history] is None else: assert beliefs[history] is not None diff --git a/tests/test_profile_invalidation.py b/tests/test_profile_invalidation.py index dfad65bfc3..bb8be96a45 100644 --- a/tests/test_profile_invalidation.py +++ b/tests/test_profile_invalidation.py @@ -94,6 +94,8 @@ def test_mixed_behavior_profile_game_structure_changed(): _ = profile.beliefs with pytest.raises(gbt.GameStructureChangedError): profile.copy() + with pytest.raises(gbt.GameStructureChangedError): + _ = profile.event_probs with pytest.raises(gbt.GameStructureChangedError): _ = profile.infoset_probs with pytest.raises(gbt.GameStructureChangedError):