From 1b05b3a44153d4b4a72bf05ac5f433312b28d25d Mon Sep 17 00:00:00 2001 From: Ted Turocy Date: Tue, 28 Jan 2025 16:03:43 +0000 Subject: [PATCH 1/4] 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/4] 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() From 9f957a4c6f5272ea95af5c7250dc2efaa00d27f7 Mon Sep 17 00:00:00 2001 From: drdkad Date: Sat, 8 Mar 2025 11:18:08 +0000 Subject: [PATCH 3/4] Add Action.members property to access nodes constituting a given action --- src/pygambit/action.pxi | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pygambit/action.pxi b/src/pygambit/action.pxi index 14b9c29a1..34ab373ab 100644 --- a/src/pygambit/action.pxi +++ b/src/pygambit/action.pxi @@ -106,3 +106,8 @@ class Action: return decimal.Decimal(py_string.decode("ascii")) else: return Rational(py_string.decode("ascii")) + + @property + def members(self) -> set[Node]: + """Get the set of nodes resulting from taking this action.""" + return {node.children[self.number] for node in self.infoset.members} From 84312d81b9b9efdbc1b74af101f8c6b815e7016f Mon Sep 17 00:00:00 2001 From: drdkad Date: Fri, 21 Mar 2025 10:56:18 +0000 Subject: [PATCH 4/4] add generation of images in outcomes of (i) nodes, (ii) infosets, (iii) actions --- src/pygambit/game.pxi | 35 ----------------- src/pygambit/image.py | 54 +++++++++++++++++++++++++++ tests/test_images.py | 54 +++++++++++++++++++++++++++ tests/test_reduced_strategic_forms.py | 18 --------- 4 files changed, 108 insertions(+), 53 deletions(-) create mode 100644 src/pygambit/image.py create mode 100644 tests/test_images.py delete mode 100644 tests/test_reduced_strategic_forms.py diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 0c6a8d26c..d90aea628 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -1948,38 +1948,3 @@ 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/src/pygambit/image.py b/src/pygambit/image.py new file mode 100644 index 000000000..2eda65e63 --- /dev/null +++ b/src/pygambit/image.py @@ -0,0 +1,54 @@ +import typing + +from pygambit import Action, Game, Infoset, Node, Outcome + +ImageDict = dict[Node, set[Outcome]] + + +def build_node_images(game: Game) -> ImageDict: + """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. + """ + node_images = {} + + def dfs(node) -> set[Outcome]: + if node.is_terminal: + if node.outcome is None: + node_images[node] = set() + else: + node_images[node] = {node.outcome} + else: + union = set() + for child in node.children: + union |= dfs(child) + node_images[node] = union + return node_images[node] + + dfs(game.root) + return node_images + + +def build_set_image( + infoset_or_action: typing.Union[Infoset, Action], + node_images: ImageDict) -> set[Outcome]: + # Get the set of nodes constituting a given infoset or action + image_in_nodes = infoset_or_action.members + + # Calculate the union of images of the member nodes + union = set() + for node in image_in_nodes: + union |= node_images[node] + return union diff --git a/tests/test_images.py b/tests/test_images.py new file mode 100644 index 000000000..1e900cab8 --- /dev/null +++ b/tests/test_images.py @@ -0,0 +1,54 @@ +import pygambit as gbt +from pygambit.image import build_node_images, build_set_image + + +def test_build_images_of_nodes_in_outcomes(): + """Generate images in outcomes 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 == build_node_images(g) + + +def test_build_images_of_infosets_in_outcomes(): + """Generate images in outcomes of the infosets of a simple centipede game with 4 terminal nodes + """ + g = gbt.read_efg("tests/test_games/e02.efg") + node_images = build_node_images(g) + z1, z2, z3, z4 = g.outcomes + infosets = g.infosets + expected_images = { + infosets[0]: {z1, z2, z3, z4}, + infosets[1]: {z3, z4}, + infosets[2]: {z2, z3, z4} + } + built_images = {infoset: build_set_image(infoset, node_images) for infoset in infosets} + assert expected_images == built_images + + +def test_build_images_of_actions_in_outcomes(): + """Generate images in outcomes of the actions of a simple centipede game with 4 terminal nodes + """ + g = gbt.read_efg("tests/test_games/e02.efg") + node_images = build_node_images(g) + z1, z2, z3, z4 = g.outcomes + actions = g.actions + expected_images = { + actions[0]: {z1}, + actions[1]: {z2, z3, z4}, + actions[2]: {z3}, + actions[3]: {z4}, + actions[4]: {z2}, + actions[5]: {z3, z4} + } + built_images = {action: build_set_image(action, node_images) for action in g.actions} + assert expected_images == built_images diff --git a/tests/test_reduced_strategic_forms.py b/tests/test_reduced_strategic_forms.py deleted file mode 100644 index 8a0deb49c..000000000 --- a/tests/test_reduced_strategic_forms.py +++ /dev/null @@ -1,18 +0,0 @@ -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()