Skip to content

Commit

Permalink
Raise informative exception when trying to call Game().
Browse files Browse the repository at this point in the history
Games are intended to be created using one of the provided factory functions.
However, calling `Game()` did not immediately raise an exception; only when
attempting to operate on the game did a (less informative) exception occur.

This now raises a `ValueError` when calling the `Game` constructor.

Relatedly, this applies the technique across all game-related objects, and
standardises the way in which those objects are initialised internally.

Closes #463.
  • Loading branch information
tturocy committed Dec 8, 2024
1 parent cd09291 commit 6dc0a0b
Show file tree
Hide file tree
Showing 14 changed files with 474 additions and 370 deletions.
2 changes: 2 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
### Fixed
- Reading .efg and .nfg game files which did not have whitespace at the end would lead to
an infinite loop (#457)
- Attempting to call the default constructor on Game objects (rather than one of the factory
functions) now raises a more informative excepition (#463)


## [16.2.0] - 2024-04-05
Expand Down
14 changes: 11 additions & 3 deletions src/pygambit/action.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ class Action:
"""A choice available at an ``Infoset`` in a ``Game``."""
action = cython.declare(c_GameAction)

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

@staticmethod
@cython.cfunc
def wrap(action: c_GameAction) -> Action:
obj: Action = Action.__new__(Action)
obj.action = action
return obj

def __repr__(self) -> str:
if self.label:
return f"Action(infoset={self.infoset}, label='{self.label}')"
Expand Down Expand Up @@ -72,9 +82,7 @@ class Action:
@property
def infoset(self) -> Infoset:
"""Get the information set to which the action belongs."""
i = Infoset()
i.infoset = self.action.deref().GetInfoset()
return i
return Infoset.wrap(self.action.deref().GetInfoset())

@property
def prob(self) -> typing.Union[decimal.Decimal, Rational]:
Expand Down
108 changes: 70 additions & 38 deletions src/pygambit/behavmixed.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,31 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
import cython
from cython.operator cimport dereference as deref


@cython.cclass
class MixedAction:
"""A probability distribution over a player's actions at an information set.
A ``MixedAction`` represents a component of a ``MixedBehaviorProfile``. The
full profile is accessible via the `profile` attribute, and the information set
at which the ``MixedAction`` applies is accessible via `infoset`.
"""
def __init__(self, profile: MixedBehaviorProfile, infoset: Infoset) -> None:
self._profile = profile
self._infoset = infoset
_profile = cython.declare(MixedBehaviorProfile)
_infoset = cython.declare(Infoset)

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

@staticmethod
@cython.cfunc
def wrap(profile: MixedBehaviorProfile, infoset: Infoset) -> MixedAction:
obj: MixedAction = MixedAction.__new__(MixedAction)
obj._profile = profile
obj._infoset = infoset
return obj

@property
def profile(self) -> MixedBehaviorProfile:
Expand Down Expand Up @@ -154,6 +166,7 @@ class MixedAction:
raise TypeError(f"strategy index must be Action or str, not {index.__class__.__name__}")


@cython.cclass
class MixedBehavior:
"""A set of probability distributions describing a player's behavior.
Expand All @@ -162,9 +175,19 @@ class MixedBehavior:
attribute, and the player for whom the ``MixedBehavior`` applies is accessible
via `player`.
"""
def __init__(self, profile: MixedBehaviorProfile, player: Player) -> None:
self._profile = profile
self._player = player
_profile = cython.declare(MixedBehaviorProfile)
_player = cython.declare(Player)

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

@staticmethod
@cython.cfunc
def wrap(profile: MixedBehaviorProfile, player: Player) -> MixedBehavior:
obj: MixedBehavior = MixedBehavior.__new__(MixedBehavior)
obj._profile = profile
obj._player = player
return obj

@property
def profile(self) -> MixedBehaviorProfile:
Expand Down Expand Up @@ -345,6 +368,9 @@ class MixedBehaviorProfile:
MixedStrategyProfile
Represents a mixed strategy profile over a ``Game``.
"""
def __init__(self, *args, **kwargs) -> None:
raise ValueError("Cannot create a MixedBehaviorProfile outside a Game.")

def __repr__(self) -> str:
return str([self[player] for player in self.game.players])

Expand Down Expand Up @@ -438,18 +464,18 @@ class MixedBehaviorProfile:
if isinstance(index, Infoset):
if index.game != self.game:
raise MismatchError("infoset must belong to this game")
return MixedAction(self, index)
return MixedAction.wrap(self, index)
if isinstance(index, Player):
if index.game != self.game:
raise MismatchError("player must belong to this game")
return MixedBehavior(self, index)
return MixedBehavior.wrap(self, index)
if isinstance(index, str):
try:
return MixedBehavior(self, self.game._resolve_player(index, "__getitem__"))
return MixedBehavior.wrap(self, self.game._resolve_player(index, "__getitem__"))
except KeyError:
pass
try:
return MixedAction(self, self.game._resolve_infoset(index, "__getitem__"))
return MixedAction.wrap(self, self.game._resolve_infoset(index, "__getitem__"))
except KeyError:
pass
try:
Expand Down Expand Up @@ -843,6 +869,15 @@ class MixedBehaviorProfile:
class MixedBehaviorProfileDouble(MixedBehaviorProfile):
profile = cython.declare(shared_ptr[c_MixedBehaviorProfileDouble])

@staticmethod
@cython.cfunc
def wrap(profile: shared_ptr[c_MixedBehaviorProfileDouble]) -> MixedBehaviorProfileDouble:
obj: MixedBehaviorProfileDouble = (
MixedBehaviorProfileDouble.__new__(MixedBehaviorProfileDouble)
)
obj.profile = profile
return obj

def _check_validity(self) -> None:
if deref(self.profile).IsInvalidated():
raise GameStructureChangedError()
Expand Down Expand Up @@ -896,38 +931,41 @@ class MixedBehaviorProfileDouble(MixedBehaviorProfile):
)

def _copy(self) -> MixedBehaviorProfileDouble:
behav = MixedBehaviorProfileDouble()
behav.profile = make_shared[c_MixedBehaviorProfileDouble](deref(self.profile))
return behav
return MixedBehaviorProfileDouble.wrap(
make_shared[c_MixedBehaviorProfileDouble](deref(self.profile))
)

def _as_strategy(self) -> MixedStrategyProfileDouble:
mixed = MixedStrategyProfileDouble()
mixed.profile = (
make_shared[c_MixedStrategyProfileDouble](deref(self.profile).ToMixedProfile())
)
return mixed
return MixedStrategyProfileDouble.wrap(make_shared[c_MixedStrategyProfileDouble](
deref(self.profile).ToMixedProfile()
))

def _liap_value(self) -> float:
return deref(self.profile).GetLiapValue()

def _normalize(self) -> MixedBehaviorProfileDouble:
profile = MixedBehaviorProfileDouble()
profile.profile = (
return MixedBehaviorProfileDouble.wrap(
make_shared[c_MixedBehaviorProfileDouble](deref(self.profile).Normalize())
)
return profile

@property
def _game(self) -> Game:
g = Game()
g.game = deref(self.profile).GetGame()
return g
return Game.wrap(deref(self.profile).GetGame())


@cython.cclass
class MixedBehaviorProfileRational(MixedBehaviorProfile):
profile = cython.declare(shared_ptr[c_MixedBehaviorProfileRational])

@staticmethod
@cython.cfunc
def wrap(profile: shared_ptr[c_MixedBehaviorProfileRational]) -> MixedBehaviorProfileRational:
obj: MixedBehaviorProfileRational = (
MixedBehaviorProfileRational.__new__(MixedBehaviorProfileRational)
)
obj.profile = profile
return obj

def _check_validity(self) -> None:
if deref(self.profile).IsInvalidated():
raise GameStructureChangedError()
Expand Down Expand Up @@ -987,29 +1025,23 @@ class MixedBehaviorProfileRational(MixedBehaviorProfile):
)

def _copy(self) -> MixedBehaviorProfileRational:
behav = MixedBehaviorProfileRational()
behav.profile = make_shared[c_MixedBehaviorProfileRational](deref(self.profile))
return behav
return MixedBehaviorProfileRational.wrap(
make_shared[c_MixedBehaviorProfileRational](deref(self.profile))
)

def _as_strategy(self) -> MixedStrategyProfileRational:
mixed = MixedStrategyProfileRational()
mixed.profile = (
make_shared[c_MixedStrategyProfileRational](deref(self.profile).ToMixedProfile())
)
return mixed
return MixedStrategyProfileRational.wrap(make_shared[c_MixedStrategyProfileRational](
deref(self.profile).ToMixedProfile()
))

def _liap_value(self) -> Rational:
return rat_to_py(deref(self.profile).GetLiapValue())

def _normalize(self) -> MixedBehaviorProfileRational:
profile = MixedBehaviorProfileRational()
profile.profile = (
return MixedBehaviorProfileRational.wrap(
make_shared[c_MixedBehaviorProfileRational](deref(self.profile).Normalize())
)
return profile

@property
def _game(self) -> Game:
g = Game()
g.game = deref(self.profile).GetGame()
return g
return Game.wrap(deref(self.profile).GetGame())
Loading

0 comments on commit 6dc0a0b

Please sign in to comment.