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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

### Changed
- `Game.comment` has been renamed to `Game.description`
- With behaviour profiles that reach some information sets with probability zero, beliefs, action
values, and infoset values are not well-defined. These functions now return a
`std::optional` in C++ and type or `None` in Python, where nulls indicate these quantities
are not defined. (#446)

### Added
- Implement linear-time algorithm to find all root nodes of proper subgames, using an adaptation of
Expand All @@ -20,10 +24,6 @@
root node is a member of an absent-minded infoset. (#584)
- Removed spurious warning in graphical interface when loading file as a command-line argument
(or also by clicking on a file in MSW, as that uses the command-line mechanism). (#801)

## [16.5.1] - unreleased

### Fixed
- `Game.reveal` raised a null pointer access exception or dumped core in some cases (#749)

## [16.5.0] - 2026-01-05
Expand Down
32 changes: 26 additions & 6 deletions src/games/behavmixed.cc
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,11 @@ template <class T> T MixedBehaviorProfile<T>::GetAgentLiapValue() const
{
CheckVersion();
EnsureRegrets();
auto value = static_cast<T>(0);
T value{0};
for (auto infoset : m_support.GetGame()->GetInfosets()) {
if (GetInfosetProb(infoset) == T{0}) {
continue;
}
for (auto action : m_support.GetActions(infoset)) {
value += sqr(std::max(m_cache.m_actionValues[action] - m_cache.m_infosetValues[infoset],
static_cast<T>(0)));
Expand All @@ -285,10 +288,14 @@ template <class T> T MixedBehaviorProfile<T>::GetInfosetProb(const GameInfoset &
[&](const auto &node) -> T { return m_cache.m_realizProbs[node]; });
}

template <class T> const T &MixedBehaviorProfile<T>::GetBeliefProb(const GameNode &node) const
template <class T>
std::optional<T> MixedBehaviorProfile<T>::GetBeliefProb(const GameNode &node) const
{
CheckVersion();
EnsureBeliefs();
if (!node->GetInfoset() || GetInfosetProb(node->GetInfoset()) == T{0}) {
return std::nullopt;
}
return m_cache.m_beliefs[node];
}

Expand All @@ -309,13 +316,17 @@ const T &MixedBehaviorProfile<T>::GetPayoff(const GamePlayer &p_player,
{
CheckVersion();
EnsureNodeValues();
return m_cache.m_nodeValues[p_node][p_player];
return m_cache.m_nodeValues.at(p_node).at(p_player);
}

template <class T> const T &MixedBehaviorProfile<T>::GetPayoff(const GameInfoset &p_infoset) const
template <class T>
std::optional<T> MixedBehaviorProfile<T>::GetPayoff(const GameInfoset &p_infoset) const
{
CheckVersion();
EnsureRegrets();
if (GetInfosetProb(p_infoset) == T{0}) {
return std::nullopt;
}
return m_cache.m_infosetValues[p_infoset];
}

Expand All @@ -331,24 +342,33 @@ template <class T> T MixedBehaviorProfile<T>::GetActionProb(const GameAction &ac
return m_probs[m_profileIndex.at(action)];
}

template <class T> const T &MixedBehaviorProfile<T>::GetPayoff(const GameAction &act) const
template <class T> std::optional<T> MixedBehaviorProfile<T>::GetPayoff(const GameAction &act) const
{
CheckVersion();
EnsureActionValues();
if (GetInfosetProb(act->GetInfoset()) == T{0}) {
return std::nullopt;
}
return m_cache.m_actionValues[act];
}

template <class T> const T &MixedBehaviorProfile<T>::GetRegret(const GameAction &act) const
template <class T> T MixedBehaviorProfile<T>::GetRegret(const GameAction &act) const
{
CheckVersion();
EnsureRegrets();
if (GetInfosetProb(act->GetInfoset()) == T{0}) {
return T{0};
}
return m_cache.m_regret.at(act);
}

template <class T> T MixedBehaviorProfile<T>::GetRegret(const GameInfoset &p_infoset) const
{
CheckVersion();
EnsureRegrets();
if (GetInfosetProb(p_infoset) == T{0}) {
return T{0};
}
T br_payoff = maximize_function(p_infoset->GetActions(), [this](const auto &action) -> T {
return m_cache.m_actionValues.at(action);
});
Expand Down
10 changes: 5 additions & 5 deletions src/games/behavmixed.h
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,11 @@ template <class T> class MixedBehaviorProfile {

const T &GetRealizProb(const GameNode &node) const;
T GetInfosetProb(const GameInfoset &p_infoset) const;
const T &GetBeliefProb(const GameNode &node) const;
std::optional<T> GetBeliefProb(const GameNode &node) const;
Vector<T> GetPayoff(const GameNode &node) const;
const T &GetPayoff(const GamePlayer &player, const GameNode &node) const;
const T &GetPayoff(const GameInfoset &p_infoset) const;
const T &GetPayoff(const GameAction &act) const;
const T &GetPayoff(const GamePlayer &p_player, const GameNode &p_node) const;
std::optional<T> GetPayoff(const GameInfoset &p_infoset) const;
std::optional<T> GetPayoff(const GameAction &act) const;
T GetActionProb(const GameAction &act) const;

/// @brief Computes the regret to playing \p p_action
Expand All @@ -256,7 +256,7 @@ template <class T> class MixedBehaviorProfile {
/// @param[in] p_action The action to compute the regret for.
/// @sa GetRegret(const GameInfoset &) const
/// GetAgentMaxRegret() const
const T &GetRegret(const GameAction &p_action) const;
T GetRegret(const GameAction &p_action) const;

/// @brief Computes the regret at information set \p p_infoset
/// @details Computes the regret at the information set to the player of playing
Expand Down
11 changes: 0 additions & 11 deletions src/games/behavpure.cc
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,6 @@ T PureBehaviorProfile::GetPayoff(const GameNode &p_node, const GamePlayer &p_pla
template double PureBehaviorProfile::GetPayoff(const GameNode &, const GamePlayer &) const;
template Rational PureBehaviorProfile::GetPayoff(const GameNode &, const GamePlayer &) const;

template <class T> T PureBehaviorProfile::GetPayoff(const GameAction &p_action) const
{
PureBehaviorProfile copy(*this);
copy.SetAction(p_action);
return copy.GetPayoff<T>(p_action->GetInfoset()->GetPlayer());
}

// Explicit instantiations
template double PureBehaviorProfile::GetPayoff(const GameAction &) const;
template Rational PureBehaviorProfile::GetPayoff(const GameAction &) const;

MixedBehaviorProfile<Rational> PureBehaviorProfile::ToMixedBehaviorProfile() const
{
MixedBehaviorProfile<Rational> temp(m_efg);
Expand Down
2 changes: 0 additions & 2 deletions src/games/behavpure.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ class PureBehaviorProfile {
template <class T> T GetPayoff(const GamePlayer &p_player) const;
/// Get the payoff to the player, conditional on reaching a node
template <class T> T GetPayoff(const GameNode &, const GamePlayer &) const;
/// Get the payoff to playing the action, conditional on the profile
template <class T> T GetPayoff(const GameAction &) const;

/// Convert to a mixed behavior representation
MixedBehaviorProfile<Rational> ToMixedBehaviorProfile() const;
Expand Down
20 changes: 10 additions & 10 deletions src/gui/analysis.cc
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,9 @@ std::string AnalysisProfileList<T>::GetBeliefProb(const GameNode &p_node, int p_
}

try {
if (m_behavProfiles[index]->GetInfosetProb(p_node->GetInfoset()) > Rational(0)) {
return lexical_cast<std::string>(m_behavProfiles[index]->GetBeliefProb(p_node),
m_doc->GetStyle().NumDecimals());
auto belief = m_behavProfiles[index]->GetBeliefProb(p_node);
if (belief.has_value()) {
return lexical_cast<std::string>(belief.value(), m_doc->GetStyle().NumDecimals());
}
// We don't compute assessments yet!
return "*";
Expand Down Expand Up @@ -295,9 +295,9 @@ std::string AnalysisProfileList<T>::GetInfosetValue(const GameNode &p_node, int
}

try {
if (m_behavProfiles[index]->GetInfosetProb(p_node->GetInfoset()) > Rational(0)) {
return lexical_cast<std::string>(m_behavProfiles[index]->GetPayoff(p_node->GetInfoset()),
m_doc->GetStyle().NumDecimals());
auto payoff = m_behavProfiles[index]->GetPayoff(p_node->GetInfoset());
if (payoff.has_value()) {
return lexical_cast<std::string>(payoff.value(), m_doc->GetStyle().NumDecimals());
}
// In the absence of beliefs, this is not well-defined in general
return "*";
Expand Down Expand Up @@ -367,10 +367,10 @@ std::string AnalysisProfileList<T>::GetActionValue(const GameNode &p_node, int p
}

try {
if (m_behavProfiles[index]->GetInfosetProb(p_node->GetInfoset()) > Rational(0)) {
return lexical_cast<std::string>(
m_behavProfiles[index]->GetPayoff(p_node->GetInfoset()->GetAction(p_act)),
m_doc->GetStyle().NumDecimals());
std::optional<T> actionValue =
m_behavProfiles[index]->GetPayoff(p_node->GetInfoset()->GetAction(p_act));
if (actionValue.has_value()) {
return lexical_cast<std::string>(actionValue.value(), m_doc->GetStyle().NumDecimals());
}
// In the absence of beliefs, this is not well-defined
return "*";
Expand Down
65 changes: 52 additions & 13 deletions src/pygambit/behavmixed.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -587,10 +587,13 @@ class MixedBehaviorProfile:
self._check_validity()
return self._is_defined_at(self.game._resolve_infoset(infoset, "is_defined_at"))

def belief(self, node: NodeReference) -> ProfileDType:
def belief(self, node: NodeReference) -> ProfileDType | None:
"""Returns the conditional probability that a node is reached, given that
its information set is reached.

If the information set is not reachable, the belief is not well-defined.
In this case, the function returns `None`.

Parameters
----------
node
Expand All @@ -600,6 +603,10 @@ class MixedBehaviorProfile:
------
MismatchError
If `node` is not in the same game as the profile

See Also
--------
MixedBehaviorProfile.infoset_prob
"""
self._check_validity()
return self._belief(self.game._resolve_node(node, "belief"))
Expand Down Expand Up @@ -661,10 +668,13 @@ class MixedBehaviorProfile:
raise ValueError("node_value() is not defined for the chance player")
return self._node_value(resolved_player, resolved_node)

def infoset_value(self, infoset: InfosetReference) -> ProfileDType:
def infoset_value(self, infoset: InfosetReference) -> ProfileDType | None:
"""Returns the expected payoff to the player conditional on reaching an information set,
if all players play according to the profile.

If the information set is not reachable, the expected payoff is not well-defined.
In this case, the function returns `None`.

Parameters
----------
infoset : Infoset or str
Expand All @@ -679,17 +689,24 @@ class MixedBehaviorProfile:
If `infoset` is a string and no information set in the game has that label.
ValueError
If `infoset` resolves to an infoset that belongs to the chance player

See Also
--------
MixedBehaviorProfile.infoset_prob
"""
self._check_validity()
resolved_infoset = self.game._resolve_infoset(infoset, "infoset_value")
if resolved_infoset.player.is_chance:
raise ValueError("infoset_value() is not defined for the chance player")
return self._infoset_value(resolved_infoset)

def action_value(self, action: ActionReference) -> ProfileDType:
def action_value(self, action: ActionReference) -> ProfileDType | None:
"""Returns the expected payoff to the player of playing an action conditional on reaching
its information set, if all players play according to the profile.

If the information set is not reachable, the expected payoff is not well-defined.
In this case, the function returns `None`.

Parameters
----------
action : Action or str
Expand All @@ -704,6 +721,10 @@ class MixedBehaviorProfile:
If `action` is a string and no action in the game has that label.
ValueError
If `action` resolves to an action that belongs to the chance player

See Also
--------
MixedBehaviorProfile.infoset_prob
"""
self._check_validity()
resolved_action = self.game._resolve_action(action, "action_value")
Expand Down Expand Up @@ -945,22 +966,31 @@ class MixedBehaviorProfileDouble(MixedBehaviorProfile):
return deref(self.profile).GetPayoff(player.player)

def _belief(self, node: Node) -> float:
return deref(self.profile).GetBeliefProb(node.node)
cdef optional[double] value = deref(self.profile).GetBeliefProb(node.node)
if value.has_value():
return value.value()
return None

def _realiz_prob(self, node: Node) -> float:
return deref(self.profile).GetRealizProb(node.node)

def _infoset_prob(self, infoset: Infoset) -> float:
return deref(self.profile).GetInfosetProb(infoset.infoset)

def _infoset_value(self, infoset: Infoset) -> float:
return deref(self.profile).GetPayoff(infoset.infoset)
def _infoset_value(self, infoset: Infoset) -> float | None:
cdef optional[double] value = deref(self.profile).GetPayoff(infoset.infoset)
if value.has_value():
return value.value()
return None

def _node_value(self, player: Player, node: Node) -> float:
return deref(self.profile).GetPayoff(player.player, node.node)

def _action_value(self, action: Action) -> float:
return deref(self.profile).GetPayoff(action.action)
def _action_value(self, action: Action) -> float | None:
cdef optional[double] value = deref(self.profile).GetPayoff(action.action)
if value.has_value():
return value.value()
return None

def _action_regret(self, action: Action) -> float:
return deref(self.profile).GetRegret(action.action)
Expand Down Expand Up @@ -1047,22 +1077,31 @@ class MixedBehaviorProfileRational(MixedBehaviorProfile):
return rat_to_py(deref(self.profile).GetPayoff(player.player))

def _belief(self, node: Node) -> Rational:
return rat_to_py(deref(self.profile).GetBeliefProb(node.node))
cdef optional[c_Rational] value = deref(self.profile).GetBeliefProb(node.node)
if value.has_value():
return rat_to_py(value.value())
return None

def _realiz_prob(self, node: Node) -> Rational:
return rat_to_py(deref(self.profile).GetRealizProb(node.node))

def _infoset_prob(self, infoset: Infoset) -> Rational:
return rat_to_py(deref(self.profile).GetInfosetProb(infoset.infoset))

def _infoset_value(self, infoset: Infoset) -> Rational:
return rat_to_py(deref(self.profile).GetPayoff(infoset.infoset))
def _infoset_value(self, infoset: Infoset) -> Rational | None:
cdef optional[c_Rational] value = deref(self.profile).GetPayoff(infoset.infoset)
if value.has_value():
return rat_to_py(value.value())
return None

def _node_value(self, player: Player, node: Node) -> Rational:
return rat_to_py(deref(self.profile).GetPayoff(player.player, node.node))

def _action_value(self, action: Action) -> Rational:
return rat_to_py(deref(self.profile).GetPayoff(action.action))
def _action_value(self, action: Action) -> Rational | None:
cdef optional[c_Rational] value = deref(self.profile).GetPayoff(action.action)
if value.has_value():
return rat_to_py(value.value())
return None

def _action_regret(self, action: Action) -> Rational:
return rat_to_py(deref(self.profile).GetRegret(action.action))
Expand Down
7 changes: 4 additions & 3 deletions src/pygambit/gambit.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ from libcpp.memory cimport shared_ptr, unique_ptr
from libcpp.list cimport list as stdlist
from libcpp.vector cimport vector as stdvector
from libcpp.set cimport set as stdset
from libcpp.optional cimport optional


cdef extern from "gambit.h":
Expand Down Expand Up @@ -362,12 +363,12 @@ cdef extern from "games/behavmixed.h" namespace "Gambit":
T getitem "operator[]"(int) except +IndexError
T getaction "operator[]"(c_GameAction) except +IndexError
T GetPayoff(c_GamePlayer) except +
T GetBeliefProb(c_GameNode) except +
optional[T] GetBeliefProb(c_GameNode) except +
T GetRealizProb(c_GameNode) except +
T GetInfosetProb(c_GameInfoset) except +
T GetPayoff(c_GameInfoset) except +
optional[T] GetPayoff(c_GameInfoset) except +
T GetPayoff(c_GamePlayer, c_GameNode) except +
T GetPayoff(c_GameAction) except +
optional[T] GetPayoff(c_GameAction) except +
T GetRegret(c_GameAction) except +
T GetRegret(c_GameInfoset) except +
T GetAgentMaxRegret() except +
Expand Down
4 changes: 2 additions & 2 deletions src/pygambit/game.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,7 @@ class Game:
Changed from reporting minimum payoff in any (non-null) outcome to the minimum
payoff in any play of the game.

See also
See Also
--------
Game.max_payoff
Player.min_payoff
Expand All @@ -799,7 +799,7 @@ class Game:
Changed from reporting maximum payoff in any (non-null) outcome to the maximum
payoff in any play of the game.

See also
See Also
--------
Game.min_payoff
Player.max_payoff
Expand Down
Loading
Loading