From 1b05b3a44153d4b4a72bf05ac5f433312b28d25d Mon Sep 17 00:00:00 2001 From: Ted Turocy Date: Tue, 28 Jan 2025 16:03:43 +0000 Subject: [PATCH 1/2] Expose access to action map for a strategy This provides a crude interface to the mapping from infoset to action as part of a strategy for an extensive game. A function `.action()` has been added in Python as well. This is not a final polished interface for this information, just a starter to enable work on issue 440. --- src/games/game.cc | 9 +++++++++ src/games/game.h | 3 +++ src/pygambit/gambit.pxd | 1 + src/pygambit/strategy.pxi | 3 +++ 4 files changed, 16 insertions(+) diff --git a/src/games/game.cc b/src/games/game.cc index da71247b0..7ea040d5d 100644 --- a/src/games/game.cc +++ b/src/games/game.cc @@ -50,6 +50,15 @@ GameOutcomeRep::GameOutcomeRep(GameRep *p_game, int p_number) // class GameStrategyRep //======================================================================== +GameAction GameStrategyRep::GetAction(const GameInfoset &p_infoset) const +{ + if (p_infoset->GetPlayer() != m_player) { + throw MismatchException(); + } + int action = m_behav[p_infoset->GetNumber()]; + return (action) ? p_infoset->GetActions()[action] : nullptr; +} + void GameStrategyRep::DeleteStrategy() { if (m_player->GetGame()->IsTree()) { diff --git a/src/games/game.h b/src/games/game.h index d819c3e51..ce3df9b50 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -265,6 +265,9 @@ class GameStrategyRep : public GameObject { /// Returns the index of the strategy for its player int GetNumber() const { return m_number; } + /// Returns the action specified by the strategy at the information set + GameAction GetAction(const GameInfoset &) const; + /// Remove this strategy from the game void DeleteStrategy(); //@} diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index 02d7f5947..cfa61c3db 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -95,6 +95,7 @@ cdef extern from "games/game.h": c_GamePlayer GetPlayer() except + string GetLabel() except + void SetLabel(string) except + + c_GameAction GetAction(c_GameInfoset) except + void DeleteStrategy() except + cdef cppclass c_GameActionRep "GameActionRep": diff --git a/src/pygambit/strategy.pxi b/src/pygambit/strategy.pxi index 2cef0aaaa..b228efbde 100644 --- a/src/pygambit/strategy.pxi +++ b/src/pygambit/strategy.pxi @@ -73,3 +73,6 @@ class Strategy: def number(self) -> int: """The number of the strategy.""" return self.strategy.deref().GetNumber() - 1 + + def action(self, infoset: Infoset) -> Action: + return Action.wrap(self.strategy.deref().GetAction(infoset.infoset)) From f09a849c66c9529b643c3ab5b7d564d610365738 Mon Sep 17 00:00:00 2001 From: drdkad Date: Tue, 4 Mar 2025 18:19:43 +0000 Subject: [PATCH 2/2] Implement compute_images method of the Game class --- src/pygambit/game.pxi | 36 +++++++++++++++++++++++++++ tests/test_reduced_strategic_forms.py | 18 ++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 tests/test_reduced_strategic_forms.py diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 98d84638d..0c6a8d26c 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -19,6 +19,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # +from collections import deque import io import itertools import pathlib @@ -1947,3 +1948,38 @@ class Game: if len(resolved_strategy.player.strategies) == 1: raise UndefinedOperationError("Cannot delete the only strategy for a player") resolved_strategy.strategy.deref().DeleteStrategy() + + def compute_images(self) -> dict[Node, set[Outcome]]: + """Recursively compute images in outcomes for each node in the game tree. + + For each node in the tree, calculates the set of outcomes that can be reached from it. + + Returns + ------- + dict[Node, set[Outcome]] + A dictionary mapping each node to the set of outcomes that can be reached from it: + - For terminal nodes, this is either {node.outcome} or an empty set + - For non-terminal nodes, this is the union of all images in outcomes of children + + Notes + ----- + This traverses the game tree using depth-first search, building the sets of outcomes + from the bottom up. + """ + images_in_outcomes = {} + + def dfs(node) -> set[Outcome]: + if node.is_terminal: + if node.outcome is None: + images_in_outcomes[node] = set() + else: + images_in_outcomes[node] = {node.outcome} + else: + union = set() + for child in node.children: + union |= dfs(child) + images_in_outcomes[node] = union + return images_in_outcomes[node] + + dfs(self.root) + return images_in_outcomes diff --git a/tests/test_reduced_strategic_forms.py b/tests/test_reduced_strategic_forms.py new file mode 100644 index 000000000..8a0deb49c --- /dev/null +++ b/tests/test_reduced_strategic_forms.py @@ -0,0 +1,18 @@ +import pygambit as gbt + + +def test_build_images_of_nodes_in_outcomes(): + """Generate images in outcome of the nodes of a simple centipede game with 4 terminal nodes + """ + g = gbt.read_efg("tests/test_games/e02.efg") + z1, z2, z3, z4 = g.outcomes + expected_images = { + g.root: {z1, z2, z3, z4}, + g.root.children[0]: {z1}, + g.root.children[1]: {z2, z3, z4}, + g.root.children[1].children[0]: {z2}, + g.root.children[1].children[1]: {z3, z4}, + g.root.children[1].children[1].children[0]: {z3}, + g.root.children[1].children[1].children[1]: {z4} + } + assert expected_images == g.compute_images()