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
11 changes: 11 additions & 0 deletions doc/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ @article{GovWil04
category = {articles_equilibria}
}

@article{HalPas21,
author = {Halpern, J. Y. and Pass, R.},
title = {Sequential equilibrium in games of imperfect recall},
journal = {ACM Transactions on Economics and Computation},
volume = {9},
number = {4},
pages = {1--26},
year = {2021},
category = {articles_general}
}

@article{Jiang11,
author = {Jiang, A. X. and Leyton-Brown, K. and Bhat, N.},
title = {Action-graph games},
Expand Down
24 changes: 19 additions & 5 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 @@ -475,6 +474,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 @@ -484,15 +484,29 @@ template <class T> void MixedBehaviorProfile<T>::ComputeRealizationProbs() const
m_cache.m_realizProbs[child] = incomingProb * GetActionProb(action);
}
}

for (const auto &player : game->GetPlayersWithChance()) {
for (const auto &infoset : player->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
{
m_cache.m_beliefs.clear();

// Normalise each member's realization probability by the infoset's upper-frontier probability
// (m_infosetProbs, computed in ComputeRealizationProbs), following Halpern and Pass (2021).
// For an absent-minded infoset the frontier excludes the reentry members, so the member beliefs
// may sum to above 1; for a non-absent-minded infoset the frontier is all members and this is
// the standard Selten (1975) normalization.
for (const auto &infoset : m_support.GetGame()->GetInfosets()) {
const T infosetProb = sum_function(
infoset->GetMembers(), [&](const auto &node) -> T { return m_cache.m_realizProbs[node]; });
const T infosetProb = m_cache.m_infosetProbs[infoset];
if (infosetProb == static_cast<T>(0)) {
continue;
}
Expand Down
13 changes: 10 additions & 3 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 All @@ -72,10 +74,11 @@ template <class T> class MixedBehaviorProfile {

/// @name Auxiliary functions for cached computation of interesting values
//@{
/// Compute the realisation probabilities of all nodes
/// Compute the realization probabilities of all nodes, and of information sets
/// (the probability a given information set is reached at least once)
void ComputeRealizationProbs() const;
/// Compute the realisation probabilities of information sets, and beliefs at
/// information sets reached with positive probability
/// Compute beliefs (conditional reach probabilities) at information sets reached
/// with positive probability, normalized over each set's upper frontier
void ComputeBeliefs() const;
/// Compute the expected payoffs conditional on reaching each node
void ComputeNodeValues() const;
Expand Down Expand Up @@ -241,6 +244,10 @@ template <class T> class MixedBehaviorProfile {

const T &GetRealizProb(const GameNode &node) const;
T GetInfosetProb(const GameInfoset &p_infoset) const;
/// Returns the belief at a given non-terminal node:
/// the probability of reaching it conditional on reaching its infoset,
/// normalised over the infoset's upper frontier (Halpern and Pass, 2021).
/// Returns std::nullopt for a terminal node, or if the infoset is not reached.
std::optional<T> GetBeliefProb(const GameNode &node) const;
Vector<T> GetPayoff(const GameNode &node) const;
const T &GetPayoff(const GamePlayer &p_player, const GameNode &p_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 @@ -1055,6 +1055,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<GameSubgame> GetSubgames() const { throw UndefinedException(); }
/// Returns the smallest subgame containing the information set
Expand Down
46 changes: 40 additions & 6 deletions src/games/gametree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,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 All @@ -868,11 +869,9 @@ bool GameTreeRep::IsAbsentMinded(const GameInfoset &p_infoset) const
if (p_infoset->GetGame().get() != this) {
throw MismatchException();
}

if (!m_unreachableNodes && !m_root->IsTerminal()) {
BuildUnreachableNodes();
if (!m_ownPriorActionInfo) {
BuildOwnPriorActions();
}

return contains(m_absentMindedInfosets, p_infoset.get());
}

Expand All @@ -891,6 +890,23 @@ GameSubgame GameTreeRep::GetMinimalSubgame(const GameInfoset &p_infoset) const
return it->second;
}

std::vector<std::pair<GameInfoset, GameNode>> GameTreeRep::GetAbsentMindedReentries() const
{
if (!m_ownPriorActionInfo) {
BuildOwnPriorActions();
}
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 @@ -962,6 +978,7 @@ void GameTreeRep::ClearComputedValues() const
const_cast<GameTreeRep *>(this)->m_unreachableNodes = nullptr;
m_absentMindedInfosets.clear();
m_subgameData.Invalidate();
m_absentMindedReentries.clear();
m_computedValues = false;
}

Expand Down Expand Up @@ -1057,13 +1074,22 @@ void GameTreeRep::BuildOwnPriorActions() const
{
if (m_root->IsTerminal()) {
m_ownPriorActionInfo = std::make_shared<OwnPriorActionInfo>();
m_absentMindedInfosets.clear();
m_absentMindedReentries.clear();
return;
}

struct OwnPriorActionsVisitor {
std::shared_ptr<OwnPriorActionInfo> m_info;
std::map<GamePlayer, std::stack<GameAction>> m_priorActions;

// A node is a re-entry of its information set iff an ancestor on the current
// root-to-node path shares that information set. m_pathMemberCount counts, per information
// set, how many nodes on the current path belong to it.
std::map<GameInfosetRep *, int> m_pathMemberCount;
std::set<GameInfosetRep *> m_absentMindedInfosets;
std::vector<std::pair<GameInfosetRep *, GameNodeRep *>> m_absentMindedReentries;

explicit OwnPriorActionsVisitor(const GameTreeRep *p_game)
: m_info(std::make_shared<OwnPriorActionInfo>())
{
Expand All @@ -1082,6 +1108,11 @@ void GameTreeRep::BuildOwnPriorActions() const
m_info->infoset_map[infoset].insert(raw_prior);

stack.emplace(nullptr);

if (m_pathMemberCount[infoset]++ > 0) {
m_absentMindedInfosets.insert(infoset);
m_absentMindedReentries.emplace_back(infoset, p_node.get());
}
}
return DFSCallbackResult::Continue;
}
Expand All @@ -1097,6 +1128,7 @@ void GameTreeRep::BuildOwnPriorActions() const
{
if (auto *infoset = p_node->m_infoset) {
m_priorActions.at(infoset->m_player->shared_from_this()).pop();
m_pathMemberCount[infoset]--;
}
return DFSCallbackResult::Continue;
}
Expand All @@ -1110,6 +1142,8 @@ void GameTreeRep::BuildOwnPriorActions() const
visitor);

m_ownPriorActionInfo = visitor.m_info;
m_absentMindedInfosets = std::move(visitor.m_absentMindedInfosets);
m_absentMindedReentries = std::move(visitor.m_absentMindedReentries);
}

GameAction GameTreeRep::GetOwnPriorAction(const GameNode &p_node) const
Expand Down Expand Up @@ -1184,9 +1218,9 @@ void GameTreeRep::BuildUnreachableNodes() const
}

if (!child->IsTerminal()) {
// Check for Absent-Minded Re-entry of the infoset
// On a re-entry, a pure strategy replays the action chosen at the earlier visit,
// so only that branch is reachable; prune the rest.
if (path_choices.find(child->m_infoset->shared_from_this()) != path_choices.end()) {
m_absentMindedInfosets.insert(child->m_infoset);
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 @@ -50,6 +50,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;
// The subgames of the game, held in two synchronized forms:
// m_subgamePostorder for iteration (children before parents),
// m_subgameByRoot for O(1) lookup by root node and ownership of the GameSubgameRep objects.
Expand Down Expand Up @@ -125,6 +126,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<GameSubgame> GetSubgames() const override;
GameSubgame GetMinimalSubgame(const GameInfoset &) const override;
//@}
Expand Down
29 changes: 25 additions & 4 deletions src/pygambit/behavmixed.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -591,8 +591,17 @@ class MixedBehaviorProfile:
"""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`.
The conditioning event is that the information set is reached at least once,
so beliefs are normalized by the upper-frontier probability returned by
`infoset_prob` (following :cite:p:`HalPas21`), rather than by the sum of the
members' realization probabilities. For a non-absent-minded information set
the two approaches agree. For an absent-minded information set they need not:
the beliefs over its members may sum to more than one.

If the information set is reached with zero probability under the profile, the
belief is not well-defined and the function returns `None`. This is the same
reach probability returned by `infoset_prob`, so a `None` belief corresponds
exactly to `infoset_prob` being zero there.

Parameters
----------
Expand Down Expand Up @@ -751,13 +760,21 @@ class MixedBehaviorProfile:
self._check_validity()
return self._realiz_prob(self.game._resolve_node(node, "realiz_prob"))

def infoset_prob(self, infoset: NodeReference) -> ProfileDType:
def infoset_prob(self, infoset: InfosetReference) -> ProfileDType:
"""Returns the probability with which an information set is reached.

This is the probability that the information set is reached *at least once*
under the profile: the realization probability of its upper frontier, i.e. the
members not preceded by another member of the same information set.
For a non-absent-minded information set, its upper frontier coincides with it.
For an absent-minded information set, with a play passing through it more than once,
the members below its frontier are excluded, so this is generally less than
the sum of the members' realization probabilities; see :cite:p:`HalPas21`.

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 for. If a string is passed, the
information set is determined by finding the information set with that label, if any.

Raises
Expand All @@ -766,6 +783,10 @@ class MixedBehaviorProfile:
If `infoset` is an ``Infoset`` from a different game.
KeyError
If `infoset` is a string and no information set in the game has that label.

See Also
--------
MixedBehaviorProfile.belief
"""
self._check_validity()
return self._infoset_prob(self.game._resolve_infoset(infoset, "infoset_prob"))
Expand Down
Loading
Loading