From 1cf788d9fc1c8db291307d81d5b19c3c444fa3ee Mon Sep 17 00:00:00 2001 From: drdkad Date: Tue, 11 Nov 2025 16:21:22 +0000 Subject: [PATCH 01/13] Implement `GameTreeRep::GetOwnPriorActions` --- src/games/game.h | 2 ++ src/games/gameagg.h | 4 ++++ src/games/gamebagg.h | 4 ++++ src/games/gametable.h | 5 +++++ src/games/gametree.cc | 21 +++++++++++++++++++++ src/games/gametree.h | 1 + 6 files changed, 37 insertions(+) diff --git a/src/games/game.h b/src/games/game.h index 529dbc507..addacd58e 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -719,6 +719,8 @@ class GameRep : public std::enable_shared_from_this { /// Returns the set of terminal nodes which are descendants of members of an action virtual std::vector GetPlays(GameAction action) const { throw UndefinedException(); } + /// Returns, for a given infoset, the set of the most recent action(s) of the player active in it + virtual std::vector GetOwnPriorActions(GameInfoset infoset) const = 0; /// Returns true if the game is perfect recall virtual bool IsPerfectRecall() const = 0; //@} diff --git a/src/games/gameagg.h b/src/games/gameagg.h index d829dbab1..d1304158c 100644 --- a/src/games/gameagg.h +++ b/src/games/gameagg.h @@ -84,6 +84,10 @@ class GameAGGRep : public GameRep { //@{ bool IsTree() const override { return false; } bool IsAgg() const override { return true; } + std::vector GetOwnPriorActions(GameInfoset infoset) const override + { + throw UndefinedException(); + } bool IsPerfectRecall() const override { return true; } bool IsConstSum() const override; /// Returns the smallest payoff to any player in any outcome of the game diff --git a/src/games/gamebagg.h b/src/games/gamebagg.h index a3deffc9d..e453d5e56 100644 --- a/src/games/gamebagg.h +++ b/src/games/gamebagg.h @@ -91,6 +91,10 @@ class GameBAGGRep : public GameRep { //@{ bool IsTree() const override { return false; } virtual bool IsBagg() const { return true; } + std::vector GetOwnPriorActions(GameInfoset infoset) const override + { + throw UndefinedException(); + } bool IsPerfectRecall() const override { return true; } bool IsConstSum() const override { throw UndefinedException(); } /// Returns the smallest payoff to any player in any outcome of the game diff --git a/src/games/gametable.h b/src/games/gametable.h index 874851d70..9d150ee62 100644 --- a/src/games/gametable.h +++ b/src/games/gametable.h @@ -57,6 +57,11 @@ class GameTableRep : public GameExplicitRep { //@{ bool IsTree() const override { return false; } bool IsConstSum() const override; + std::vector GetOwnPriorActions(GameInfoset infoset) const override + { + throw UndefinedException(); + } + bool IsPerfectRecall() const override { return true; } //@} diff --git a/src/games/gametree.cc b/src/games/gametree.cc index de0c008a2..55d15aee2 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -736,6 +736,27 @@ bool GameTreeRep::IsConstSum() const } } +std::vector GameTreeRep::GetOwnPriorActions(GameInfoset infoset) const +{ + if (m_infosetParents.empty() && !m_root->IsTerminal()) { + const_cast(this)->BuildInfosetParents(); + } + + auto it = m_infosetParents.find(infoset.get()); + if (it == m_infosetParents.end()) { + throw UndefinedException("Cannot get prior actions for an unreached information set."); + } + + std::vector own_prior_actions; + for (auto action : it->second) { + if (action) { + own_prior_actions.push_back(action->shared_from_this()); + } + } + + return own_prior_actions; +} + bool GameTreeRep::IsPerfectRecall() const { if (m_infosetParents.empty() && !m_root->IsTerminal()) { diff --git a/src/games/gametree.h b/src/games/gametree.h index 085cd557b..6dd182f01 100644 --- a/src/games/gametree.h +++ b/src/games/gametree.h @@ -73,6 +73,7 @@ class GameTreeRep : public GameExplicitRep { //@{ bool IsTree() const override { return true; } bool IsConstSum() const override; + std::vector GetOwnPriorActions(GameInfoset infoset) const override; bool IsPerfectRecall() const override; //@} From e671cd2155e75a404e751b8ac994ae9612404c8c Mon Sep 17 00:00:00 2001 From: drdkad Date: Tue, 11 Nov 2025 16:22:28 +0000 Subject: [PATCH 02/13] Implement `Game.get_own_prior_actions` --- src/pygambit/gambit.pxd | 1 + src/pygambit/game.pxi | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index e492d9c00..65ae14b87 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -295,6 +295,7 @@ cdef extern from "games/game.h": stdvector[c_GameNode] GetPlays(c_GameNode) except + stdvector[c_GameNode] GetPlays(c_GameInfoset) except + stdvector[c_GameNode] GetPlays(c_GameAction) except + + stdvector[c_GameAction] GetOwnPriorActions(c_GameInfoset) 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 082b08b62..905e3b1b8 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -777,6 +777,23 @@ class Game: """Whether the game is constant sum.""" return self.game.deref().IsConstSum() + def get_own_prior_actions(self, infoset: typing.Union[Infoset, str]): + """ For a given information set, find the most recent action(s) + of the player active in it which precede this information set. + + Raises + ------ + RuntimeError + If the information set is not reachable from the root of the game. + """ + infoset = self._resolve_infoset(infoset, "get_own_prior_actions") + + own_prior_actions = [] + for action in self.game.deref().GetOwnPriorActions(cython.cast(Infoset, infoset).infoset): + own_prior_actions.append(Action.wrap(action)) + + return own_prior_actions + @property def is_perfect_recall(self) -> bool: """Whether the game is perfect recall. From d75c3066da49f42fd5369d5a3cb3e7982fa4fc05 Mon Sep 17 00:00:00 2001 From: drdkad Date: Tue, 11 Nov 2025 16:24:18 +0000 Subject: [PATCH 03/13] Add tests and update documentation for get_own_prior_actions --- ChangeLog | 4 ++ doc/pygambit.api.rst | 1 + tests/test_extensive.py | 98 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/ChangeLog b/ChangeLog index c98fe0f2e..532e583ed 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,6 +2,10 @@ ## [16.5.0] - unreleased +### Added +- Implement `GameTreeRep::GetOwnPriorActions` (C++) and `Game.get_own_prior_actions` (Python) to compute + the set of actions taken by a player on the path leading to one of their own information sets (#582) + ### Changed - In the graphical interface, removed option to configure information set link drawing; information sets are always drawn and indicators are always drawn if an information set spans multiple levels. diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index 185f69335..7a35e168a 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -109,6 +109,7 @@ Information about the game Game.infosets Game.nodes Game.contingencies + Game.get_own_prior_actions .. autosummary:: :toctree: api/ diff --git a/tests/test_extensive.py b/tests/test_extensive.py index b30c15398..e4bc89bd8 100644 --- a/tests/test_extensive.py +++ b/tests/test_extensive.py @@ -49,6 +49,104 @@ def test_game_add_players_nolabel(): game.add_player() +@pytest.mark.parametrize( + "game_file, infoset_specification, expected_actions", + [ + # ====================================================================== + # Game 1: binary_3_levels_generic_payoffs.efg (Perfect Recall) + # ====================================================================== + # Case: Player 1's root infoset. No prior actions. + ("binary_3_levels_generic_payoffs.efg", ("Player 1", 0), []), + # Case: Player 1's second infoset (L branch). Reached after P1's own + # action 'Left' (0) from their root infoset. + ( + "binary_3_levels_generic_payoffs.efg", + ("Player 1", 1), + [("Player 1", 0, 0)], + ), + # Case: Player 1's third infoset (R branch). Reached after P1's own + # action 'Right' (1) from their root infoset. + ( + "binary_3_levels_generic_payoffs.efg", + ("Player 1", 2), + [("Player 1", 0, 1)], + ), + # Case: Player 2's only infoset. Reached after P1's actions, so it has + # no *own* prior actions for Player 2. + ("binary_3_levels_generic_payoffs.efg", ("Player 2", 0), []), + # ====================================================================== + # GAME 2: wichardt.efg (Imperfect Recall, all infosets reachable) + # ====================================================================== + # Case: The root infoset for Player 1. Should have no prior actions. + ("wichardt.efg", ("Player 1", 0), []), + # Case: Player 1's second infoset. This is reached after P1's action 'L' + # from infoset 0, or P1's action 'R' from infoset 0. A key test case. + ( + "wichardt.efg", + ("Player 1", 1), + [("Player 1", 0, 0), ("Player 1", 0, 1)], + ), + # Case: Player 2's only infoset. Has no *own* prior actions. + ("wichardt.efg", ("Player 2", 0), []), + # ====================================================================== + # GAME 3: noPR-action-AM-two-hops.efg (Absent-Mindedness, one unreached) + # ====================================================================== + # Case: Player 1's infoset 0. Reached via its own action '1' and + # P1's action '2' from infoset 1. + ( + "noPR-action-AM-two-hops.efg", + ("Player 1", 0), + [("Player 1", 0, 0), ("Player 1", 1, 1)], + ), + # Case: Player 1's infoset 1. Reached via P1's action '1' from infoset 0. + ("noPR-action-AM-two-hops.efg", ("Player 1", 1), [("Player 1", 0, 0)]), + # Case: Player 2's infoset 0. Reached via P2's own action '1'. + ("noPR-action-AM-two-hops.efg", ("Player 2", 0), [("Player 2", 0, 0)]), + ], +) +def test_get_own_prior_actions_return_values( + game_file: str, + infoset_specification: tuple[str, int], + expected_actions: list[tuple[str, int, str]], +): + """ + Verifies the get_own_prior_actions method returns correct actions for + a variety of reachable information sets across different games. + """ + game = games.read_from_file(game_file) + player_label, infoset_num = infoset_specification + + player = game.players[player_label] + infoset = player.infosets[infoset_num] + + result_actions = game.get_own_prior_actions(infoset) + + results = [ + ( + action.infoset.player.label, + action.infoset.number, + action.number, + ) + for action in result_actions + ] + + assert set(results) == set(expected_actions) + + +def test_get_own_prior_actions_on_unreached_infoset_raises_error(): + """ + Verifies that calling get_own_prior_actions on an infoset that is + unreachable from the game's root correctly raises a RuntimeError. + """ + game = games.read_from_file("noPR-action-AM-two-hops.efg") + unreached_infoset = game.players["Player 2"].infosets[1] + + with pytest.raises( + RuntimeError, match="Cannot get prior actions for an unreached information set." + ): + game.get_own_prior_actions(unreached_infoset) + + @pytest.mark.parametrize("game_input,expected_result", [ # Games with perfect recall from files (game_input is a string) ("e01.efg", True), From 3e832d078b04eb9f3c1ecb8f2291a0ed951cec03 Mon Sep 17 00:00:00 2001 From: drdkad Date: Tue, 11 Nov 2025 16:31:46 +0000 Subject: [PATCH 04/13] change push_back to emplace_back --- src/games/gametree.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/games/gametree.cc b/src/games/gametree.cc index 55d15aee2..c504d0f53 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -750,7 +750,7 @@ std::vector GameTreeRep::GetOwnPriorActions(GameInfoset infoset) con std::vector own_prior_actions; for (auto action : it->second) { if (action) { - own_prior_actions.push_back(action->shared_from_this()); + own_prior_actions.emplace_back(action->shared_from_this()); } } From 882a7431b5f94f892160fa06a1fafd943f1cf2c5 Mon Sep 17 00:00:00 2001 From: drdkad Date: Tue, 11 Nov 2025 16:39:21 +0000 Subject: [PATCH 05/13] Update ChangeLog --- ChangeLog | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 532e583ed..e11de3766 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,8 +3,9 @@ ## [16.5.0] - unreleased ### Added -- Implement `GameTreeRep::GetOwnPriorActions` (C++) and `Game.get_own_prior_actions` (Python) to compute - the set of actions taken by a player on the path leading to one of their own information sets (#582) +- Implement `GameTreeRep::GetOwnPriorActions` (C++) and `Game.get_own_prior_actions` (Python) + to compute, for a given information set, the set of last actions taken by the player acting + in the information set before reaching it ### Changed - In the graphical interface, removed option to configure information set link drawing; information sets From 7aa5f494998e4fd725bdce0d4c49862c8932c10a Mon Sep 17 00:00:00 2001 From: Ted Turocy Date: Wed, 12 Nov 2025 10:41:28 +0000 Subject: [PATCH 06/13] Update gametree.cc --- src/games/gametree.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/games/gametree.cc b/src/games/gametree.cc index c504d0f53..4d150e851 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -744,7 +744,7 @@ std::vector GameTreeRep::GetOwnPriorActions(GameInfoset infoset) con auto it = m_infosetParents.find(infoset.get()); if (it == m_infosetParents.end()) { - throw UndefinedException("Cannot get prior actions for an unreached information set."); + throw UndefinedException("Cannot get prior actions for an unreachable information set."); } std::vector own_prior_actions; From bcfd76911af3bba971a006cb1453339dfb7ccacc Mon Sep 17 00:00:00 2001 From: drdkad Date: Wed, 12 Nov 2025 17:06:37 +0000 Subject: [PATCH 07/13] Include the null action as a prior action; assign the empty list of prior actions to unreachable sets --- src/games/gametree.cc | 13 +++++++++---- src/pygambit/game.pxi | 30 ++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/games/gametree.cc b/src/games/gametree.cc index 4d150e851..f0f4548e2 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -743,14 +743,19 @@ std::vector GameTreeRep::GetOwnPriorActions(GameInfoset infoset) con } auto it = m_infosetParents.find(infoset.get()); + + // If the infoset is unreachable, return an empty vector. if (it == m_infosetParents.end()) { - throw UndefinedException("Cannot get prior actions for an unreachable information set."); + return {}; } std::vector own_prior_actions; - for (auto action : it->second) { - if (action) { - own_prior_actions.emplace_back(action->shared_from_this()); + for (auto action_ptr : it->second) { + if (action_ptr) { + own_prior_actions.emplace_back(action_ptr->shared_from_this()); + } + else { + own_prior_actions.emplace_back(nullptr); } } diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 905e3b1b8..49a662847 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -777,20 +777,30 @@ class Game: """Whether the game is constant sum.""" return self.game.deref().IsConstSum() - def get_own_prior_actions(self, infoset: typing.Union[Infoset, str]): - """ For a given information set, find the most recent action(s) - of the player active in it which precede this information set. + def get_own_prior_actions( + self, + infoset: typing.Union[Infoset, str] + ) -> typing.List[typing.Optional[Action]]: + """ For a given infoset and the player active in it, + find the set of last action(s) the player took along the paths reaching the infoset. - Raises - ------ - RuntimeError - If the information set is not reachable from the root of the game. + .. versionadded:: 16.5 + + Returns + ------- + list of Action or None + A list of the preceding actions. + An element can be None, if the infoset contains the player's first possible move. + An empty list is returned if the information set is unreachable. """ infoset = self._resolve_infoset(infoset, "get_own_prior_actions") - own_prior_actions = [] - for action in self.game.deref().GetOwnPriorActions(cython.cast(Infoset, infoset).infoset): - own_prior_actions.append(Action.wrap(action)) + own_prior_actions = [ + None if not action else Action.wrap(action) + for action in self.game.deref().GetOwnPriorActions( + cython.cast(Infoset, infoset).infoset + ) + ] return own_prior_actions From 0ddde30a849ebc9e24f7e08cebadc076a366e96d Mon Sep 17 00:00:00 2001 From: drdkad Date: Wed, 12 Nov 2025 17:06:49 +0000 Subject: [PATCH 08/13] Update tests --- tests/test_extensive.py | 70 ++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 43 deletions(-) diff --git a/tests/test_extensive.py b/tests/test_extensive.py index e4bc89bd8..ceb53170f 100644 --- a/tests/test_extensive.py +++ b/tests/test_extensive.py @@ -55,63 +55,61 @@ def test_game_add_players_nolabel(): # ====================================================================== # Game 1: binary_3_levels_generic_payoffs.efg (Perfect Recall) # ====================================================================== - # Case: Player 1's root infoset. No prior actions. - ("binary_3_levels_generic_payoffs.efg", ("Player 1", 0), []), - # Case: Player 1's second infoset (L branch). Reached after P1's own - # action 'Left' (0) from their root infoset. + # Player 1's root infoset. Reachable + ("binary_3_levels_generic_payoffs.efg", ("Player 1", 0), [None]), + # Player 1's second infoset. Reached after P1's own action 0 ("1"). ( "binary_3_levels_generic_payoffs.efg", ("Player 1", 1), [("Player 1", 0, 0)], ), - # Case: Player 1's third infoset (R branch). Reached after P1's own - # action 'Right' (1) from their root infoset. + # Player 1's third infoset. Reached after P1's own action 1 ("2"). ( "binary_3_levels_generic_payoffs.efg", ("Player 1", 2), [("Player 1", 0, 1)], ), - # Case: Player 2's only infoset. Reached after P1's actions, so it has - # no *own* prior actions for Player 2. - ("binary_3_levels_generic_payoffs.efg", ("Player 2", 0), []), + # Player 2's only infoset. Reachable + ("binary_3_levels_generic_payoffs.efg", ("Player 2", 0), [None]), # ====================================================================== - # GAME 2: wichardt.efg (Imperfect Recall, all infosets reachable) + # Game 2: wichardt.efg (Imperfect Recall) # ====================================================================== - # Case: The root infoset for Player 1. Should have no prior actions. - ("wichardt.efg", ("Player 1", 0), []), - # Case: Player 1's second infoset. This is reached after P1's action 'L' - # from infoset 0, or P1's action 'R' from infoset 0. A key test case. + # The root infoset for Player 1. Reachable + ("wichardt.efg", ("Player 1", 0), [None]), + # Player 1's second infoset. It can be reached after either action 0 ("L") or 1 ("R"). ( "wichardt.efg", ("Player 1", 1), [("Player 1", 0, 0), ("Player 1", 0, 1)], ), - # Case: Player 2's only infoset. Has no *own* prior actions. - ("wichardt.efg", ("Player 2", 0), []), + # Player 2's only infoset. Reachable. + ("wichardt.efg", ("Player 2", 0), [None]), # ====================================================================== - # GAME 3: noPR-action-AM-two-hops.efg (Absent-Mindedness, one unreached) + # Game 3: noPR-action-AM-two-hops.efg (Absent-Mindedness) # ====================================================================== - # Case: Player 1's infoset 0. Reached via its own action '1' and - # P1's action '2' from infoset 1. + # Player 1's infoset 0. Has the property of Absent-Mindedness: + # Contains the root vertex and can be further reached via two different prior actions. ( "noPR-action-AM-two-hops.efg", ("Player 1", 0), - [("Player 1", 0, 0), ("Player 1", 1, 1)], + [None, ("Player 1", 0, 0), ("Player 1", 1, 1)], ), - # Case: Player 1's infoset 1. Reached via P1's action '1' from infoset 0. + # Player 1's infoset 1. Reached via a single prior action. ("noPR-action-AM-two-hops.efg", ("Player 1", 1), [("Player 1", 0, 0)]), - # Case: Player 2's infoset 0. Reached via P2's own action '1'. - ("noPR-action-AM-two-hops.efg", ("Player 2", 0), [("Player 2", 0, 0)]), + # Player 2's infoset 0. Reached via a single prior action. + ("noPR-action-AM-two-hops.efg", ("Player 2", 0), [None, ("Player 2", 0, 0)]), + # Player 2's infoset 1. This infoset is unreachable. + ("noPR-action-AM-two-hops.efg", ("Player 2", 1), []), ], ) -def test_get_own_prior_actions_return_values( +def test_get_own_prior_actions( game_file: str, infoset_specification: tuple[str, int], - expected_actions: list[tuple[str, int, str]], + expected_actions: list, ): """ - Verifies the get_own_prior_actions method returns correct actions for - a variety of reachable information sets across different games. + Verifies get_own_prior_actions returns correct sets of actions for various infosets: + root, perfect recall, imperfect recall, absent-minded, and unreachable cases. """ game = games.read_from_file(game_file) player_label, infoset_num = infoset_specification @@ -122,7 +120,7 @@ def test_get_own_prior_actions_return_values( result_actions = game.get_own_prior_actions(infoset) results = [ - ( + None if action is None else ( action.infoset.player.label, action.infoset.number, action.number, @@ -130,21 +128,7 @@ def test_get_own_prior_actions_return_values( for action in result_actions ] - assert set(results) == set(expected_actions) - - -def test_get_own_prior_actions_on_unreached_infoset_raises_error(): - """ - Verifies that calling get_own_prior_actions on an infoset that is - unreachable from the game's root correctly raises a RuntimeError. - """ - game = games.read_from_file("noPR-action-AM-two-hops.efg") - unreached_infoset = game.players["Player 2"].infosets[1] - - with pytest.raises( - RuntimeError, match="Cannot get prior actions for an unreached information set." - ): - game.get_own_prior_actions(unreached_infoset) + assert sorted(results, key=str) == sorted(expected_actions, key=str) @pytest.mark.parametrize("game_input,expected_result", [ From 9dd679c68e6102cb8d58ad4eb8ed1e9e7a5f225f Mon Sep 17 00:00:00 2001 From: drdkad Date: Wed, 12 Nov 2025 17:29:31 +0000 Subject: [PATCH 09/13] Fix a typo --- src/pygambit/game.pxi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 49a662847..cfe443177 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -790,7 +790,7 @@ class Game: ------- list of Action or None A list of the preceding actions. - An element can be None, if the infoset contains the player's first possible move. + An element in the list can be None, if the infoset contains a first move of the player. An empty list is returned if the information set is unreachable. """ infoset = self._resolve_infoset(infoset, "get_own_prior_actions") From 59c64cb05a470111100b730bc3bf2ea12cd7a6d9 Mon Sep 17 00:00:00 2001 From: Ted Turocy Date: Thu, 13 Nov 2025 11:52:44 +0000 Subject: [PATCH 10/13] Update ChangeLog --- ChangeLog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index e11de3766..55304142f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -5,7 +5,7 @@ ### Added - Implement `GameTreeRep::GetOwnPriorActions` (C++) and `Game.get_own_prior_actions` (Python) to compute, for a given information set, the set of last actions taken by the player acting - in the information set before reaching it + in the information set before reaching it. (#582) ### Changed - In the graphical interface, removed option to configure information set link drawing; information sets From bbf9e2b274dfc615a5fb2470a0cdbe7c9a6bf488 Mon Sep 17 00:00:00 2001 From: Ted Turocy Date: Thu, 13 Nov 2025 12:01:53 +0000 Subject: [PATCH 11/13] Update game.pxi --- src/pygambit/game.pxi | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index cfe443177..8200212b0 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -781,29 +781,30 @@ class Game: self, infoset: typing.Union[Infoset, str] ) -> typing.List[typing.Optional[Action]]: - """ For a given infoset and the player active in it, - find the set of last action(s) the player took along the paths reaching the infoset. + """Return the list of actions which immediately precede `infoset` in the graph of + the player's information set. + + An "own prior action" is an action such that, for a given member node of the information + set, it is the action most recently played by the player on the path to that node. + If there is a member node where there is no such action, that is, the player has not + yet played prior to reaching that node, the own prior action is null, which is represented + by `None` in the list of actions returned. .. versionadded:: 16.5 Returns ------- - list of Action or None - A list of the preceding actions. - An element in the list can be None, if the infoset contains a first move of the player. - An empty list is returned if the information set is unreachable. + list of {Action, None} + The list of the prior actions. """ infoset = self._resolve_infoset(infoset, "get_own_prior_actions") - - own_prior_actions = [ + return [ None if not action else Action.wrap(action) for action in self.game.deref().GetOwnPriorActions( cython.cast(Infoset, infoset).infoset ) ] - return own_prior_actions - @property def is_perfect_recall(self) -> bool: """Whether the game is perfect recall. From 4c5cc3721838957c20c3c214beb34bde7c74b25e Mon Sep 17 00:00:00 2001 From: Ted Turocy Date: Thu, 13 Nov 2025 12:03:52 +0000 Subject: [PATCH 12/13] Update gametree.cc --- src/games/gametree.cc | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/games/gametree.cc b/src/games/gametree.cc index f0f4548e2..02c683153 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -751,14 +751,8 @@ std::vector GameTreeRep::GetOwnPriorActions(GameInfoset infoset) con std::vector own_prior_actions; for (auto action_ptr : it->second) { - if (action_ptr) { - own_prior_actions.emplace_back(action_ptr->shared_from_this()); - } - else { - own_prior_actions.emplace_back(nullptr); - } + own_prior_actions.emplace_back((action_ptr) ? action_ptr->shared_from_this() : nullptr); } - return own_prior_actions; } From dbdbef2b5793679c7e7dad945433e08540151e67 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Thu, 13 Nov 2025 12:16:48 +0000 Subject: [PATCH 13/13] Adjust docstring further. --- src/pygambit/game.pxi | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 8200212b0..dd1d6bb8f 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -790,6 +790,9 @@ class Game: yet played prior to reaching that node, the own prior action is null, which is represented by `None` in the list of actions returned. + If a member node is not reachable due to the path to the node passing through an + absent-minded information set, that node has no own prior action. + .. versionadded:: 16.5 Returns