From 5e1ca01f3ed298164b722c854f273ec988493bf1 Mon Sep 17 00:00:00 2001 From: drdkad Date: Fri, 9 May 2025 11:01:45 +0100 Subject: [PATCH 01/11] Add GetPlays and implementation details to GameTreeRep --- src/games/game.h | 3 +++ src/games/gameagg.h | 2 ++ src/games/gamebagg.h | 2 ++ src/games/gametable.h | 2 ++ src/games/gametree.cc | 34 ++++++++++++++++++++++++++++++++++ src/games/gametree.h | 7 +++++++ 6 files changed, 50 insertions(+) diff --git a/src/games/game.h b/src/games/game.h index 41af10140..56f5adc16 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -474,6 +474,9 @@ class GameRep : public BaseGameRep { /// Returns the largest payoff to the player in any outcome of the game virtual Rational GetMaxPayoff(const GamePlayer &p_player) const = 0; + /// Returns the set of terminal nodes which are descendants of node + virtual std::vector GetPlays(GameNode node) const = 0; + /// Returns true if the game is perfect recall. If not, /// a pair of violating information sets is returned in the parameters. virtual bool IsPerfectRecall(GameInfoset &, GameInfoset &) const = 0; diff --git a/src/games/gameagg.h b/src/games/gameagg.h index 7390714a8..7bdae9e10 100644 --- a/src/games/gameagg.h +++ b/src/games/gameagg.h @@ -103,6 +103,8 @@ class GameAGGRep : public GameRep { size_t NumNodes() const override { throw UndefinedException(); } /// Returns the number of non-terminal nodes in the game size_t NumNonterminalNodes() const override { throw UndefinedException(); } + /// Returns the set of terminal nodes which are descendants of node + std::vector GetPlays(GameNode node) const override { throw UndefinedException(); } //@} /// @name General data access diff --git a/src/games/gamebagg.h b/src/games/gamebagg.h index 0a576f961..7c95410bb 100644 --- a/src/games/gamebagg.h +++ b/src/games/gamebagg.h @@ -111,6 +111,8 @@ class GameBAGGRep : public GameRep { size_t NumNodes() const override { throw UndefinedException(); } /// Returns the number of non-terminal nodes in the game size_t NumNonterminalNodes() const override { throw UndefinedException(); } + /// Returns the set of terminal nodes which are descendants of node + std::vector GetPlays(GameNode node) const override { throw UndefinedException(); } //@} /// @name General data access diff --git a/src/games/gametable.h b/src/games/gametable.h index bbfc243c9..1041fe636 100644 --- a/src/games/gametable.h +++ b/src/games/gametable.h @@ -92,6 +92,8 @@ class GameTableRep : public GameExplicitRep { size_t NumNodes() const override { throw UndefinedException(); } /// Returns the number of non-terminal nodes in the game size_t NumNonterminalNodes() const override { throw UndefinedException(); } + /// Returns the set of terminal nodes which are descendants of node + std::vector GetPlays(GameNode node) const override { throw UndefinedException(); } //@} /// @name Outcomes diff --git a/src/games/gametree.cc b/src/games/gametree.cc index 711195609..255056d5e 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -886,6 +886,7 @@ void GameTreeRep::ClearComputedValues() const } player->m_strategies.clear(); } + const_cast(this)->m_nodePlays.clear(); m_computedValues = false; } @@ -894,6 +895,7 @@ void GameTreeRep::BuildComputedValues() const if (m_computedValues) { return; } + const_cast(this)->BuildConsistentPlays(); const_cast(this)->Canonicalize(); for (const auto &player : m_players) { player->MakeReducedStrats(m_root, nullptr); @@ -901,6 +903,29 @@ void GameTreeRep::BuildComputedValues() const m_computedValues = true; } +void GameTreeRep::BuildConsistentPlays() +{ + m_nodePlays.clear(); + BuildConsistentPlaysRecursiveImpl(m_root); +} + +std::vector GameTreeRep::BuildConsistentPlaysRecursiveImpl(GameNodeRep *node) +{ + std::vector consistent_plays; + if (node->IsTerminal()) { + consistent_plays = std::vector{node}; + } + else { + for (GameNodeRep *child : node->GetChildren()) { + auto child_consisent_plays = BuildConsistentPlaysRecursiveImpl(child); + consistent_plays.insert(consistent_plays.end(), child_consisent_plays.begin(), + child_consisent_plays.end()); + } + } + m_nodePlays[node] = consistent_plays; + return consistent_plays; +} + //------------------------------------------------------------------------ // GameTreeRep: Writing data files //------------------------------------------------------------------------ @@ -1040,6 +1065,15 @@ Array GameTreeRep::NumInfosets() const // GameTreeRep: Outcomes //------------------------------------------------------------------------ +std::vector GameTreeRep::GetPlays(GameNode node) const +{ + BuildComputedValues(); + std::vector consistent_plays = m_nodePlays.at(node); + std::vector consistent_plays_copy; + std::copy(consistent_plays.cbegin(), consistent_plays.cend(), consistent_plays_copy.begin()); + return consistent_plays_copy; +} + void GameTreeRep::DeleteOutcome(const GameOutcome &p_outcome) { IncrementVersion(); diff --git a/src/games/gametree.h b/src/games/gametree.h index 76347dba2..e253b9b66 100644 --- a/src/games/gametree.h +++ b/src/games/gametree.h @@ -38,6 +38,7 @@ class GameTreeRep : public GameExplicitRep { GamePlayerRep *m_chance; std::size_t m_numNodes = 1; std::size_t m_numNonterminalNodes = 0; + std::map> m_nodePlays; /// @name Private auxiliary functions //@{ @@ -50,6 +51,7 @@ class GameTreeRep : public GameExplicitRep { //@{ void Canonicalize(); void BuildComputedValues() const override; + void BuildConsistentPlays(); void ClearComputedValues() const; /// Removes the node from the information set, invalidating if emptied @@ -143,6 +145,8 @@ class GameTreeRep : public GameExplicitRep { void DeleteAction(GameAction) override; void SetOutcome(GameNode, const GameOutcome &p_outcome) override; + std::vector GetPlays(GameNode node) const override; + Game CopySubgame(GameNode) const override; //@} @@ -153,6 +157,9 @@ class GameTreeRep : public GameExplicitRep { NewMixedStrategyProfile(double, const StrategySupportProfile &) const override; MixedStrategyProfile NewMixedStrategyProfile(const Rational &, const StrategySupportProfile &) const override; + +private: + std::vector BuildConsistentPlaysRecursiveImpl(GameNodeRep *node); }; template class TreeMixedStrategyProfileRep : public MixedStrategyProfileRep { From 66da4173b34e20e26cd815b0666d6ae95c13a66e Mon Sep 17 00:00:00 2001 From: drdkad Date: Tue, 13 May 2025 16:25:46 +0100 Subject: [PATCH 02/11] Fix implementation of GameTreeRep::GetPlays --- src/games/gametree.cc | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/games/gametree.cc b/src/games/gametree.cc index 255056d5e..36609cbcf 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -1068,9 +1068,15 @@ Array GameTreeRep::NumInfosets() const std::vector GameTreeRep::GetPlays(GameNode node) const { BuildComputedValues(); - std::vector consistent_plays = m_nodePlays.at(node); + + const std::vector &consistent_plays = m_nodePlays.at(node); std::vector consistent_plays_copy; - std::copy(consistent_plays.cbegin(), consistent_plays.cend(), consistent_plays_copy.begin()); + consistent_plays_copy.reserve(consistent_plays.size()); + + std::transform(consistent_plays.cbegin(), consistent_plays.cend(), + std::back_inserter(consistent_plays_copy), + [](GameNodeRep *rep_ptr) -> GameNode { return GameNode(rep_ptr); }); + return consistent_plays_copy; } From b7672369b47d018ad4a2e00779b981f177b62174 Mon Sep 17 00:00:00 2001 From: drdkad Date: Tue, 13 May 2025 16:29:38 +0100 Subject: [PATCH 03/11] Add Game.get_plays method and a test --- src/pygambit/gambit.pxd | 1 + src/pygambit/game.pxi | 9 +++++++++ tests/test_node.py | 15 +++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index 8efd884fe..a84250f7f 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -208,6 +208,7 @@ cdef extern from "games/game.h": c_Rational GetMinPayoff(c_GamePlayer) except + c_Rational GetMaxPayoff() except + c_Rational GetMaxPayoff(c_GamePlayer) except + + stdvector[c_GameNode] GetPlays(c_GameNode) except + bool IsPerfectRecall() except + c_GameInfoset AppendMove(c_GameNode, c_GamePlayer, int) except +ValueError diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 734b28456..92dd6ce71 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -2013,3 +2013,12 @@ class Game: if len(resolved_strategy.player.strategies) == 1: raise UndefinedOperationError("Cannot delete the only strategy for a player") self.game.deref().DeleteStrategy(resolved_strategy.strategy) + + def get_plays(self, node: typing.Union[Node, str]) -> typing.List[Node]: + resolved_node = cython.cast(Node, self._resolve_node(node, "get_plays", "node_obj")) + + plays = [] + for item in self.game.deref().GetPlays(resolved_node.node): + plays.append(Node.wrap(item)) + + return plays diff --git a/tests/test_node.py b/tests/test_node.py index b4c25595e..5f3446ca2 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -772,3 +772,18 @@ def test_nonterminal_len_after_copy_tree(): assert len(game._nonterminal_nodes) == initial_number_of_nodes \ + number_of_nonterminal_src_ancestors + + +def test_get_plays_node(): + """Verify `get_plays` returns plays reachable from a given node. + """ + game = games.read_from_file("e02.efg") + list_nodes = list(game.nodes) + + test_node = list_nodes[2] # path=[1] + + expected_set_of_plays = { + list_nodes[3], list_nodes[5], list_nodes[6] + } # paths=[0, 1], [0, 1, 1], [1, 1, 1] + + assert set(game.get_plays(test_node)) == expected_set_of_plays From 724ccb74526576387ea7d85296f35ba7bcf21cfb Mon Sep 17 00:00:00 2001 From: drdkad Date: Tue, 13 May 2025 18:30:19 +0100 Subject: [PATCH 04/11] Overload GetPlays for Infoset and Action; clean GameTreeRep::GetPlays(GameNode node) --- src/games/game.h | 4 ++++ src/games/gameagg.h | 7 +++++++ src/games/gamebagg.h | 7 +++++++ src/games/gametable.h | 7 +++++++ src/games/gametree.cc | 27 ++++++++++++++++++++++++++- src/games/gametree.h | 2 ++ src/pygambit/gambit.pxd | 2 ++ src/pygambit/game.pxi | 22 +++++++++++++--------- 8 files changed, 68 insertions(+), 10 deletions(-) diff --git a/src/games/game.h b/src/games/game.h index 56f5adc16..93135ca82 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -476,6 +476,10 @@ class GameRep : public BaseGameRep { /// Returns the set of terminal nodes which are descendants of node virtual std::vector GetPlays(GameNode node) const = 0; + /// Returns the set of terminal nodes which are descendants of members of an infoset + virtual std::vector GetPlays(GameInfoset infoset) const = 0; + /// Returns the set of terminal nodes which are descendants of members of an action + virtual std::vector GetPlays(GameAction action) const = 0; /// Returns true if the game is perfect recall. If not, /// a pair of violating information sets is returned in the parameters. diff --git a/src/games/gameagg.h b/src/games/gameagg.h index 7bdae9e10..0dbac253b 100644 --- a/src/games/gameagg.h +++ b/src/games/gameagg.h @@ -105,6 +105,13 @@ class GameAGGRep : public GameRep { size_t NumNonterminalNodes() const override { throw UndefinedException(); } /// Returns the set of terminal nodes which are descendants of node std::vector GetPlays(GameNode node) const override { throw UndefinedException(); } + /// Returns the set of terminal nodes which are descendants of members of an infoset + std::vector GetPlays(GameInfoset infoset) const override + { + throw UndefinedException(); + } + /// Returns the set of terminal nodes which are descendants of members of an action + std::vector GetPlays(GameAction action) const override { throw UndefinedException(); } //@} /// @name General data access diff --git a/src/games/gamebagg.h b/src/games/gamebagg.h index 7c95410bb..784d72d40 100644 --- a/src/games/gamebagg.h +++ b/src/games/gamebagg.h @@ -113,6 +113,13 @@ class GameBAGGRep : public GameRep { size_t NumNonterminalNodes() const override { throw UndefinedException(); } /// Returns the set of terminal nodes which are descendants of node std::vector GetPlays(GameNode node) const override { throw UndefinedException(); } + /// Returns the set of terminal nodes which are descendants of members of an infoset + std::vector GetPlays(GameInfoset infoset) const override + { + throw UndefinedException(); + } + /// Returns the set of terminal nodes which are descendants of members of an action + std::vector GetPlays(GameAction action) const override { throw UndefinedException(); } //@} /// @name General data access diff --git a/src/games/gametable.h b/src/games/gametable.h index 1041fe636..eaa45a8a5 100644 --- a/src/games/gametable.h +++ b/src/games/gametable.h @@ -94,6 +94,13 @@ class GameTableRep : public GameExplicitRep { size_t NumNonterminalNodes() const override { throw UndefinedException(); } /// Returns the set of terminal nodes which are descendants of node std::vector GetPlays(GameNode node) const override { throw UndefinedException(); } + /// Returns the set of terminal nodes which are descendants of members of an infoset + std::vector GetPlays(GameInfoset infoset) const override + { + throw UndefinedException(); + } + /// Returns the set of terminal nodes which are descendants of members of an action + std::vector GetPlays(GameAction action) const override { throw UndefinedException(); } //@} /// @name Outcomes diff --git a/src/games/gametree.cc b/src/games/gametree.cc index 36609cbcf..1cff18027 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -1075,11 +1075,36 @@ std::vector GameTreeRep::GetPlays(GameNode node) const std::transform(consistent_plays.cbegin(), consistent_plays.cend(), std::back_inserter(consistent_plays_copy), - [](GameNodeRep *rep_ptr) -> GameNode { return GameNode(rep_ptr); }); + [](GameNodeRep *rep_ptr) -> GameNode { return {rep_ptr}; }); return consistent_plays_copy; } +std::vector GameTreeRep::GetPlays(GameInfoset infoset) const +{ + std::vector plays; + auto members = infoset->GetMembers(); + + for (const GameNode &node : members) { + std::vector member_plays = GetPlays(node); + plays.insert(plays.end(), member_plays.begin(), member_plays.end()); + } + return plays; +} + +std::vector GameTreeRep::GetPlays(GameAction action) const +{ + std::vector plays; + auto infoset = action->GetInfoset(); + auto members = infoset->GetMembers(); + + for (const GameNode &node : members) { + std::vector child_plays = GetPlays(node->GetChild(action)); + plays.insert(plays.end(), child_plays.begin(), child_plays.end()); + } + return plays; +} + void GameTreeRep::DeleteOutcome(const GameOutcome &p_outcome) { IncrementVersion(); diff --git a/src/games/gametree.h b/src/games/gametree.h index e253b9b66..74ffc9865 100644 --- a/src/games/gametree.h +++ b/src/games/gametree.h @@ -146,6 +146,8 @@ class GameTreeRep : public GameExplicitRep { void SetOutcome(GameNode, const GameOutcome &p_outcome) override; std::vector GetPlays(GameNode node) const override; + std::vector GetPlays(GameInfoset infoset) const override; + std::vector GetPlays(GameAction action) const override; Game CopySubgame(GameNode) const override; //@} diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index a84250f7f..d2449cdef 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -209,6 +209,8 @@ cdef extern from "games/game.h": c_Rational GetMaxPayoff() except + c_Rational GetMaxPayoff(c_GamePlayer) except + stdvector[c_GameNode] GetPlays(c_GameNode) except + + stdvector[c_GameNode] GetPlays(c_GameInfoset) except + + stdvector[c_GameNode] GetPlays(c_GameAction) except + bool IsPerfectRecall() except + c_GameInfoset AppendMove(c_GameNode, c_GamePlayer, int) except +ValueError diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 92dd6ce71..6ef60eacf 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -22,7 +22,6 @@ import io import itertools import pathlib -import warnings import numpy as np import scipy.stats @@ -2014,11 +2013,16 @@ class Game: raise UndefinedOperationError("Cannot delete the only strategy for a player") self.game.deref().DeleteStrategy(resolved_strategy.strategy) - def get_plays(self, node: typing.Union[Node, str]) -> typing.List[Node]: - resolved_node = cython.cast(Node, self._resolve_node(node, "get_plays", "node_obj")) - - plays = [] - for item in self.game.deref().GetPlays(resolved_node.node): - plays.append(Node.wrap(item)) - - return plays + def get_plays(self, obj: typing.Union[Node, Infoset, Action]) -> typing.List[Node]: + if isinstance(obj, Node): + return [Node.wrap(n) for n in self.game.deref().GetPlays(cython.cast(Node, obj).node)] + elif isinstance(obj, Infoset): + return [ + Node.wrap(n) for n in self.game.deref().GetPlays(cython.cast(Infoset, obj).infoset) + ] + elif isinstance(obj, Action): + return [ + Node.wrap(n) for n in self.game.deref().GetPlays(cython.cast(Action, obj).action) + ] + else: + raise TypeError("The object needs to be either Node, Infoset, or Action") From 8fb3d873bbcfe72e4fdf8a42a86d9cbe993bcd5d Mon Sep 17 00:00:00 2001 From: drdkad Date: Wed, 14 May 2025 11:39:56 +0100 Subject: [PATCH 05/11] Add two more tests for Game.get_plays method --- tests/test_node.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_node.py b/tests/test_node.py index 5f3446ca2..d186e8717 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -787,3 +787,35 @@ def test_get_plays_node(): } # paths=[0, 1], [0, 1, 1], [1, 1, 1] assert set(game.get_plays(test_node)) == expected_set_of_plays + + +def test_get_plays_infoset(): + """Verify `get_plays` returns plays reachable from a given infoset. + """ + game = games.read_from_file("e01.efg") + list_nodes = list(game.nodes) + list_infosets = list(game.infosets) + + test_infoset = list_infosets[2] # members' paths=[1, 0], [1] + + expected_set_of_plays = { + list_nodes[4], list_nodes[5], list_nodes[7], list_nodes[8] + } # paths=[0, 1, 0], [1, 1, 0], [0, 1], [1, 1] + + assert set(game.get_plays(test_infoset)) == expected_set_of_plays + + +def test_get_plays_action(): + """Verify `get_plays` returns plays reachable from a given action. + """ + game = games.read_from_file("e01.efg") + list_nodes = list(game.nodes) + list_infosets = list(game.infosets) + + test_action = list_infosets[2].actions[0] # members' paths=[0, 1, 0], [0, 1] + + expected_set_of_plays = { + list_nodes[4], list_nodes[7] + } # paths=[0, 1, 0], [0, 1] + + assert set(game.get_plays(test_action)) == expected_set_of_plays From 46a98963be6213ed7b9ad99c5f1ffe1b02f60f27 Mon Sep 17 00:00:00 2001 From: drdkad Date: Fri, 16 May 2025 08:28:13 +0100 Subject: [PATCH 06/11] Change GameRep::GetPlays to regular virtual method and remove redundant overrides --- src/games/game.h | 6 +++--- src/games/gameagg.h | 9 --------- src/games/gamebagg.h | 9 --------- src/games/gametable.h | 9 --------- 4 files changed, 3 insertions(+), 30 deletions(-) diff --git a/src/games/game.h b/src/games/game.h index 93135ca82..53663a098 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -475,11 +475,11 @@ class GameRep : public BaseGameRep { virtual Rational GetMaxPayoff(const GamePlayer &p_player) const = 0; /// Returns the set of terminal nodes which are descendants of node - virtual std::vector GetPlays(GameNode node) const = 0; + virtual std::vector GetPlays(GameNode node) const { throw UndefinedException(); } /// Returns the set of terminal nodes which are descendants of members of an infoset - virtual std::vector GetPlays(GameInfoset infoset) const = 0; + virtual std::vector GetPlays(GameInfoset infoset) const { throw UndefinedException(); } /// Returns the set of terminal nodes which are descendants of members of an action - virtual std::vector GetPlays(GameAction action) const = 0; + virtual std::vector GetPlays(GameAction action) const { throw UndefinedException(); } /// Returns true if the game is perfect recall. If not, /// a pair of violating information sets is returned in the parameters. diff --git a/src/games/gameagg.h b/src/games/gameagg.h index 0dbac253b..7390714a8 100644 --- a/src/games/gameagg.h +++ b/src/games/gameagg.h @@ -103,15 +103,6 @@ class GameAGGRep : public GameRep { size_t NumNodes() const override { throw UndefinedException(); } /// Returns the number of non-terminal nodes in the game size_t NumNonterminalNodes() const override { throw UndefinedException(); } - /// Returns the set of terminal nodes which are descendants of node - std::vector GetPlays(GameNode node) const override { throw UndefinedException(); } - /// Returns the set of terminal nodes which are descendants of members of an infoset - std::vector GetPlays(GameInfoset infoset) const override - { - throw UndefinedException(); - } - /// Returns the set of terminal nodes which are descendants of members of an action - std::vector GetPlays(GameAction action) const override { throw UndefinedException(); } //@} /// @name General data access diff --git a/src/games/gamebagg.h b/src/games/gamebagg.h index 784d72d40..0a576f961 100644 --- a/src/games/gamebagg.h +++ b/src/games/gamebagg.h @@ -111,15 +111,6 @@ class GameBAGGRep : public GameRep { size_t NumNodes() const override { throw UndefinedException(); } /// Returns the number of non-terminal nodes in the game size_t NumNonterminalNodes() const override { throw UndefinedException(); } - /// Returns the set of terminal nodes which are descendants of node - std::vector GetPlays(GameNode node) const override { throw UndefinedException(); } - /// Returns the set of terminal nodes which are descendants of members of an infoset - std::vector GetPlays(GameInfoset infoset) const override - { - throw UndefinedException(); - } - /// Returns the set of terminal nodes which are descendants of members of an action - std::vector GetPlays(GameAction action) const override { throw UndefinedException(); } //@} /// @name General data access diff --git a/src/games/gametable.h b/src/games/gametable.h index eaa45a8a5..bbfc243c9 100644 --- a/src/games/gametable.h +++ b/src/games/gametable.h @@ -92,15 +92,6 @@ class GameTableRep : public GameExplicitRep { size_t NumNodes() const override { throw UndefinedException(); } /// Returns the number of non-terminal nodes in the game size_t NumNonterminalNodes() const override { throw UndefinedException(); } - /// Returns the set of terminal nodes which are descendants of node - std::vector GetPlays(GameNode node) const override { throw UndefinedException(); } - /// Returns the set of terminal nodes which are descendants of members of an infoset - std::vector GetPlays(GameInfoset infoset) const override - { - throw UndefinedException(); - } - /// Returns the set of terminal nodes which are descendants of members of an action - std::vector GetPlays(GameAction action) const override { throw UndefinedException(); } //@} /// @name Outcomes From 0e5a9c31d2e252e341011dbd9e0f1d6e6ae79c9b Mon Sep 17 00:00:00 2001 From: drdkad Date: Fri, 16 May 2025 08:38:54 +0100 Subject: [PATCH 07/11] BuildConsistentPlays call optimization --- src/games/gametree.cc | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/games/gametree.cc b/src/games/gametree.cc index 1cff18027..6620100f4 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -895,7 +895,6 @@ void GameTreeRep::BuildComputedValues() const if (m_computedValues) { return; } - const_cast(this)->BuildConsistentPlays(); const_cast(this)->Canonicalize(); for (const auto &player : m_players) { player->MakeReducedStrats(m_root, nullptr); @@ -1067,7 +1066,7 @@ Array GameTreeRep::NumInfosets() const std::vector GameTreeRep::GetPlays(GameNode node) const { - BuildComputedValues(); + const_cast(this)->BuildConsistentPlays(); const std::vector &consistent_plays = m_nodePlays.at(node); std::vector consistent_plays_copy; @@ -1083,9 +1082,8 @@ std::vector GameTreeRep::GetPlays(GameNode node) const std::vector GameTreeRep::GetPlays(GameInfoset infoset) const { std::vector plays; - auto members = infoset->GetMembers(); - for (const GameNode &node : members) { + for (const GameNode &node : infoset->GetMembers()) { std::vector member_plays = GetPlays(node); plays.insert(plays.end(), member_plays.begin(), member_plays.end()); } @@ -1095,10 +1093,8 @@ std::vector GameTreeRep::GetPlays(GameInfoset infoset) const std::vector GameTreeRep::GetPlays(GameAction action) const { std::vector plays; - auto infoset = action->GetInfoset(); - auto members = infoset->GetMembers(); - for (const GameNode &node : members) { + for (const GameNode &node : action->GetInfoset()->GetMembers()) { std::vector child_plays = GetPlays(node->GetChild(action)); plays.insert(plays.end(), child_plays.begin(), child_plays.end()); } From eb56c26330295de296906b63f49039b479360f20 Mon Sep 17 00:00:00 2001 From: drdkad Date: Fri, 16 May 2025 08:48:04 +0100 Subject: [PATCH 08/11] Change wording in the TypeError in Game.get_plays --- src/pygambit/game.pxi | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 6ef60eacf..d90d1f08f 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -2025,4 +2025,6 @@ class Game: Node.wrap(n) for n in self.game.deref().GetPlays(cython.cast(Action, obj).action) ] else: - raise TypeError("The object needs to be either Node, Infoset, or Action") + raise TypeError( + f"obj must be either Node, Infoset, or Action, not {obj.__class__.__name__}" + ) From 8360f14794a2ffacd46f3b83c664320d5b9c1bb3 Mon Sep 17 00:00:00 2001 From: drdkad Date: Mon, 19 May 2025 12:57:46 +0100 Subject: [PATCH 09/11] Add an entry in ChangeLog, a function to the documentation index, and a docstring for Game.get_plays method --- ChangeLog | 7 +++++++ doc/pygambit.api.rst | 1 + src/games/gametree.cc | 6 +++--- src/pygambit/game.pxi | 19 +++++++++++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 44821fff3..dd620c0bd 100644 --- a/ChangeLog +++ b/ChangeLog @@ -9,6 +9,13 @@ - The deprecated functions `Game.read_game`, `Game.parse_game` and `Game.write` functions have been removed as planned. (#357) +### Added +- Introduced functionality to identify terminal plays consistent with specific game elements: + In Python, `Game.get_plays(obj)` now returns a list of terminal `Node` objects (plays) + that are descendants of the input `obj` (which can be a `Node`, `Infoset`, or `Action`). + Corresponding C++ methods `GameRep::GetPlays(GameNode node) const`, + `GameRep::GetPlays(GameInfoset infoset) const`, and + `GameRep::GetPlays(GameAction action) const` were implemented to support this. ## [16.3.1] - unreleased diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index 3b32c7f02..016a285bd 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -107,6 +107,7 @@ Information about the game Game.actions Game.infosets Game.nodes + Game.get_plays Game.contingencies .. autosummary:: diff --git a/src/games/gametree.cc b/src/games/gametree.cc index 6620100f4..dbb168f86 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -916,9 +916,9 @@ std::vector GameTreeRep::BuildConsistentPlaysRecursiveImpl(GameNo } else { for (GameNodeRep *child : node->GetChildren()) { - auto child_consisent_plays = BuildConsistentPlaysRecursiveImpl(child); - consistent_plays.insert(consistent_plays.end(), child_consisent_plays.begin(), - child_consisent_plays.end()); + auto child_consistent_plays = BuildConsistentPlaysRecursiveImpl(child); + consistent_plays.insert(consistent_plays.end(), child_consistent_plays.begin(), + child_consistent_plays.end()); } } m_nodePlays[node] = consistent_plays; diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index d90d1f08f..b4e6f43ad 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -2014,6 +2014,25 @@ class Game: self.game.deref().DeleteStrategy(resolved_strategy.strategy) def get_plays(self, obj: typing.Union[Node, Infoset, Action]) -> typing.List[Node]: + """Return terminal nodes consistent with a game object. + + For a given `Node`, `Infoset`, or `Action`, returns a list of all terminal `Node` objects + that represent (terminal) plays of the game consistent with the specified object. + + Parameters + ---------- + obj : Node or Infoset or Action + + Returns + ------- + list of Node + + Raises + ------ + TypeError + If `obj` is not a `Node`, `Infoset`, or `Action` + object from this game. + """ if isinstance(obj, Node): return [Node.wrap(n) for n in self.game.deref().GetPlays(cython.cast(Node, obj).node)] elif isinstance(obj, Infoset): From f4154da6395bc356d5724044f72a5d1ec1b1ab8d Mon Sep 17 00:00:00 2001 From: drdkad Date: Tue, 20 May 2025 07:49:09 +0100 Subject: [PATCH 10/11] Add .plays property to Node, Infoset, and Action classes; remove Game.get_plays method; correct tests and documentation --- ChangeLog | 10 ++++------ doc/pygambit.api.rst | 4 +++- src/pygambit/action.pxi | 9 +++++++++ src/pygambit/game.pxi | 35 ----------------------------------- src/pygambit/infoset.pxi | 8 ++++++++ src/pygambit/node.pxi | 6 ++++++ tests/test_actions.py | 16 ++++++++++++++++ tests/test_infosets.py | 16 ++++++++++++++++ tests/test_node.py | 36 ++---------------------------------- 9 files changed, 64 insertions(+), 76 deletions(-) diff --git a/ChangeLog b/ChangeLog index dd620c0bd..4554f3288 100644 --- a/ChangeLog +++ b/ChangeLog @@ -10,12 +10,10 @@ been removed as planned. (#357) ### Added -- Introduced functionality to identify terminal plays consistent with specific game elements: - In Python, `Game.get_plays(obj)` now returns a list of terminal `Node` objects (plays) - that are descendants of the input `obj` (which can be a `Node`, `Infoset`, or `Action`). - Corresponding C++ methods `GameRep::GetPlays(GameNode node) const`, - `GameRep::GetPlays(GameInfoset infoset) const`, and - `GameRep::GetPlays(GameAction action) const` were implemented to support this. +- Added `Node.plays`, `Infoset.plays`, and `Action.plays` properties. + These properties return a list of terminal `Node` objects representing (terminal) plays + consistent with the specific node, information set, or action. + This functionality is backed by new C++ `GameRep::GetPlays()` overloads. ## [16.3.1] - unreleased diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index 016a285bd..50052a873 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -107,7 +107,6 @@ Information about the game Game.actions Game.infosets Game.nodes - Game.get_plays Game.contingencies .. autosummary:: @@ -146,6 +145,7 @@ Information about the game Node.infoset Node.player Node.is_successor_of + Node.plays .. autosummary:: @@ -158,6 +158,7 @@ Information about the game Infoset.actions Infoset.members Infoset.precedes + Infoset.plays .. autosummary:: @@ -167,6 +168,7 @@ Information about the game Action.infoset Action.precedes Action.prob + Action.plays .. autosummary:: diff --git a/src/pygambit/action.pxi b/src/pygambit/action.pxi index cf465d122..576834b54 100644 --- a/src/pygambit/action.pxi +++ b/src/pygambit/action.pxi @@ -106,3 +106,12 @@ class Action: return decimal.Decimal(py_string.decode("ascii")) else: return Rational(py_string.decode("ascii")) + + @property + def plays(self) -> typing.List[Node]: + """Returns a list of all terminal `Node` objects consistent with it. + """ + return [ + Node.wrap(n) for n in + self.action.deref().GetInfoset().deref().GetGame().deref().GetPlays(self.action) + ] diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index b4e6f43ad..42509c2bc 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -2012,38 +2012,3 @@ class Game: if len(resolved_strategy.player.strategies) == 1: raise UndefinedOperationError("Cannot delete the only strategy for a player") self.game.deref().DeleteStrategy(resolved_strategy.strategy) - - def get_plays(self, obj: typing.Union[Node, Infoset, Action]) -> typing.List[Node]: - """Return terminal nodes consistent with a game object. - - For a given `Node`, `Infoset`, or `Action`, returns a list of all terminal `Node` objects - that represent (terminal) plays of the game consistent with the specified object. - - Parameters - ---------- - obj : Node or Infoset or Action - - Returns - ------- - list of Node - - Raises - ------ - TypeError - If `obj` is not a `Node`, `Infoset`, or `Action` - object from this game. - """ - if isinstance(obj, Node): - return [Node.wrap(n) for n in self.game.deref().GetPlays(cython.cast(Node, obj).node)] - elif isinstance(obj, Infoset): - return [ - Node.wrap(n) for n in self.game.deref().GetPlays(cython.cast(Infoset, obj).infoset) - ] - elif isinstance(obj, Action): - return [ - Node.wrap(n) for n in self.game.deref().GetPlays(cython.cast(Action, obj).action) - ] - else: - raise TypeError( - f"obj must be either Node, Infoset, or Action, not {obj.__class__.__name__}" - ) diff --git a/src/pygambit/infoset.pxi b/src/pygambit/infoset.pxi index aef0afb9e..552b41d32 100644 --- a/src/pygambit/infoset.pxi +++ b/src/pygambit/infoset.pxi @@ -175,3 +175,11 @@ class Infoset: def player(self) -> Player: """The player who has the move at this information set.""" return Player.wrap(self.infoset.deref().GetPlayer()) + + @property + def plays(self) -> typing.List[Node]: + """Returns a list of all terminal `Node` objects consistent with it. + """ + return [ + Node.wrap(n) for n in self.infoset.deref().GetGame().deref().GetPlays(self.infoset) + ] diff --git a/src/pygambit/node.pxi b/src/pygambit/node.pxi index 78741252e..f8ec2bd29 100644 --- a/src/pygambit/node.pxi +++ b/src/pygambit/node.pxi @@ -212,3 +212,9 @@ class Node: if self.node.deref().GetOutcome() == cython.cast(c_GameOutcome, NULL): return None return Outcome.wrap(self.node.deref().GetOutcome()) + + @property + def plays(self) -> typing.List[Node]: + """Returns a list of all terminal `Node` objects consistent with it. + """ + return [Node.wrap(n) for n in self.node.deref().GetGame().deref().GetPlays(self.node)] diff --git a/tests/test_actions.py b/tests/test_actions.py index 10d55ca35..c7d4c000e 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -127,3 +127,19 @@ def test_action_delete_chance(game: gbt.Game): assert p2 == p1 / (1-old_probs[0]) with pytest.raises(gbt.UndefinedOperationError): game.delete_action(chance_iset.actions[0]) + + +def test_action_plays(): + """Verify `get_plays` returns plays reachable from a given action. + """ + game = games.read_from_file("e01.efg") + list_nodes = list(game.nodes) + list_infosets = list(game.infosets) + + test_action = list_infosets[2].actions[0] # members' paths=[0, 1, 0], [0, 1] + + expected_set_of_plays = { + list_nodes[4], list_nodes[7] + } # paths=[0, 1, 0], [0, 1] + + assert set(test_action.plays) == expected_set_of_plays diff --git a/tests/test_infosets.py b/tests/test_infosets.py index 9d9c96495..581895618 100644 --- a/tests/test_infosets.py +++ b/tests/test_infosets.py @@ -53,3 +53,19 @@ def test_infoset_add_action_error(): game = games.read_from_file("basic_extensive_game.efg") with pytest.raises(gbt.MismatchError): game.add_action(game.players[0].infosets[0], game.players[1].infosets[0].actions[0]) + + +def test_infoset_plays(): + """Verify `get_plays` returns plays reachable from a given infoset. + """ + game = games.read_from_file("e01.efg") + list_nodes = list(game.nodes) + list_infosets = list(game.infosets) + + test_infoset = list_infosets[2] # members' paths=[1, 0], [1] + + expected_set_of_plays = { + list_nodes[4], list_nodes[5], list_nodes[7], list_nodes[8] + } # paths=[0, 1, 0], [1, 1, 0], [0, 1], [1, 1] + + assert set(test_infoset.plays) == expected_set_of_plays diff --git a/tests/test_node.py b/tests/test_node.py index d186e8717..af4c48420 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -774,7 +774,7 @@ def test_nonterminal_len_after_copy_tree(): + number_of_nonterminal_src_ancestors -def test_get_plays_node(): +def test_node_plays(): """Verify `get_plays` returns plays reachable from a given node. """ game = games.read_from_file("e02.efg") @@ -786,36 +786,4 @@ def test_get_plays_node(): list_nodes[3], list_nodes[5], list_nodes[6] } # paths=[0, 1], [0, 1, 1], [1, 1, 1] - assert set(game.get_plays(test_node)) == expected_set_of_plays - - -def test_get_plays_infoset(): - """Verify `get_plays` returns plays reachable from a given infoset. - """ - game = games.read_from_file("e01.efg") - list_nodes = list(game.nodes) - list_infosets = list(game.infosets) - - test_infoset = list_infosets[2] # members' paths=[1, 0], [1] - - expected_set_of_plays = { - list_nodes[4], list_nodes[5], list_nodes[7], list_nodes[8] - } # paths=[0, 1, 0], [1, 1, 0], [0, 1], [1, 1] - - assert set(game.get_plays(test_infoset)) == expected_set_of_plays - - -def test_get_plays_action(): - """Verify `get_plays` returns plays reachable from a given action. - """ - game = games.read_from_file("e01.efg") - list_nodes = list(game.nodes) - list_infosets = list(game.infosets) - - test_action = list_infosets[2].actions[0] # members' paths=[0, 1, 0], [0, 1] - - expected_set_of_plays = { - list_nodes[4], list_nodes[7] - } # paths=[0, 1, 0], [0, 1] - - assert set(game.get_plays(test_action)) == expected_set_of_plays + assert set(test_node.plays) == expected_set_of_plays From 238cb0d26b3e39c763acb17cdf9e11bc7fed5bee Mon Sep 17 00:00:00 2001 From: drdkad Date: Tue, 20 May 2025 08:07:40 +0100 Subject: [PATCH 11/11] Correct a typo in tests docstring --- tests/test_actions.py | 2 +- tests/test_infosets.py | 2 +- tests/test_node.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_actions.py b/tests/test_actions.py index c7d4c000e..9b46e7d3d 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -130,7 +130,7 @@ def test_action_delete_chance(game: gbt.Game): def test_action_plays(): - """Verify `get_plays` returns plays reachable from a given action. + """Verify `action.plays` returns plays reachable from a given action. """ game = games.read_from_file("e01.efg") list_nodes = list(game.nodes) diff --git a/tests/test_infosets.py b/tests/test_infosets.py index 581895618..a6f5b6d6a 100644 --- a/tests/test_infosets.py +++ b/tests/test_infosets.py @@ -56,7 +56,7 @@ def test_infoset_add_action_error(): def test_infoset_plays(): - """Verify `get_plays` returns plays reachable from a given infoset. + """Verify `infoset.plays` returns plays reachable from a given infoset. """ game = games.read_from_file("e01.efg") list_nodes = list(game.nodes) diff --git a/tests/test_node.py b/tests/test_node.py index af4c48420..c61b56745 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -775,7 +775,7 @@ def test_nonterminal_len_after_copy_tree(): def test_node_plays(): - """Verify `get_plays` returns plays reachable from a given node. + """Verify `node.plays` returns plays reachable from a given node. """ game = games.read_from_file("e02.efg") list_nodes = list(game.nodes)