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
33 changes: 13 additions & 20 deletions src/games/gametree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ GameAction GameTreeInfosetRep::InsertAction(GameAction p_action /* =0 */)
new GameTreeNodeRep(m_efg, member));
}

m_efg->m_numNodes += m_members.size();

m_efg->ClearComputedValues();
m_efg->Canonicalize();
return action;
Expand Down Expand Up @@ -444,6 +446,9 @@ void GameTreeNodeRep::DeleteTree()
m_children.front()->Invalidate();
erase_atindex(m_children, 1);
}

m_efg->m_numNodes--;

if (m_infoset) {
m_infoset->RemoveMember(this);
m_infoset = nullptr;
Expand Down Expand Up @@ -600,9 +605,11 @@ GameInfoset GameTreeNodeRep::AppendMove(GameInfoset p_infoset)
m_efg->IncrementVersion();
m_infoset = dynamic_cast<GameTreeInfosetRep *>(p_infoset.operator->());
m_infoset->AddMember(this);
std::for_each(
m_infoset->m_actions.begin(), m_infoset->m_actions.end(),
[this](const GameActionRep *) { m_children.push_back(new GameTreeNodeRep(m_efg, this)); });
std::for_each(m_infoset->m_actions.begin(), m_infoset->m_actions.end(),
[this](const GameActionRep *) {
m_children.push_back(new GameTreeNodeRep(m_efg, this));
m_efg->m_numNodes++;
});
m_efg->ClearComputedValues();
m_efg->Canonicalize();
return m_infoset;
Expand Down Expand Up @@ -647,6 +654,9 @@ GameInfoset GameTreeNodeRep::InsertMove(GameInfoset p_infoset)
newNode->m_children.push_back(new GameTreeNodeRep(m_efg, newNode));
});

// Total nodes added = 1 (newNode) + (NumActions - 1) (new children of newNode) = NumActions
m_efg->m_numNodes += newNode->m_infoset->m_actions.size();

m_efg->ClearComputedValues();
m_efg->Canonicalize();
return p_infoset;
Expand Down Expand Up @@ -1015,23 +1025,6 @@ void GameTreeRep::DeleteOutcome(const GameOutcome &p_outcome)
ClearComputedValues();
}

//------------------------------------------------------------------------
// GameTreeRep: Nodes
//------------------------------------------------------------------------

namespace {
size_t CountNodes(GameNode p_node)
{
size_t num = 1;
for (size_t i = 1; i <= p_node->NumChildren(); num += CountNodes(p_node->GetChild(i++)))
;
return num;
}

} // end anonymous namespace

size_t GameTreeRep::NumNodes() const { return CountNodes(m_root); }

//------------------------------------------------------------------------
// GameTreeRep: Modification
//------------------------------------------------------------------------
Expand Down
3 changes: 2 additions & 1 deletion src/games/gametree.h
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ class GameTreeRep : public GameExplicitRep {
mutable bool m_computedValues{false}, m_doCanon{true};
GameTreeNodeRep *m_root;
GamePlayerRep *m_chance;
std::size_t m_numNodes = 1;

/// @name Private auxiliary functions
//@{
Expand Down Expand Up @@ -265,7 +266,7 @@ class GameTreeRep : public GameExplicitRep {
/// Returns the root node of the game
GameNode GetRoot() const override { return m_root; }
/// Returns the number of nodes in the game
size_t NumNodes() const override;
size_t NumNodes() const override { return m_numNodes; }
//@}

void DeleteOutcome(const GameOutcome &) override;
Expand Down
79 changes: 47 additions & 32 deletions src/pygambit/game.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,40 @@ def read_agg(filepath_or_buffer: typing.Union[str, pathlib.Path, io.IOBase]) ->
return read_game(filepath_or_buffer, parser=ParseAggGame)


@cython.cclass
class GameNodes:
"""Represents the set of nodes in a game."""
game = cython.declare(c_Game)

def __init__(self, *args, **kwargs) -> None:
raise ValueError("Cannot create GameNodes outside a Game.")

@staticmethod
@cython.cfunc
def wrap(game: c_Game) -> GameNodes:
obj: GameNodes = GameNodes.__new__(GameNodes)
obj.game = game
return obj

def __repr__(self) -> str:
return f"GameNodes(game={Game.wrap(self.game)})"

def __len__(self) -> int:
"""The number of nodes in the game."""
if not self.game.deref().IsTree():
return 0
return self.game.deref().NumNodes()

def __iter__(self) -> typing.Iterator[Node]:
def dfs(node):
yield node
for child in node.children:
yield from dfs(child)
if not self.game.deref().IsTree():
return
yield from dfs(Node.wrap(self.game.deref().GetRoot()))


@cython.cclass
class GameOutcomes:
"""Represents the set of outcomes in a game."""
Expand Down Expand Up @@ -667,6 +701,18 @@ class Game:
"""The set of outcomes in the game."""
return GameOutcomes.wrap(self.game)

@property
def nodes(self) -> GameNodes:
"""The set of nodes in the game.

Iteration over this property yields the nodes in the order of depth-first search.

.. versionchanged:: 16.4
Changed from a method ``nodes()`` to a property. Access as
``game.nodes`` instead of ``game.nodes()``.
"""
return GameNodes.wrap(self.game)

@property
def contingencies(self) -> pygambit.gameiter.Contingencies:
"""An iterator over the contingencies in the game."""
Expand Down Expand Up @@ -1062,37 +1108,6 @@ class Game:
raise ValueError("attempted to remove the last strategy for player")
return profile

def nodes(
self,
subtree: typing.Optional[typing.Union[Node, str]] = None
) -> typing.List[Node]:
"""Return a list of nodes in the game tree. If `subtree` is not None, returns
the nodes in the subtree rooted at that node.

Nodes are returned in prefix-traversal order: a node appears prior to the list of
nodes in the subtrees rooted at the node's children.

Parameters
----------
subtree : Node or str, optional
If specified, return only the nodes in the subtree rooted at `subtree`.

Raises
------
MismatchError
If `node` is a `Node` from a different game.
"""
if not self.is_tree:
return []
if subtree:
resolved_node = cython.cast(Node, self._resolve_node(subtree, "nodes", "subtree"))
else:
resolved_node = self.root
return (
[resolved_node] +
[n for child in resolved_node.children for n in self.nodes(child)]
)

@cython.cfunc
def _to_format(
self,
Expand Down Expand Up @@ -1369,7 +1384,7 @@ class Game:
raise ValueError(
f"{funcname}(): {argname} cannot be an empty string or all spaces"
)
for n in self.nodes():
for n in self.nodes:
if n.label == node:
return n
raise KeyError(f"{funcname}(): no node with label '{node}'")
Expand Down
28 changes: 14 additions & 14 deletions tests/test_behav.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ def test_realiz_prob_nodes_reference(game: gbt.Game, node_idx: int,
realiz_prob: typing.Union[str, float], rational_flag: bool):
profile = game.mixed_behavior_profile(rational=rational_flag)
realiz_prob = (gbt.Rational(realiz_prob) if rational_flag else realiz_prob)
node = game.nodes()[node_idx]
node = list(game.nodes)[node_idx]
assert profile.realiz_prob(node) == realiz_prob


Expand Down Expand Up @@ -890,7 +890,7 @@ def test_martingale_property_of_node_value(game: gbt.Game, rational_flag: bool):
realization probabilities of those children
"""
profile = game.mixed_behavior_profile(rational=rational_flag)
for node in game.nodes():
for node in game.nodes:
if node.is_terminal or node.player.is_chance:
continue
expected_val = 0
Expand Down Expand Up @@ -1080,23 +1080,23 @@ def _get_and_check_answers(game: gbt.Game, action_probs1: tuple, action_probs2:
######################################################################################
# belief (at nodes)
(games.create_mixed_behav_game_efg(), PROBS_1A_doub, PROBS_2A_doub, False,
lambda x, y: x.belief(y), lambda x: x.nodes()),
lambda x, y: x.belief(y), lambda x: x.nodes),
(games.create_mixed_behav_game_efg(), PROBS_1A_rat, PROBS_2A_rat, True,
lambda x, y: x.belief(y), lambda x: x.nodes()),
lambda x, y: x.belief(y), lambda x: x.nodes),
(games.create_myerson_2_card_poker_efg(), PROBS_1B_doub, PROBS_2B_doub, False,
lambda x, y: x.belief(y), lambda x: x.nodes()),
lambda x, y: x.belief(y), lambda x: x.nodes),
(games.create_myerson_2_card_poker_efg(), PROBS_1A_rat, PROBS_2A_rat, True,
lambda x, y: x.belief(y), lambda x: x.nodes()),
lambda x, y: x.belief(y), lambda x: x.nodes),
######################################################################################
# realiz_prob (at nodes)
(games.create_mixed_behav_game_efg(), PROBS_1A_doub, PROBS_2A_doub, False,
lambda x, y: x.realiz_prob(y), lambda x: x.nodes()),
lambda x, y: x.realiz_prob(y), lambda x: x.nodes),
(games.create_mixed_behav_game_efg(), PROBS_1A_rat, PROBS_2A_rat, True,
lambda x, y: x.realiz_prob(y), lambda x: x.nodes()),
lambda x, y: x.realiz_prob(y), lambda x: x.nodes),
(games.create_myerson_2_card_poker_efg(), PROBS_1B_doub, PROBS_2B_doub, False,
lambda x, y: x.realiz_prob(y), lambda x: x.nodes()),
lambda x, y: x.realiz_prob(y), lambda x: x.nodes),
(games.create_myerson_2_card_poker_efg(), PROBS_1A_rat, PROBS_2A_rat, True,
lambda x, y: x.realiz_prob(y), lambda x: x.nodes()),
lambda x, y: x.realiz_prob(y), lambda x: x.nodes),
######################################################################################
# infoset_prob
(games.create_mixed_behav_game_efg(), PROBS_1A_doub, PROBS_2A_doub, False,
Expand Down Expand Up @@ -1141,16 +1141,16 @@ def _get_and_check_answers(game: gbt.Game, action_probs1: tuple, action_probs2:
# node_value
(games.create_mixed_behav_game_efg(), PROBS_1A_doub, PROBS_2A_doub, False,
lambda x, y: x.node_value(player=y[0], node=y[1]),
lambda x: list(product(x.players, x.nodes()))),
lambda x: list(product(x.players, x.nodes))),
(games.create_mixed_behav_game_efg(), PROBS_1A_rat, PROBS_2A_rat, True,
lambda x, y: x.node_value(player=y[0], node=y[1]),
lambda x: list(product(x.players, x.nodes()))),
lambda x: list(product(x.players, x.nodes))),
(games.create_myerson_2_card_poker_efg(), PROBS_1B_doub, PROBS_2B_doub, False,
lambda x, y: x.node_value(player=y[0], node=y[1]),
lambda x: list(product(x.players, x.nodes()))),
lambda x: list(product(x.players, x.nodes))),
(games.create_myerson_2_card_poker_efg(), PROBS_1A_rat, PROBS_2A_rat, True,
lambda x, y: x.node_value(player=y[0], node=y[1]),
lambda x: list(product(x.players, x.nodes()))),
lambda x: list(product(x.players, x.nodes))),
######################################################################################
# liap_value (of profile, hence [1] for objects_to_test, any singleton collection would do)
(games.create_mixed_behav_game_efg(), PROBS_1A_doub, PROBS_2A_doub, False,
Expand Down
5 changes: 0 additions & 5 deletions tests/test_extensive.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,6 @@ def test_game_add_players_nolabel():
game.add_player()


def test_game_num_nodes():
game = games.read_from_file("basic_extensive_game.efg")
assert len(game.nodes()) == 15


def test_game_is_perfect_recall():
game = games.read_from_file("perfect_recall.efg")
assert game.is_perfect_recall
Expand Down
2 changes: 1 addition & 1 deletion tests/test_game_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def test_resolve_strategy_invalid(
]
)
def test_resolve_node(game: gbt.Game) -> None:
_test_valid_resolutions(game.nodes(),
_test_valid_resolutions(game.nodes,
lambda label, fn: game._resolve_node(label, fn))


Expand Down
Loading