Skip to content
Merged
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
30 changes: 19 additions & 11 deletions src/pygambit/behavmixed.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class InfosetIndexedVector(_LabeledVector):
# as the KeyError below, from _values simply not having that entry -- not as a
# resolution-time error that would also wrongly block a valid chance lookup in
# infoset_probs.
resolved_node = self._game._resolve_infoset_or_event(
resolved_node = cython.cast(Game, self._game)._resolve_infoset_or_event(
selector, f"{type(self).__name__}.__getitem__"
)
try:
Expand Down Expand Up @@ -362,7 +362,9 @@ class MixedBehavior:
raise TypeError(
f"MixedBehavior index must be Selector, not {selector.__class__.__name__}"
)
infoset = self._game._resolve_infoset(selector, "MixedBehavior.__getitem__")
infoset = cython.cast(Game, self._game)._resolve_infoset(
selector, "MixedBehavior.__getitem__"
)
if infoset.player != self._player:
raise MismatchError(
"selector must resolve to an information set belonging to this player"
Expand Down Expand Up @@ -461,11 +463,11 @@ class MixedBehaviorProfile:
if isinstance(index, str):
values = {
_canonical_history(node): self._mixed_action_at(node)
for node in self.game._get_infosets(index)
for node in cython.cast(Game, self.game)._get_infosets(index)
}
return MixedBehavior.wrap(self.game, index, values)
if isinstance(index, Selector):
resolved_infoset = self.game._resolve_infoset(
resolved_infoset = cython.cast(Game, self.game)._resolve_infoset(
index, "MixedBehaviorProfile.__getitem__"
)
return self._mixed_action_at(resolved_infoset)
Expand All @@ -478,7 +480,7 @@ class MixedBehaviorProfile:
belonging to a personal player, excluding the chance player's.
"""
for player in self.game.players:
yield from self.game._get_infosets(player)
yield from cython.cast(Game, self.game)._get_infosets(player)

def _events(self) -> typing.Iterator[Node]:
"""Iterates over a representative node of every event of the chance player in
Expand Down Expand Up @@ -680,7 +682,9 @@ class MixedBehaviorProfile:
self._check_validity()
if not isinstance(index, Selector):
raise TypeError(f"profile index must be Selector, not {index.__class__.__name__}")
infoset = self.game._resolve_infoset(index, "MixedBehaviorProfile.__setitem__")
infoset = cython.cast(Game, self.game)._resolve_infoset(
index, "MixedBehaviorProfile.__setitem__"
)
self._setprob_infoset(infoset, distribution, sparse=True)

def set_mixed_action(
Expand Down Expand Up @@ -731,7 +735,9 @@ class MixedBehaviorProfile:
self._check_validity()
if not isinstance(index, Selector):
raise TypeError(f"profile index must be Selector, not {index.__class__.__name__}")
infoset = self.game._resolve_infoset(index, "MixedBehaviorProfile.set_mixed_action")
infoset = cython.cast(Game, self.game)._resolve_infoset(
index, "MixedBehaviorProfile.set_mixed_action"
)
self._setprob_infoset(infoset, distribution, sparse=sparse)

@property
Expand All @@ -753,7 +759,7 @@ class MixedBehaviorProfile:
.. versionadded:: 17.0.0
"""
self._check_validity()
histories: dict = self.game._all_histories()
histories: dict = cython.cast(Game, self.game)._all_histories()
return HistoryValuesVector({
p: HistoryValueVector({
history: self._node_value(p, n) for n, history in histories.items()
Expand Down Expand Up @@ -810,7 +816,8 @@ class MixedBehaviorProfile:
"""
self._check_validity()
return RealizProbVector({
history: self._realiz_prob(n) for n, history in self.game._all_histories().items()
history: self._realiz_prob(n)
for n, history in cython.cast(Game, self.game)._all_histories().items()
})

@property
Expand Down Expand Up @@ -887,7 +894,8 @@ class MixedBehaviorProfile:
"""
self._check_validity()
return BeliefVector({
history: self._belief(n) for n, history in self.game._all_histories().items()
history: self._belief(n)
for n, history in cython.cast(Game, self.game)._all_histories().items()
})

@property
Expand Down Expand Up @@ -1308,7 +1316,7 @@ class MixedBehaviorProfileRational(MixedBehaviorProfile):
def _as_float(self) -> MixedBehaviorProfileDouble:
profile: MixedBehaviorProfileDouble = self.game.mixed_behavior_profile()
for player in self.game.players:
for node in self.game._get_infosets(player):
for node in cython.cast(Game, self.game)._get_infosets(player):
profile._setprob_infoset(
node,
{
Expand Down
14 changes: 9 additions & 5 deletions src/pygambit/behavspt.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ class BehaviorSupport:
raise TypeError(
f"BehaviorSupport index must be Selector, not {selector.__class__.__name__}"
)
resolved_node = self._game._resolve_infoset(selector, "BehaviorSupport.__getitem__")
resolved_node = cython.cast(Game, self._game)._resolve_infoset(
selector, "BehaviorSupport.__getitem__"
)
if resolved_node.player != self._player:
raise MismatchError(
"selector must resolve to an information set belonging to this player"
Expand Down Expand Up @@ -212,11 +214,11 @@ class BehaviorSupportProfile:
if isinstance(index, str):
values = {
_canonical_history(node): self._action_support_at(node)
for node in self.game._get_infosets(index)
for node in cython.cast(Game, self.game)._get_infosets(index)
}
return BehaviorSupport.wrap(self.game, index, values)
if isinstance(index, Selector):
resolved_node = self.game._resolve_infoset(
resolved_node = cython.cast(Game, self.game)._resolve_infoset(
index, "BehaviorSupportProfile.__getitem__"
)
return self._action_support_at(resolved_node)
Expand Down Expand Up @@ -297,7 +299,7 @@ class BehaviorSupportProfile:
raise TypeError(
f"profile index must be Selector, not {infoset.__class__.__name__}"
)
resolved_node = self.game._resolve_infoset(
resolved_node = cython.cast(Game, self.game)._resolve_infoset(
infoset, "BehaviorSupportProfile.__setitem__"
)
self._set_support(resolved_node, actions)
Expand Down Expand Up @@ -336,5 +338,7 @@ class BehaviorSupportProfile:
f"is_infoset_reachable(): infoset must be a Selector, "
f"not {infoset.__class__.__name__}"
)
resolved_node: Node = self.game._resolve_infoset(infoset, "is_infoset_reachable")
resolved_node: Node = cython.cast(Game, self.game)._resolve_infoset(
infoset, "is_infoset_reachable"
)
return deref(self.profile).IsReachable(resolved_node._infoset_handle())
123 changes: 52 additions & 71 deletions src/pygambit/game.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ class Game:
def description(self, value: str) -> None:
self.game.deref().SetDescription(value.encode("utf-8"))

@cython.cfunc
def _get_infosets(self, player: str) -> list[Node]:
"""Internal: like `get_infosets`, but keeps `Node` objects rather than
materializing each into a History -- used internally where the actual node
Expand Down Expand Up @@ -500,6 +501,7 @@ class Game:
"""An iterator over the contingencies in the game."""
return pygambit.gameiter.Contingencies(self)

@cython.cfunc
def _root(self) -> Node:
"""The root node of the game. Not part of the public API; the public
equivalent is the trivial empty History, `()`, or `H.path()` as a Selector.
Expand All @@ -510,6 +512,7 @@ class Game:
)
return Node.wrap(self.game.deref().GetRoot())

@cython.cfunc
def _all_nodes(self) -> list:
"""All nodes in the game, in depth-first traversal order. Not part of
the public API; the public equivalent is `Game.get_histories(H.after())`.
Expand All @@ -520,6 +523,7 @@ class Game:
)
return [Node.wrap(node) for node in self.game.deref().GetNodes()]

@cython.cfunc
def _all_histories(self) -> dict:
"""The History of every node in the game, computed in one linear sweep.
Not part of the public API; used where most or all of a possibly-large
Expand Down Expand Up @@ -554,6 +558,7 @@ class Game:
))
return histories

@cython.cfunc
def _get_nodes(self, selector: Selector) -> list[Node]:
"""Evaluate `selector` (an `H`-built expression) against this game.

Expand Down Expand Up @@ -787,6 +792,7 @@ class Game:
)
return [_history_of(member) for member in self._get_members(history)]

@cython.cfunc
def _get_members(self, history: Selector) -> list[Node]:
"""Internal: like `get_members`, but keeps `Node` objects rather than
materializing each into a History -- used internally where the actual node
Expand All @@ -799,6 +805,7 @@ class Game:
return []
return [Node.wrap(member) for member in infoset_handle.deref().GetMembers()]

@cython.cfunc
def _group_nodes(self, grouped: GroupedSelector) -> dict:
"""Internal: like `_get_groups`, but keeps `Node` objects rather than
materializing each into a `History` -- used by mutation methods that
Expand Down Expand Up @@ -1718,55 +1725,36 @@ class Game:
return outcome
raise KeyError(f"{funcname}(): no outcome with label '{label}'")

def _resolve_node(self, node: typing.Any, funcname: str, argname: str = "node") -> Node:
"""Resolve an attempt to reference a node of the game. A bare `Node` is not
accepted -- every public method that reaches this already requires a
`Selector` (or, for internal callers, an already-resolved `Node`, never
routed back through here).
@cython.cfunc
def _resolve_node(self, node: Selector, funcname: str, argname: str = "node") -> Node:
"""Resolve an attempt to reference a node of the game. Every public method
that reaches this already requires a `Selector` (or, for internal callers,
an already-resolved `Node`, never routed back through here).

Parameters
----------
node : Any
An object to resolve as a reference to a node.
node : Selector
An `H`-built expression, evaluated against this game, that must resolve
to exactly one node.
funcname : str
The name of the function to raise any exception on behalf of.
argname : str, default 'node'
The name of the argument being checked

Raises
------
KeyError
If `node` is a string and no node in the game has that label.
TypeError
If `node` is not a `Selector`, `tuple`, or `str`
ValueError
If `node` is an empty `str` or all spaces
If `node` does not resolve to exactly one node.
"""
if isinstance(node, Selector):
resolved = self._get_nodes(node)
if len(resolved) != 1:
raise ValueError(
f"{funcname}(): {argname} selector must resolve to exactly one "
f"node, resolved to {len(resolved)}"
)
return resolved[0]
elif isinstance(node, tuple):
# A History -- the manual fallback: root-anchored, every step exact.
return self._resolve_node(Selector().path(*node), funcname, argname)
elif isinstance(node, str):
if not node.strip():
raise ValueError(
f"{funcname}(): {argname} cannot be an empty string or all spaces"
)
for n in self._all_nodes():
if cython.cast(Node, n)._label == node:
return n
raise KeyError(f"{funcname}(): no node with label '{node}'")
raise TypeError(
f"{funcname}(): {argname} must be Selector, tuple, or str, "
f"not {node.__class__.__name__}"
)
resolved = self._get_nodes(node)
if len(resolved) != 1:
raise ValueError(
f"{funcname}(): {argname} selector must resolve to exactly one "
f"node, resolved to {len(resolved)}"
)
return resolved[0]

@cython.cfunc
def _resolve_nodes(self,
nodes: typing.Any,
funcname: str,
Expand All @@ -1789,16 +1777,17 @@ class Game:
raise ValueError(f"{funcname}(): Each node must be referenced only once")
return resolved_nodes

@cython.cfunc
def _resolve_infoset(self,
infoset: typing.Any, funcname: str, argname: str = "infoset") -> Node:
infoset: Selector, funcname: str, argname: str = "infoset") -> Node:
"""Resolve an attempt to reference a personal player's information set of the
game, via a `Selector` resolving to a member node, or such a node's label.
game, via a `Selector` resolving to a member node.

Parameters
----------
infoset : Selector, tuple, or str
A `Selector`/History resolving to a node belonging to the information
set, or such a node's label.
infoset : Selector
An `H`-built expression, evaluated against this game, that must resolve
to a node belonging to the information set.
funcname : str
The name of the function to raise any exception on behalf of.
argname : str, default 'infoset'
Expand All @@ -1812,13 +1801,10 @@ class Game:

Raises
------
KeyError
If `infoset` is a string and no node in the game has that label.
TypeError
If `infoset` is not a `Selector`, `tuple`, or `str`
ValueError
If `infoset` resolves to a chance event rather than a personal player's
information set, or to no information set at all (the node is terminal).
If `infoset` does not resolve to exactly one node, or the node resolves
to a chance event rather than a personal player's information set, or to
no information set at all (the node is terminal).
"""
resolved_node = self._resolve_node(infoset, funcname, argname)
is_personal, is_chance = _node_infoset_kind(resolved_node)
Expand All @@ -1828,16 +1814,17 @@ class Game:
funcname, argname
)

@cython.cfunc
def _resolve_event(self,
event: typing.Any, funcname: str, argname: str = "event") -> Node:
event: Selector, funcname: str, argname: str = "event") -> Node:
"""Resolve an attempt to reference a chance event of the game, via a
`Selector` resolving to a member node, or such a node's label.
`Selector` resolving to a member node.

Parameters
----------
event : Selector, tuple, or str
A `Selector`/History resolving to a node belonging to the event, or
such a node's label.
event : Selector
An `H`-built expression, evaluated against this game, that must resolve
to a node belonging to the event.
funcname : str
The name of the function to raise any exception on behalf of.
argname : str, default 'event'
Expand All @@ -1851,13 +1838,10 @@ class Game:

Raises
------
KeyError
If `event` is a string and no node in the game has that label.
TypeError
If `event` is not a `Selector`, `tuple`, or `str`
ValueError
If `event` resolves to a personal player's information set rather than a
chance event, or to no event at all (the node is terminal).
If `event` does not resolve to exactly one node, or the node resolves to
a personal player's information set rather than a chance event, or to no
event at all (the node is terminal).
"""
resolved_node = self._resolve_node(event, funcname, argname)
is_personal, is_chance = _node_infoset_kind(resolved_node)
Expand All @@ -1867,20 +1851,21 @@ class Game:
funcname, argname
)

@cython.cfunc
def _resolve_infoset_or_event(self,
infoset: typing.Any,
infoset: Selector,
funcname: str,
argname: str = "infoset") -> Node:
"""Resolve an attempt to reference an information set or event of the game
(whichever applies), via a `Selector` resolving to a member node, or such a
node's label. For operations that apply uniformly to either, such as
attaching to an existing one.
(whichever applies), via a `Selector` resolving to a member node. For
operations that apply uniformly to either, such as attaching to an existing
one.

Parameters
----------
infoset : Selector, tuple, or str
A `Selector`/History resolving to a node belonging to the information
set or event, or such a node's label.
infoset : Selector
An `H`-built expression, evaluated against this game, that must resolve
to a node belonging to the information set or event.
funcname : str
The name of the function to raise any exception on behalf of.
argname : str, default 'infoset'
Expand All @@ -1894,13 +1879,9 @@ class Game:

Raises
------
KeyError
If `infoset` is a string and no node in the game has that label.
TypeError
If `infoset` is not a `Selector`, `tuple`, or `str`
ValueError
If `infoset` resolves to no information set or event (the node is
terminal).
If `infoset` does not resolve to exactly one node, or resolves to no
information set or event (the node is terminal).
"""
resolved_node = self._resolve_node(infoset, funcname, argname)
is_personal, is_chance = _node_infoset_kind(resolved_node)
Expand Down
Loading
Loading