From 03fad7ef88ad02578c2c080ae329e103135219fc Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 7 Nov 2025 16:28:28 +0000 Subject: [PATCH 1/7] Change __getitem__ on Node.children to index actions not children. It is perhaps telling that this was not a very useful way to index children, but this change did not break any tests! --- src/pygambit/node.pxi | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pygambit/node.pxi b/src/pygambit/node.pxi index 4124406b0..c5c685982 100644 --- a/src/pygambit/node.pxi +++ b/src/pygambit/node.pxi @@ -48,13 +48,13 @@ class NodeChildren: def __getitem__(self, index: typing.Union[int, str]) -> Node: if isinstance(index, str): if not index.strip(): - raise ValueError("Node label cannot be empty or all whitespace") - matches = [x for x in self if x.label == index.strip()] - if not matches: - raise KeyError(f"Node has no child with label '{index}'") - if len(matches) > 1: - raise ValueError(f"Node has multiple children with label '{index}'") - return matches[0] + raise ValueError("Action label cannot be empty or all whitespace") + if self.parent.deref().GetInfoset() == cython.cast(c_GameInfoset, NULL): + raise ValueError(f"No action with label '{index}' at node") + for action in self.parent.deref().GetInfoset().deref().GetActions(): + if action.deref().GetLabel() == cython.cast(str, index): + return Node.wrap(self.parent.deref().GetChild(action)) + raise ValueError(f"No action with label '{index}' at node") if isinstance(index, int): if self.parent.deref().GetInfoset() == cython.cast(c_GameInfoset, NULL): raise IndexError("Index out of range") From e13b02d895de3edd82ccc8fbfae03b6f9b95263b Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 7 Nov 2025 16:43:41 +0000 Subject: [PATCH 2/7] Add operator __add__ to node, accepting an action, and tests for same. --- src/pygambit/node.pxi | 18 ++++++++++++++++-- tests/test_node.py | 24 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/pygambit/node.pxi b/src/pygambit/node.pxi index c5c685982..0406e0cad 100644 --- a/src/pygambit/node.pxi +++ b/src/pygambit/node.pxi @@ -52,7 +52,7 @@ class NodeChildren: if self.parent.deref().GetInfoset() == cython.cast(c_GameInfoset, NULL): raise ValueError(f"No action with label '{index}' at node") for action in self.parent.deref().GetInfoset().deref().GetActions(): - if action.deref().GetLabel() == cython.cast(str, index): + if action.deref().GetLabel().decode("ascii") == cython.cast(str, index): return Node.wrap(self.parent.deref().GetChild(action)) raise ValueError(f"No action with label '{index}' at node") if isinstance(index, int): @@ -61,7 +61,7 @@ class NodeChildren: return Node.wrap(self.parent.deref().GetChild( self.parent.deref().GetInfoset().deref().GetAction(index + 1) )) - raise TypeError(f"Child index must be int or str, not {index.__class__.__name__}") + raise TypeError(f"Action index must be int or str, not {index.__class__.__name__}") @cython.cclass @@ -213,3 +213,17 @@ class 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)] + + def __add__(self, action: str) -> Node: + """Return the child of the node which succeeds this node after `action` is played) + + .. versionadded:: 16.5.0 + """ + if not action.strip(): + raise ValueError("Action label cannot be empty or all whitespace") + if self.node.deref().GetInfoset() == cython.cast(c_GameInfoset, NULL): + raise ValueError(f"No action with label '{action}' at node") + for act in self.node.deref().GetInfoset().deref().GetActions(): + if act.deref().GetLabel().decode("ascii") == cython.cast(str, action): + return Node.wrap(self.node.deref().GetChild(act)) + raise ValueError(f"No action with label '{action}' at node") diff --git a/tests/test_node.py b/tests/test_node.py index 4104e0e64..cf58eeb42 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -790,6 +790,30 @@ def test_node_plays(): assert set(test_node.plays) == expected_set_of_plays +def test_node_children_action_label(): + game = games.read_from_file("poker.efg") + assert game.root.children["Red"] == game.root.children[0] + assert game.root.children["Black"].children["Fold"] == game.root.children[1].children[1] + + +def test_node_children_nonexistent_action(): + game = games.read_from_file("poker.efg") + with pytest.raises(ValueError): + _ = game.root.children["Green"] + + +def test_node_plus_action(): + game = games.read_from_file("poker.efg") + assert game.root + "Red" == game.root.children["Red"] + assert game.root + "Black" + "Fold" == game.root.children["Black"].children["Fold"] + + +def test_node_plus_nonexistent_action(): + game = games.read_from_file("poker.efg") + with pytest.raises(ValueError): + _ = game.root + "Green" + + @pytest.mark.parametrize( "game_obj", [ From 4c2604b69146214e83e0016a2234628c29e249bf Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Fri, 7 Nov 2025 17:04:15 +0000 Subject: [PATCH 3/7] Also allow action as RHS of + operator, indexing of children. --- src/games/game.h | 3 ++- src/pygambit/node.pxi | 44 ++++++++++++++++++++++++------------------- tests/test_node.py | 17 +++++++++++++++++ 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/games/game.h b/src/games/game.h index 529dbc507..53cea45f8 100644 --- a/src/games/game.h +++ b/src/games/game.h @@ -82,6 +82,7 @@ class UndefinedException final : public std::runtime_error { class MismatchException : public std::runtime_error { public: MismatchException() : std::runtime_error("Operation between objects in different games") {} + explicit MismatchException(const std::string &s) : std::runtime_error(s) {} ~MismatchException() noexcept override = default; }; @@ -444,7 +445,7 @@ class GameNodeRep : public std::enable_shared_from_this { GameNode GetChild(const GameAction &p_action) { if (p_action->GetInfoset().get() != m_infoset) { - throw MismatchException(); + throw MismatchException("Action is from a different information set than node"); } return m_children.at(p_action->GetNumber() - 1); } diff --git a/src/pygambit/node.pxi b/src/pygambit/node.pxi index 0406e0cad..f0281b257 100644 --- a/src/pygambit/node.pxi +++ b/src/pygambit/node.pxi @@ -45,23 +45,16 @@ class NodeChildren: for child in self.parent.deref().GetChildren(): yield Node.wrap(child) - def __getitem__(self, index: typing.Union[int, str]) -> Node: - if isinstance(index, str): - if not index.strip(): - raise ValueError("Action label cannot be empty or all whitespace") - if self.parent.deref().GetInfoset() == cython.cast(c_GameInfoset, NULL): - raise ValueError(f"No action with label '{index}' at node") - for action in self.parent.deref().GetInfoset().deref().GetActions(): - if action.deref().GetLabel().decode("ascii") == cython.cast(str, index): - return Node.wrap(self.parent.deref().GetChild(action)) - raise ValueError(f"No action with label '{index}' at node") + def __getitem__(self, index: typing.Union[int, str, Action]) -> Node: + if isinstance(index, (Action, str)): + return Node.wrap(self.parent) + index if isinstance(index, int): if self.parent.deref().GetInfoset() == cython.cast(c_GameInfoset, NULL): raise IndexError("Index out of range") return Node.wrap(self.parent.deref().GetChild( self.parent.deref().GetInfoset().deref().GetAction(index + 1) )) - raise TypeError(f"Action index must be int or str, not {index.__class__.__name__}") + raise TypeError(f"Action must be int, str, or Action, not {index.__class__.__name__}") @cython.cclass @@ -214,16 +207,29 @@ class Node: """ return [Node.wrap(n) for n in self.node.deref().GetGame().deref().GetPlays(self.node)] - def __add__(self, action: str) -> Node: + def __add__(self, action: typing.Union[str, Action]) -> Node: """Return the child of the node which succeeds this node after `action` is played) + Raises + ------ + ValueError + If 'action' does not specify an action label at the information set, or if + 'action' is an action not from the node's information set + .. versionadded:: 16.5.0 """ - if not action.strip(): - raise ValueError("Action label cannot be empty or all whitespace") - if self.node.deref().GetInfoset() == cython.cast(c_GameInfoset, NULL): + if isinstance(action, str): + if not action.strip(): + raise ValueError("Action label cannot be empty or all whitespace") + if self.node.deref().GetInfoset() == cython.cast(c_GameInfoset, NULL): + raise ValueError(f"No action with label '{action}' at node") + for act in self.node.deref().GetInfoset().deref().GetActions(): + if act.deref().GetLabel().decode("ascii") == cython.cast(str, action): + return Node.wrap(self.node.deref().GetChild(act)) raise ValueError(f"No action with label '{action}' at node") - for act in self.node.deref().GetInfoset().deref().GetActions(): - if act.deref().GetLabel().decode("ascii") == cython.cast(str, action): - return Node.wrap(self.node.deref().GetChild(act)) - raise ValueError(f"No action with label '{action}' at node") + if isinstance(action, Action): + try: + return Node.wrap(self.node.deref().GetChild(cython.cast(Action, action).action)) + except IndexError: + raise ValueError(f"Action is from a different information set than node") + raise TypeError(f"Action must be str or Action, not {action.__class__.__name__}") diff --git a/tests/test_node.py b/tests/test_node.py index cf58eeb42..f8212df94 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -796,12 +796,23 @@ def test_node_children_action_label(): assert game.root.children["Black"].children["Fold"] == game.root.children[1].children[1] +def test_node_children_action(): + game = games.read_from_file("poker.efg") + assert game.root.children[game.root.infoset.actions["Red"]] == game.root.children[0] + + def test_node_children_nonexistent_action(): game = games.read_from_file("poker.efg") with pytest.raises(ValueError): _ = game.root.children["Green"] +def test_node_children_other_infoset_action(): + game = games.read_from_file("poker.efg") + with pytest.raises(ValueError): + _ = game.root.children[game.root.children[0].infoset.actions["Raise"]] + + def test_node_plus_action(): game = games.read_from_file("poker.efg") assert game.root + "Red" == game.root.children["Red"] @@ -814,6 +825,12 @@ def test_node_plus_nonexistent_action(): _ = game.root + "Green" +def test_node_plus_other_infoset_action(): + game = games.read_from_file("poker.efg") + with pytest.raises(ValueError): + _ = game.root + game.root.children["Black"].infoset.actions[0] + + @pytest.mark.parametrize( "game_obj", [ From 836cd495ab05adcfba9dd7f96dbe3127a47ca486 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Tue, 11 Nov 2025 09:49:41 +0000 Subject: [PATCH 4/7] used new + operator in construction of Kuhn poker in tests --- tests/games.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/tests/games.py b/tests/games.py index 5997924bd..7017f35a0 100644 --- a/tests/games.py +++ b/tests/games.py @@ -146,28 +146,30 @@ def create_kuhn_poker_efg() -> gbt.Game: g = gbt.Game.new_tree( players=["Alice", "Bob"], title="Three-card poker (J, Q, K), two-player" ) + cards = ["J", "Q", "K"] deals = ["JQ", "JK", "QJ", "QK", "KJ", "KQ"] + + def deals_by_infoset(player, card): + player_idx = 0 if player == "Alice" else 1 + return [d for d in deals if d[player_idx] == card] + g.append_move(g.root, g.players.chance, deals) g.set_chance_probs(g.root.infoset, [gbt.Rational(1, 6)]*6) - # group the children of the root (indices of `deals`) by each player's dealt card - alice_grouping = [[0, 1], [2, 3], [4, 5]] # J, Q, K - bob_grouping = [[0, 5], [1, 3], [2, 4]] # Q, K, J - - # Alice's first move - for ij in alice_grouping: - term_nodes = [g.root.children[k] for k in ij] + for alice_card in cards: + # Alice's first move + term_nodes = [g.root + d for d in deals_by_infoset("Alice", alice_card)] g.append_move(term_nodes, "Alice", ["Check", "Bet"]) - # Bob's move after Alice checks - for ij in bob_grouping: - term_nodes = [g.root.children[k].children[0] for k in ij] + for bob_card in cards: + # Bob's move after Alice checks + term_nodes = [g.root + d + "Check" for d in deals_by_infoset("Bob", bob_card)] g.append_move(term_nodes, "Bob", ["Check", "Bet"]) - # Alice's move if Bob's second action is bet - for ij in alice_grouping: - term_nodes = [g.root.children[k].children[0].children[1] for k in ij] + for alice_card in cards: + # Alice's move if Bob's second action is bet + term_nodes = [g.root + d + "Check" + "Bet" for d in deals_by_infoset("Alice", alice_card)] g.append_move(term_nodes, "Alice", ["Fold", "Call"]) - # Bob's move after Alice bets initially - for ij in bob_grouping: - term_nodes = [g.root.children[k].children[1] for k in ij] + for bob_card in cards: + # Bob's move after Alice bets initially + term_nodes = [g.root + d + "Bet" for d in deals_by_infoset("Bob", bob_card)] g.append_move(term_nodes, "Bob", ["Fold", "Call"]) def calculate_payoffs(term_node): From d406134985f8b6666c6b92905c9ea41f7250afb2 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 12 Nov 2025 16:33:00 +0000 Subject: [PATCH 5/7] Remove operator + for child of a node, pending further discussion. --- src/pygambit/node.pxi | 60 +++++++++++++++++++------------------------ tests/games.py | 11 +++++--- tests/test_node.py | 18 ------------- 3 files changed, 34 insertions(+), 55 deletions(-) diff --git a/src/pygambit/node.pxi b/src/pygambit/node.pxi index f0281b257..d13be56b6 100644 --- a/src/pygambit/node.pxi +++ b/src/pygambit/node.pxi @@ -45,16 +45,37 @@ class NodeChildren: for child in self.parent.deref().GetChildren(): yield Node.wrap(child) - def __getitem__(self, index: typing.Union[int, str, Action]) -> Node: - if isinstance(index, (Action, str)): - return Node.wrap(self.parent) + index - if isinstance(index, int): + def __getitem__(self, action: typing.Union[int, str, Action]) -> Node: + """Returns the successor node which is reached after 'action' is played. + + .. versionchanged: 16.5.0 + Previously indexing by string searched the labels of the child nodes, + rather than referring to actions. This implements the more natural + interpretation that strings refer to action labels. + + Relatedly, the collection can now be indexed by an Action object. + """ + if isinstance(action, str): + if not action.strip(): + raise ValueError("Action label cannot be empty or all whitespace") + if self.parent.deref().GetInfoset() == cython.cast(c_GameInfoset, NULL): + raise ValueError(f"No action with label '{action}' at node") + for act in self.parent.deref().GetInfoset().deref().GetActions(): + if act.deref().GetLabel().decode("ascii") == cython.cast(str, action): + return Node.wrap(self.parent.deref().GetChild(act)) + raise ValueError(f"No action with label '{action}' at node") + if isinstance(action, Action): + try: + return Node.wrap(self.parent.deref().GetChild(cython.cast(Action, action).action)) + except IndexError: + raise ValueError(f"Action is from a different information set than node") + if isinstance(action, int): if self.parent.deref().GetInfoset() == cython.cast(c_GameInfoset, NULL): raise IndexError("Index out of range") return Node.wrap(self.parent.deref().GetChild( - self.parent.deref().GetInfoset().deref().GetAction(index + 1) + self.parent.deref().GetInfoset().deref().GetAction(action + 1) )) - raise TypeError(f"Action must be int, str, or Action, not {index.__class__.__name__}") + raise TypeError(f"Index must be int, str, or Action, not {action.__class__.__name__}") @cython.cclass @@ -206,30 +227,3 @@ class 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)] - - def __add__(self, action: typing.Union[str, Action]) -> Node: - """Return the child of the node which succeeds this node after `action` is played) - - Raises - ------ - ValueError - If 'action' does not specify an action label at the information set, or if - 'action' is an action not from the node's information set - - .. versionadded:: 16.5.0 - """ - if isinstance(action, str): - if not action.strip(): - raise ValueError("Action label cannot be empty or all whitespace") - if self.node.deref().GetInfoset() == cython.cast(c_GameInfoset, NULL): - raise ValueError(f"No action with label '{action}' at node") - for act in self.node.deref().GetInfoset().deref().GetActions(): - if act.deref().GetLabel().decode("ascii") == cython.cast(str, action): - return Node.wrap(self.node.deref().GetChild(act)) - raise ValueError(f"No action with label '{action}' at node") - if isinstance(action, Action): - try: - return Node.wrap(self.node.deref().GetChild(cython.cast(Action, action).action)) - except IndexError: - raise ValueError(f"Action is from a different information set than node") - raise TypeError(f"Action must be str or Action, not {action.__class__.__name__}") diff --git a/tests/games.py b/tests/games.py index 331399223..f428edb17 100644 --- a/tests/games.py +++ b/tests/games.py @@ -157,19 +157,22 @@ def deals_by_infoset(player, card): g.set_chance_probs(g.root.infoset, [gbt.Rational(1, 6)]*6) for alice_card in cards: # Alice's first move - term_nodes = [g.root + d for d in deals_by_infoset("Alice", alice_card)] + term_nodes = [g.root.children[d] for d in deals_by_infoset("Alice", alice_card)] g.append_move(term_nodes, "Alice", ["Check", "Bet"]) for bob_card in cards: # Bob's move after Alice checks - term_nodes = [g.root + d + "Check" for d in deals_by_infoset("Bob", bob_card)] + term_nodes = [g.root.children[d].children["Check"] + for d in deals_by_infoset("Bob", bob_card)] g.append_move(term_nodes, "Bob", ["Check", "Bet"]) for alice_card in cards: # Alice's move if Bob's second action is bet - term_nodes = [g.root + d + "Check" + "Bet" for d in deals_by_infoset("Alice", alice_card)] + term_nodes = [g.root.children[d].children["Check"].children["Bet"] + for d in deals_by_infoset("Alice", alice_card)] g.append_move(term_nodes, "Alice", ["Fold", "Call"]) for bob_card in cards: # Bob's move after Alice bets initially - term_nodes = [g.root + d + "Bet" for d in deals_by_infoset("Bob", bob_card)] + term_nodes = [g.root.children[d].children["Bet"] + for d in deals_by_infoset("Bob", bob_card)] g.append_move(term_nodes, "Bob", ["Fold", "Call"]) def calculate_payoffs(term_node): diff --git a/tests/test_node.py b/tests/test_node.py index f8212df94..8636a7559 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -813,24 +813,6 @@ def test_node_children_other_infoset_action(): _ = game.root.children[game.root.children[0].infoset.actions["Raise"]] -def test_node_plus_action(): - game = games.read_from_file("poker.efg") - assert game.root + "Red" == game.root.children["Red"] - assert game.root + "Black" + "Fold" == game.root.children["Black"].children["Fold"] - - -def test_node_plus_nonexistent_action(): - game = games.read_from_file("poker.efg") - with pytest.raises(ValueError): - _ = game.root + "Green" - - -def test_node_plus_other_infoset_action(): - game = games.read_from_file("poker.efg") - with pytest.raises(ValueError): - _ = game.root + game.root.children["Black"].infoset.actions[0] - - @pytest.mark.parametrize( "game_obj", [ From 793c5bc323af1c02d5ffcb8af22d3bfd14391830 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 12 Nov 2025 16:39:34 +0000 Subject: [PATCH 6/7] Update game-building tutorials --- doc/pygambit.user.rst | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/doc/pygambit.user.rst b/doc/pygambit.user.rst index 293ff3565..503f07ce3 100644 --- a/doc/pygambit.user.rst +++ b/doc/pygambit.user.rst @@ -48,7 +48,7 @@ We can then also add the Seller's move in the situation after the Buyer chooses .. ipython:: python - g.append_move(g.root.children[0], "Seller", ["Honor", "Abuse"]) + g.append_move(g.root.children["Trust"], "Seller", ["Honor", "Abuse"]) Now that we have the moves of the game defined, we add payoffs. Payoffs are associated with an :py:class:`.Outcome`; each :py:class:`Outcome` has a vector of payoffs, one for each player, @@ -57,20 +57,20 @@ Seller proving themselves trustworthy: .. ipython:: python - g.set_outcome(g.root.children[0].children[0], g.add_outcome([1, 1], label="Trustworthy")) + g.set_outcome(g.root.children["Trust"].children["Honor"], g.add_outcome([1, 1], label="Trustworthy")) Next, the outcome associated with the scenario where the Buyer trusts but the Seller does not return the trust: .. ipython:: python - g.set_outcome(g.root.children[0].children[1], g.add_outcome([-1, 2], label="Untrustworthy")) + g.set_outcome(g.root.children["Trust"].children["Abuse"], g.add_outcome([-1, 2], label="Untrustworthy")) And, finally the outcome associated with the Buyer opting out of the interaction: .. ipython:: python - g.set_outcome(g.root.children[1], g.add_outcome([0, 0], label="Opt-out")) + g.set_outcome(g.root.children["Not trust"], g.add_outcome([0, 0], label="Opt-out")) Nodes without an outcome attached are assumed to have payoffs of zero for all players. Therefore, adding the outcome to this latter terminal node is not strictly necessary in Gambit, @@ -111,19 +111,19 @@ We can build this game using the following script:: g.append_move(g.root, g.players.chance, ["King", "Queen"]) for node in g.root.children: g.append_move(node, "Alice", ["Raise", "Fold"]) - g.append_move(g.root.children[0].children[0], "Bob", ["Meet", "Pass"]) - g.append_infoset(g.root.children[1].children[0], - g.root.children[0].children[0].infoset) + g.append_move(g.root.children["King"].children["Raise"], "Bob", ["Meet", "Pass"]) + g.append_infoset(g.root.children["Queen"].children["Raise"], + g.root.children["King"].children["Raise"].infoset) alice_winsbig = g.add_outcome([2, -2], label="Alice wins big") alice_wins = g.add_outcome([1, -1], label="Alice wins") bob_winsbig = g.add_outcome([-2, 2], label="Bob wins big") bob_wins = g.add_outcome([-1, 1], label="Bob wins") - g.set_outcome(g.root.children[0].children[0].children[0], alice_winsbig) - g.set_outcome(g.root.children[0].children[0].children[1], alice_wins) - g.set_outcome(g.root.children[0].children[1], bob_wins) - g.set_outcome(g.root.children[1].children[0].children[0], bob_winsbig) - g.set_outcome(g.root.children[1].children[0].children[1], alice_wins) - g.set_outcome(g.root.children[1].children[1], bob_wins) + g.set_outcome(g.root.children["King"].children["Raise"].children["Meet"], alice_winsbig) + g.set_outcome(g.root.children["King"].children["Raise"].children["Pass"], alice_wins) + g.set_outcome(g.root.children["King"].children["Fold"], bob_wins) + g.set_outcome(g.root.children["Queen"].children["Raise"].children["Meet"], bob_winsbig) + g.set_outcome(g.root.children["Queen"].children["Raise"].children["Pass"], alice_wins) + g.set_outcome(g.root.children["Queen"].children["Fold"], bob_wins) All extensive games have a chance (or nature) player, accessible as ``.Game.players.chance``. Moves belonging to the chance player can be added in the same @@ -140,9 +140,9 @@ causes each of the newly-appended moves to be in new information sets. In contr does not know Alice's card, and therefore cannot distinguish between the two nodes at which he has the decision. This is implemented in the following lines:: - g.append_move(g.root.children[0].children[0], "Bob", ["Meet", "Pass"]) - g.append_infoset(g.root.children[1].children[0], - g.root.children[0].children[0].infoset) + g.append_move(g.root.children["King"].children["Raise"], "Bob", ["Meet", "Pass"]) + g.append_infoset(g.root.children["Queen"].children["Raise"], + g.root.children["King"].children["Raise"].infoset) The call :py:meth:`.Game.append_infoset` adds a move at a terminal node as part of an existing information set (represented in ``pygambit`` as an :py:class:`.Infoset`). From 1bcd5baff67fbd02f396321d4dfbd65f04430243 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Wed, 12 Nov 2025 16:45:14 +0000 Subject: [PATCH 7/7] Add ChangeLog entry. Closes #587. --- ChangeLog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog b/ChangeLog index c1066f9fa..73cda989a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -5,6 +5,8 @@ ### 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. +- In `pygambit`, indexing the children of a node by a string inteprets the string as an action label, + not a label of a child node. In addition, indexing by an action object is now supported. (#587) ### Added - Tests for EFG Nash solvers -- `enumpoly_solve`, `lp_solve`, `lcp_solve` -- in behavior stratgegies