Skip to content

Commit 672298d

Browse files
committed
Fix bug in copy_tree, add tests
1 parent 866edb6 commit 672298d

3 files changed

Lines changed: 62 additions & 2 deletions

File tree

ChangeLog

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [16.2.2] - unreleased
4+
5+
### Fixed
6+
- `Game.copy_tree` and `Game.move_tree` implementations reversed the roles of the
7+
`src` and `dest` nodes (#499)
8+
39
## [16.2.1] - 2025-01-06
410

511
### Fixed
@@ -10,7 +16,6 @@
1016
- Attempting to call the default constructor on Game objects (rather than one of the factory
1117
functions) now raises a more informative exception (#463)
1218

13-
1419
## [16.2.0] - 2024-04-05
1520

1621
### Fixed

src/pygambit/game.pxi

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1362,7 +1362,16 @@ class Game:
13621362
resolved_node.node.deref().InsertMove(resolved_infoset.infoset)
13631363

13641364
def copy_tree(self, src: typing.Union[Node, str], dest: typing.Union[Node, str]) -> None:
1365-
"""Copy the subtree rooted at 'src' to 'dest'.
1365+
"""Copy the subtree rooted at the node `src` to the node `dest`.
1366+
1367+
Each node in the subtree copied to follow `dest` is placed in the same information set
1368+
as the corresponding node in the original subtree under `src`.
1369+
1370+
It is permitted for `dest` to be a descendant of `src`.
1371+
The operation uses the subtree rooted at `src` as it is at the time the function is called,
1372+
so no infinite recursion is triggered.
1373+
1374+
The outcome associated with `dest` is not changed by this operation.
13661375

13671376
Parameters
13681377
----------

tests/test_node.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,36 @@
1+
import typing
12
import unittest
23

34
import pygambit
45

56
from . import games
67

78

9+
# an auxiliary function used in `copy_tree` tests
10+
def subtrees_equal(
11+
n1: pygambit.Node,
12+
n2: pygambit.Node,
13+
recursion_stop_node: typing.Union[pygambit.Node, None] = None
14+
) -> bool:
15+
if n1 == recursion_stop_node:
16+
return n2.is_terminal
17+
if n1.is_terminal and n2.is_terminal:
18+
return n1.outcome == n2.outcome
19+
if n1.is_terminal is not n2.is_terminal:
20+
return False
21+
# now, both n1 and n2 are non-terminal
22+
# check that they are in the same infosets
23+
if n1.infoset != n2.infoset:
24+
return False
25+
# check that they have the same number of children
26+
if len(n1.children) != len(n2.children):
27+
return False
28+
29+
return all(
30+
subtrees_equal(c1, c2, recursion_stop_node) for (c1, c2) in zip(n1.children, n2.children)
31+
)
32+
33+
834
class TestGambitNode(unittest.TestCase):
935
def setUp(self):
1036
self.game = pygambit.Game.new_tree()
@@ -205,6 +231,26 @@ def test_node_copy_across_games(self):
205231
self.assertRaises(pygambit.MismatchError, self.game.copy_tree,
206232
self.extensive_game.root, self.game.root)
207233

234+
def test_copy_tree_onto_nondescendant_terminal_node(self):
235+
236+
g = games.read_from_file("e01.efg")
237+
src_node = g.nodes()[3] # path=[1, 0]
238+
dest_node = g.nodes()[2] # path=[0, 0]
239+
240+
g.copy_tree(src_node, dest_node)
241+
242+
assert subtrees_equal(src_node, dest_node)
243+
244+
def test_copy_tree_onto_descendant_terminal_node(self):
245+
246+
g = games.read_from_file("e01.efg")
247+
src_node = g.nodes()[1] # path=[0]
248+
dest_node = g.nodes()[4] # path=[0, 1, 0]
249+
250+
g.copy_tree(src_node, dest_node)
251+
252+
assert subtrees_equal(src_node, dest_node, dest_node)
253+
208254
def test_node_move_nonterminal(self):
209255
"""Test on moving to a nonterminal node."""
210256
self.assertRaises(pygambit.UndefinedOperationError, self.extensive_game.move_tree,

0 commit comments

Comments
 (0)