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/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} 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/game.pxi b/src/pygambit/game.pxi index 98d84638d..d90aea628 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 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/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)) 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