diff --git a/src/games/behavmixed.cc b/src/games/behavmixed.cc index 42498ad87..477a9d061 100644 --- a/src/games/behavmixed.cc +++ b/src/games/behavmixed.cc @@ -284,8 +284,7 @@ template T MixedBehaviorProfile::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 @@ -471,6 +470,7 @@ T MixedBehaviorProfile::DiffNodeValue(const GameNode &p_node, const GamePlaye template void MixedBehaviorProfile::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(1); @@ -480,6 +480,21 @@ template void MixedBehaviorProfile::ComputeRealizationProbs() const m_cache.m_realizProbs[child] = incomingProb * GetActionProb(action); } } + + for (const auto &infoset : game->GetInfosets()) { + if (game->IsAbsentMinded(infoset)) { + m_cache.m_infosetProbs[infoset] = + sum_function(infoset->GetMembers(), [&](const auto &node) -> T { + return game->IsAbsentMindedReentry(node) ? static_cast(0) + : m_cache.m_realizProbs[node]; + }); + } + else { + m_cache.m_infosetProbs[infoset] = + sum_function(infoset->GetMembers(), + [&](const auto &node) -> T { return m_cache.m_realizProbs[node]; }); + } + } } template void MixedBehaviorProfile::ComputeBeliefs() const diff --git a/src/games/behavmixed.h b/src/games/behavmixed.h index d535560c2..15392606d 100644 --- a/src/games/behavmixed.h +++ b/src/games/behavmixed.h @@ -46,6 +46,7 @@ template class MixedBehaviorProfile { Level m_level{Level::None}; std::map m_realizProbs, m_beliefs; + std::map m_infosetProbs; std::map> m_nodeValues; std::map m_infosetValues; std::map m_actionValues; @@ -60,6 +61,7 @@ template class MixedBehaviorProfile { { m_level = Level::None; m_realizProbs.clear(); + m_infosetProbs.clear(); m_beliefs.clear(); m_nodeValues.clear(); m_infosetValues.clear(); diff --git a/src/games/game.h b/src/games/game.h index fe6a4d1b8..bd17c2827 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -939,6 +939,14 @@ class GameRep : public std::enable_shared_from_this { } return false; } + /// Returns whether the path from the root to p_node passes through its infoset more than once + virtual bool IsAbsentMindedReentry(const GameNode &p_node) const + { + if (p_node->GetGame().get() != this) { + throw MismatchException(); + } + return false; + } /// Returns a list of all subgame roots in the game virtual std::vector GetSubgames() const { throw UndefinedException(); } diff --git a/src/games/gametree.cc b/src/games/gametree.cc index fd0ae033d..6be6ede9a 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -859,6 +859,23 @@ bool GameTreeRep::IsAbsentMinded(const GameInfoset &p_infoset) const return contains(m_absentMindedInfosets, p_infoset.get()); } +bool GameTreeRep::IsAbsentMindedReentry(const GameNode &p_node) const +{ + if (p_node->GetGame().get() != this) { + throw MismatchException(); + } + + if (!m_unreachableNodes && !m_root->IsTerminal()) { + BuildUnreachableNodes(); + } + + auto it = m_absentMindedReentries.find(p_node->m_infoset); + if (it == m_absentMindedReentries.end()) { + return false; + } + return contains(it->second, p_node.get()); +} + //------------------------------------------------------------------------ // GameTreeRep: Managing the representation //------------------------------------------------------------------------ @@ -924,6 +941,7 @@ void GameTreeRep::ClearComputedValues() const m_ownPriorActionInfo = nullptr; const_cast(this)->m_unreachableNodes = nullptr; m_absentMindedInfosets.clear(); + m_absentMindedReentries.clear(); m_subgames.clear(); m_computedValues = false; } @@ -1100,6 +1118,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[child->m_infoset].insert(child.get()); const GameAction replay_action = path_choices.at(child->m_infoset->shared_from_this()); position.emplace(AbsentMindedEdge{replay_action, child}); diff --git a/src/games/gametree.h b/src/games/gametree.h index aaf86ff7f..003f99035 100644 --- a/src/games/gametree.h +++ b/src/games/gametree.h @@ -47,6 +47,7 @@ class GameTreeRep final : public GameExplicitRep { mutable std::shared_ptr m_ownPriorActionInfo; mutable std::unique_ptr> m_unreachableNodes; mutable std::set m_absentMindedInfosets; + mutable std::map> m_absentMindedReentries; mutable std::vector m_subgames; /// @name Private auxiliary functions @@ -99,6 +100,8 @@ 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; + /// Returns whether the path from the root to p_node passes through its infoset more than once + bool IsAbsentMindedReentry(const GameNode &p_node) const override; std::vector GetSubgames() const override; //@} diff --git a/tests/test_behav.py b/tests/test_behav.py index b45c8d10e..fe9db8e2c 100644 --- a/tests/test_behav.py +++ b/tests/test_behav.py @@ -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", [