Skip to content
Open
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
6 changes: 6 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [16.7.0] - unreleased

### Changed
- Clarified definition of information set realisation probability to avoid double-counting of
multiple realisations due to absent-mindedness. (#826)

## [16.6.0] - 2026-03-24

### Changed
Expand Down
12 changes: 10 additions & 2 deletions src/games/behavmixed.cc
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,7 @@ template <class T> T MixedBehaviorProfile<T>::GetInfosetProb(const GameInfoset &
{
CheckVersion();
EnsureRealizations();
return sum_function(p_infoset->GetMembers(),
[&](const auto &node) -> T { return m_cache.m_realizProbs[node]; });
return m_cache.m_infosetProbs[p_infoset];
}

template <class T>
Expand Down Expand Up @@ -471,6 +470,7 @@ T MixedBehaviorProfile<T>::DiffNodeValue(const GameNode &p_node, const GamePlaye
template <class T> void MixedBehaviorProfile<T>::ComputeRealizationProbs() const
{
m_cache.m_realizProbs.clear();
m_cache.m_infosetProbs.clear();

const auto &game = m_support.GetGame();
m_cache.m_realizProbs[game->GetRoot()] = static_cast<T>(1);
Expand All @@ -480,6 +480,14 @@ template <class T> void MixedBehaviorProfile<T>::ComputeRealizationProbs() const
m_cache.m_realizProbs[child] = incomingProb * GetActionProb(action);
}
}

for (const auto &infoset : game->GetInfosets()) {
m_cache.m_infosetProbs[infoset] = sum_function(
infoset->GetMembers(), [&](const auto &node) -> T { return m_cache.m_realizProbs[node]; });
}
for (const auto &[infoset, node] : game->GetAbsentMindedReentries()) {
m_cache.m_infosetProbs[infoset] -= m_cache.m_realizProbs[node];
}
}

template <class T> void MixedBehaviorProfile<T>::ComputeBeliefs() const
Expand Down
11 changes: 11 additions & 0 deletions src/games/behavmixed.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ template <class T> class MixedBehaviorProfile {

Level m_level{Level::None};
std::map<GameNode, T> m_realizProbs, m_beliefs;
std::map<GameInfoset, T> m_infosetProbs;
std::map<GameNode, std::map<GamePlayer, T>> m_nodeValues;
std::map<GameInfoset, T> m_infosetValues;
std::map<GameAction, T> m_actionValues;
Expand All @@ -60,6 +61,7 @@ template <class T> class MixedBehaviorProfile {
{
m_level = Level::None;
m_realizProbs.clear();
m_infosetProbs.clear();
m_beliefs.clear();
m_nodeValues.clear();
m_infosetValues.clear();
Expand Down Expand Up @@ -240,6 +242,15 @@ template <class T> class MixedBehaviorProfile {
T GetAgentLiapValue() const;

const T &GetRealizProb(const GameNode &node) const;
/// @brief Computes the probability the information set \p p_infoset is reached.
/// @details Computes the probability that \p p_infoset is reached assuming
/// all players play according to the profile. If \p p_infoset is an
/// absent-minded infoset, this probability is the probability any
/// member node is reached; multiple visits do not contribute further
/// to the probability.
/// @param[in] p_infoset The information set to compute the realization probability.
/// @sa GetRealizProb(const GameNode &) const
/// GetBeliefProb(const GameNode &) const
T GetInfosetProb(const GameInfoset &p_infoset) const;
std::optional<T> GetBeliefProb(const GameNode &node) const;
Vector<T> GetPayoff(const GameNode &node) const;
Expand Down
5 changes: 5 additions & 0 deletions src/games/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,11 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
}
return false;
}
/// Returns (infoset, node) pairs where the node is a reentry of an absent-minded infoset
virtual std::vector<std::pair<GameInfoset, GameNode>> GetAbsentMindedReentries() const
{
return {};
}
/// Returns a list of all subgame roots in the game
virtual std::vector<GameNode> GetSubgames() const { throw UndefinedException(); }

Expand Down
20 changes: 20 additions & 0 deletions src/games/gametree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,7 @@ Rational GameTreeRep::GetPlayerMaxPayoff(const GamePlayer &p_player) const
return maximize_function(range, value_fn);
});
}

bool GameTreeRep::IsPerfectRecall() const
{
if (!m_ownPriorActionInfo && !m_root->IsTerminal()) {
Expand Down Expand Up @@ -859,6 +860,23 @@ bool GameTreeRep::IsAbsentMinded(const GameInfoset &p_infoset) const
return contains(m_absentMindedInfosets, p_infoset.get());
}

std::vector<std::pair<GameInfoset, GameNode>> GameTreeRep::GetAbsentMindedReentries() const
{
if (!m_unreachableNodes && !m_root->IsTerminal()) {
BuildUnreachableNodes();
}
if (m_absentMindedReentries.empty()) {
return {};
}

std::vector<std::pair<GameInfoset, GameNode>> result;
result.reserve(m_absentMindedReentries.size());
for (const auto &[infoset, node] : m_absentMindedReentries) {
result.emplace_back(infoset->shared_from_this(), node->shared_from_this());
}
return result;
}

//------------------------------------------------------------------------
// GameTreeRep: Managing the representation
//------------------------------------------------------------------------
Expand Down Expand Up @@ -924,6 +942,7 @@ void GameTreeRep::ClearComputedValues() const
m_ownPriorActionInfo = nullptr;
const_cast<GameTreeRep *>(this)->m_unreachableNodes = nullptr;
m_absentMindedInfosets.clear();
m_absentMindedReentries.clear();
m_subgames.clear();
m_computedValues = false;
}
Expand Down Expand Up @@ -1100,6 +1119,7 @@ void GameTreeRep::BuildUnreachableNodes() const
// Check for Absent-Minded Re-entry of the infoset
if (path_choices.find(child->m_infoset->shared_from_this()) != path_choices.end()) {
m_absentMindedInfosets.insert(child->m_infoset);
m_absentMindedReentries.emplace_back(child->m_infoset, child.get());
const GameAction replay_action = path_choices.at(child->m_infoset->shared_from_this());
position.emplace(AbsentMindedEdge{replay_action, child});

Expand Down
2 changes: 2 additions & 0 deletions src/games/gametree.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class GameTreeRep final : public GameExplicitRep {
mutable std::shared_ptr<OwnPriorActionInfo> m_ownPriorActionInfo;
mutable std::unique_ptr<std::set<GameNodeRep *>> m_unreachableNodes;
mutable std::set<GameInfosetRep *> m_absentMindedInfosets;
mutable std::vector<std::pair<GameInfosetRep *, GameNodeRep *>> m_absentMindedReentries;
mutable std::vector<GameNodeRep *> m_subgames;

/// @name Private auxiliary functions
Expand Down Expand Up @@ -99,6 +100,7 @@ class GameTreeRep final : public GameExplicitRep {
/// Returns the largest payoff to the player in any play of the game
Rational GetPlayerMaxPayoff(const GamePlayer &) const override;
bool IsAbsentMinded(const GameInfoset &p_infoset) const override;
std::vector<std::pair<GameInfoset, GameNode>> GetAbsentMindedReentries() const override;
std::vector<GameNode> GetSubgames() const override;
//@}

Expand Down
6 changes: 5 additions & 1 deletion src/pygambit/behavmixed.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -754,10 +754,14 @@ class MixedBehaviorProfile:
def infoset_prob(self, infoset: NodeReference) -> ProfileDType:
"""Returns the probability with which an information set is reached.

For absent-minded information sets, this returns the probability that any
member is reached; a second or subsequent visit to the information set does
not contribute further to the realization probability.

Parameters
----------
infoset : Infoset or str
The information set to get the payoff for. If a string is passed, the
The information set to get the probability of. If a string is passed, the
information set is determined by finding the information set with that label, if any.

Raises
Expand Down
31 changes: 31 additions & 0 deletions tests/test_behav.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,37 @@ def test_infoset_prob_by_label_reference(
assert profile.infoset_prob(label) == (gbt.Rational(prob) if rational_flag else prob)


@pytest.mark.parametrize(
"game,player_idx,infoset_idx,prob,rational_flag",
[
# P1 infoset 1 is absent-minded (root + one reentry)
(games.read_from_file("noPR-AM-driver-one-player.efg"), 0, 0, 1.0, False),
(games.read_from_file("noPR-AM-driver-one-player.efg"), 0, 1, 0.5, False),
(games.read_from_file("noPR-AM-driver-one-player.efg"), 0, 2, 0.125, False),
(games.read_from_file("noPR-AM-driver-one-player.efg"), 0, 0, "1", True),
(games.read_from_file("noPR-AM-driver-one-player.efg"), 0, 1, "1/2", True),
(games.read_from_file("noPR-AM-driver-one-player.efg"), 0, 2, "1/8", True),
# P1 infoset 1 has 3 members (root + both children are reentries)
(games.read_from_file("noPR-action-AM.efg"), 0, 0, 1.0, False),
(games.read_from_file("noPR-action-AM.efg"), 1, 0, 0.25, False),
(games.read_from_file("noPR-action-AM.efg"), 1, 1, 0.25, False),
(games.read_from_file("noPR-action-AM.efg"), 1, 2, 0.25, False),
(games.read_from_file("noPR-action-AM.efg"), 1, 3, 0.25, False),
(games.read_from_file("noPR-action-AM.efg"), 0, 0, "1", True),
(games.read_from_file("noPR-action-AM.efg"), 1, 0, "1/4", True),
(games.read_from_file("noPR-action-AM.efg"), 1, 1, "1/4", True),
(games.read_from_file("noPR-action-AM.efg"), 1, 2, "1/4", True),
(games.read_from_file("noPR-action-AM.efg"), 1, 3, "1/4", True),
],
)
def test_absent_minded_infoset_prob(
game: gbt.Game, player_idx: int, infoset_idx: int, prob: str | float, rational_flag: bool
):
profile = game.mixed_behavior_profile(rational=rational_flag)
ip = profile.infoset_prob(game.players[player_idx].infosets[infoset_idx])
assert ip == (gbt.Rational(prob) if rational_flag else prob)


@pytest.mark.parametrize(
"game,player_idx,infoset_idx,payoff,rational_flag",
[
Expand Down
Loading