Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion ChangeLog
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [16.2.2] - unreleased

### Fixed
- `Game.copy_tree` and `Game.move_tree` implementations reversed the roles of the
`src` and `dest` nodes (#499)

## [16.2.1] - 2025-01-06

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


## [16.2.0] - 2024-04-05

### Fixed
Expand Down
15 changes: 12 additions & 3 deletions src/pygambit/game.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -1362,7 +1362,16 @@ class Game:
resolved_node.node.deref().InsertMove(resolved_infoset.infoset)

def copy_tree(self, src: typing.Union[Node, str], dest: typing.Union[Node, str]) -> None:
"""Copy the subtree rooted at 'src' to 'dest'.
"""Copy the subtree rooted at the node `src` to the node `dest`.

Each node in the subtree copied to follow `dest` is placed in the same information set
as the corresponding node in the original subtree under `src`.

It is permitted for `dest` to be a descendant of `src`.
The operation uses the subtree rooted at `src` as it is at the time the function is called,
so no infinite recursion is triggered.

The outcome associated with `dest` is not changed by this operation.

Parameters
----------
Expand All @@ -1382,7 +1391,7 @@ class Game:
resolved_dest = cython.cast(Node, self._resolve_node(dest, "copy_tree", "dest"))
if not resolved_dest.is_terminal:
raise UndefinedOperationError("copy_tree(): `dest` must be a terminal node.")
resolved_src.node.deref().CopyTree(resolved_dest.node)
resolved_dest.node.deref().CopyTree(resolved_src.node)

def move_tree(self, src: typing.Union[Node, str], dest: typing.Union[Node, str]) -> None:
"""Move the subtree rooted at 'src' to 'dest'.
Expand All @@ -1407,7 +1416,7 @@ class Game:
raise UndefinedOperationError("move_tree(): `dest` must be a terminal node.")
if resolved_dest.is_successor_of(resolved_src):
raise UndefinedOperationError("move_tree(): `dest` cannot be a successor of `src`.")
resolved_src.node.deref().MoveTree(resolved_dest.node)
resolved_dest.node.deref().MoveTree(resolved_src.node)

def delete_parent(self, node: typing.Union[Node, str]) -> None:
"""Delete the parent node of `node`. `node` replaces its parent in the tree. All other
Expand Down
46 changes: 46 additions & 0 deletions tests/test_node.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
import typing
import unittest

import pygambit

from . import games


# an auxiliary function used in `copy_tree` tests
def subtrees_equal(
n1: pygambit.Node,
n2: pygambit.Node,
recursion_stop_node: typing.Union[pygambit.Node, None] = None
) -> bool:
if n1 == recursion_stop_node:
return n2.is_terminal
if n1.is_terminal and n2.is_terminal:
return n1.outcome == n2.outcome
if n1.is_terminal is not n2.is_terminal:
return False
# now, both n1 and n2 are non-terminal
# check that they are in the same infosets
if n1.infoset != n2.infoset:
return False
# check that they have the same number of children
if len(n1.children) != len(n2.children):
return False

return all(
subtrees_equal(c1, c2, recursion_stop_node) for (c1, c2) in zip(n1.children, n2.children)
)


class TestGambitNode(unittest.TestCase):
def setUp(self):
self.game = pygambit.Game.new_tree()
Expand Down Expand Up @@ -205,6 +231,26 @@ def test_node_copy_across_games(self):
self.assertRaises(pygambit.MismatchError, self.game.copy_tree,
self.extensive_game.root, self.game.root)

def test_copy_tree_onto_nondescendant_terminal_node(self):

g = games.read_from_file("e01.efg")
src_node = g.nodes()[3] # path=[1, 0]
dest_node = g.nodes()[2] # path=[0, 0]

g.copy_tree(src_node, dest_node)

assert subtrees_equal(src_node, dest_node)

def test_copy_tree_onto_descendant_terminal_node(self):

g = games.read_from_file("e01.efg")
src_node = g.nodes()[1] # path=[0]
dest_node = g.nodes()[4] # path=[0, 1, 0]

g.copy_tree(src_node, dest_node)

assert subtrees_equal(src_node, dest_node, dest_node)

def test_node_move_nonterminal(self):
"""Test on moving to a nonterminal node."""
self.assertRaises(pygambit.UndefinedOperationError, self.extensive_game.move_tree,
Expand Down