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
42 changes: 0 additions & 42 deletions src/pygambit/action.pxi

This file was deleted.

97 changes: 97 additions & 0 deletions src/pygambit/behavmixed.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -460,20 +460,117 @@ class MixedBehaviorProfile:
for node in self.game.get_infosets(player):
yield node.infoset

# The public API above is implemented once here and dispatches to the hooks below,
# each of which is implemented by a concrete dtype-specific subclass
# (MixedBehaviorProfileDouble/MixedBehaviorProfileRational).

def _check_validity(self) -> None:
"""Raises GameStructureChangedError if the game has structurally changed since
this profile was created.
"""
raise NotImplementedError

@property
def _game(self) -> Game:
"""The game on which this profile is defined."""
raise NotImplementedError

@cython.cfunc
def _getprob_action(self, index: c_GameAction) -> object:
"""Returns the probability with which action `index` is played."""
raise NotImplementedError

@cython.cfunc
def _setprob_action(self, index: c_GameAction, value: typing.Any) -> cython.void:
"""Sets the probability with which action `index` is played."""
raise NotImplementedError

def _to_prob(self, value: typing.Any) -> ProfileDType:
"""Coerces value (int, float, str, Decimal, or Rational) into this profile's
native probability type.
"""
raise NotImplementedError

def _is_defined_at(self, infoset: Infoset) -> bool:
"""Returns whether the profile specifies a probability distribution at infoset."""
raise NotImplementedError

def _payoff(self, player: str) -> ProfileDType:
"""Returns the expected payoff to player."""
raise NotImplementedError

def _belief(self, node: Node) -> ProfileDType | None:
"""Returns the belief probability of reaching node, conditional on play having
reached its information set; None if the information set is unreached.
"""
raise NotImplementedError

def _realiz_prob(self, node: Node) -> ProfileDType:
"""Returns the probability that play reaches node."""
raise NotImplementedError

def _infoset_prob(self, infoset: _InfosetOrEvent) -> ProfileDType:
"""Returns the probability that play reaches infoset."""
raise NotImplementedError

def _infoset_value(self, infoset: Infoset) -> ProfileDType | None:
"""Returns the expected payoff to the player owning infoset, conditional on
reaching it; None if it is unreached.
"""
raise NotImplementedError

def _node_value(self, player: str, node: Node) -> ProfileDType:
"""Returns the expected payoff to player, conditional on reaching node."""
raise NotImplementedError

@cython.cfunc
def _action_value(self, action: c_GameAction) -> object:
"""Returns the expected payoff to playing action, conditional on reaching its
information set; None if the information set is unreached.
"""
raise NotImplementedError

@cython.cfunc
def _action_regret(self, action: c_GameAction) -> object:
"""Returns the regret to playing action."""
raise NotImplementedError

def _infoset_regret(self, infoset: Infoset) -> ProfileDType:
"""Returns the regret of the player owning infoset for their behavior at it."""
raise NotImplementedError

def _agent_max_regret(self) -> ProfileDType:
"""Returns the maximum regret of any player at any information set."""
raise NotImplementedError

def _max_regret(self) -> ProfileDType:
"""Returns the maximum regret of any player over their whole strategy."""
raise NotImplementedError

def _agent_liap_value(self) -> ProfileDType:
"""Returns the agent-form Lyapunov value of the profile."""
raise NotImplementedError

def _liap_value(self) -> ProfileDType:
"""Returns the Lyapunov value of the profile."""
raise NotImplementedError

def _copy(self) -> MixedBehaviorProfile:
"""Creates a copy of the profile."""
raise NotImplementedError

def _as_strategy(self) -> MixedStrategyProfile:
"""Creates the equivalent mixed strategy profile."""
raise NotImplementedError

def _as_float(self) -> MixedBehaviorProfileDouble:
"""Creates a floating-point copy of the profile."""
raise NotImplementedError

def _normalize(self) -> MixedBehaviorProfile:
"""Creates a copy of the profile, normalized so each information set's action
probabilities sum to one.
"""
raise NotImplementedError

def _mixed_action_at(self, infoset: Infoset) -> MixedAction:
Expand Down
33 changes: 4 additions & 29 deletions src/pygambit/behavspt.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -24,49 +24,24 @@ from cython.operator cimport dereference as deref


@cython.cclass
class ActionSupport:
class ActionSupport(_LabelSet):
"""A set of actions at a specified information set in a `BehaviorSupportProfile`.

An immutable snapshot taken from a ``BehaviorSupportProfile`` at retrieval time: it
does not reflect later changes to the profile. The information set is accessible
via `infoset`.
"""
_infoset = cython.declare(Infoset)
_actions = cython.declare(tuple)

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

@staticmethod
@cython.cfunc
def wrap(infoset: Infoset, actions: tuple) -> ActionSupport:
obj: ActionSupport = ActionSupport.__new__(ActionSupport)
obj._infoset = infoset
obj._actions = actions
obj._owner = infoset
obj._labels = actions
return obj

@property
def infoset(self) -> Infoset:
return self._infoset

def __repr__(self) -> str:
return str(list(self._actions))

def __eq__(self, other: typing.Any) -> bool:
if isinstance(other, (set, frozenset, list, tuple)):
return set(self._actions) == set(other)
if not isinstance(other, ActionSupport) or self.infoset != other.infoset:
return False
return set(self._actions) == set(cython.cast(ActionSupport, other)._actions)

def __len__(self) -> int:
return len(self._actions)

def __iter__(self) -> typing.Generator[str, None, None]:
yield from self._actions

def __contains__(self, label: str) -> bool:
return label in self._actions
return self._owner


@cython.cclass
Expand Down
72 changes: 72 additions & 0 deletions src/pygambit/cli/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import sys

import click
import numpy as np

import pygambit as gbt

Expand Down Expand Up @@ -107,6 +108,22 @@ def print_banner(description: str, extra_lines: tuple[str, ...] = ()) -> None:
click.echo(err=True)


def load_game(
quiet: bool,
description: str,
file: str | None,
prog_name: str,
extra_lines: tuple[str, ...] = (),
) -> gbt.Game:
"""Standard tool startup, shared by every `gambit-*` CLI tool's `main()`: print
the banner (see `print_banner`) unless `quiet`, then read the game from `file`
(or standard input).
"""
if not quiet:
print_banner(description, extra_lines)
return read_game(open_game_file(file, prog_name))


def version_option(description: str, extra_lines: tuple[str, ...] = ()) -> callable:
"""A ``-v``/``--version`` option which prints the tool's banner and exits,
matching the behavior of the C++ command-line tools. See `print_banner` for
Expand Down Expand Up @@ -374,3 +391,58 @@ def read_behavior_profiles_csv(
profile[node] = {a: next(values) for a in node.infoset.actions}
profiles.append(profile)
return profiles


def _validate_random_start_options(
n: int | None, seed: int | None, start_file: str | None
) -> None:
"""Shared validation for the `-n`/`-R`/`-s` starting-point options common to
gambit-gnm, gambit-ipa, and gambit-liap: `-n` and `-s` are mutually exclusive,
and `-R` requires `-n`.
"""
if n is not None and start_file is not None:
raise ValueError("The -n and -s options are mutually exclusive.")
if seed is not None and n is None:
raise ValueError("The -R option requires -n.")


def resolve_strategy_starts(
game: gbt.Game,
n: int | None,
seed: int | None,
start_file: str | None,
default_count: int = 1,
) -> list[gbt.MixedStrategyProfile]:
"""Resolve strategy starting points for a `-n`/`-R`/`-s`-style tool: read from
`start_file` if given, otherwise `n` uniform-random draws (`default_count` if `n`
is not given), seeded by `seed`. Shared by gambit-gnm, gambit-ipa, and
gambit-liap's non-agent form.
"""
_validate_random_start_options(n, seed, start_file)
if start_file is not None:
return read_strategy_profiles_csv(start_file, game)
gen = np.random.default_rng(seed)
return [
game.random_strategy_profile(gen=gen)
for _ in range(n if n is not None else default_count)
]


def resolve_behavior_starts(
game: gbt.Game,
n: int | None,
seed: int | None,
start_file: str | None,
default_count: int = 1,
) -> list[gbt.MixedBehaviorProfile]:
"""Behavior-profile counterpart to `resolve_strategy_starts`; see there for the
shared `-n`/`-R`/`-s` semantics. Used by gambit-liap's agent form.
"""
_validate_random_start_options(n, seed, start_file)
if start_file is not None:
return read_behavior_profiles_csv(start_file, game)
gen = np.random.default_rng(seed)
return [
game.random_behavior_profile(gen=gen)
for _ in range(n if n is not None else default_count)
]
8 changes: 2 additions & 6 deletions src/pygambit/cli/enummixed.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@

from .common import (
handle_errors,
open_game_file,
print_banner,
read_game,
load_game,
render_profile_csv,
version_option,
)
Expand Down Expand Up @@ -61,9 +59,7 @@
@version_option(DESCRIPTION)
@handle_errors
def main(file: str | None, decimals: int | None, cliques: bool, quiet: bool) -> None:
if not quiet:
print_banner(DESCRIPTION)
game = read_game(open_game_file(file, PROG_NAME))
game = load_game(quiet, DESCRIPTION, file, PROG_NAME)
rational = decimals is None

def render(profile) -> None:
Expand Down
8 changes: 2 additions & 6 deletions src/pygambit/cli/enumpoly.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@

from .common import (
handle_errors,
open_game_file,
print_banner,
read_game,
load_game,
render_profile_csv,
render_support_csv,
validate_stop_after,
Expand Down Expand Up @@ -113,9 +111,7 @@ def main(
quiet: bool,
verbose: bool,
) -> None:
if not quiet:
print_banner(DESCRIPTION)
game = read_game(open_game_file(file, PROG_NAME))
game = load_game(quiet, DESCRIPTION, file, PROG_NAME)
if not game.is_perfect_recall:
raise ValueError("Computing equilibria of games with imperfect recall is not supported.")

Expand Down
8 changes: 2 additions & 6 deletions src/pygambit/cli/enumpure.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@

from .common import (
handle_errors,
open_game_file,
print_banner,
read_game,
load_game,
render_profile_csv,
render_profile_detail,
version_option,
Expand Down Expand Up @@ -62,9 +60,7 @@
@version_option(DESCRIPTION)
@handle_errors
def main(file: str | None, strategic: bool, agent: bool, detail: bool, quiet: bool) -> None:
if not quiet:
print_banner(DESCRIPTION)
game = read_game(open_game_file(file, PROG_NAME))
game = load_game(quiet, DESCRIPTION, file, PROG_NAME)

def render(profile) -> None:
is_behavior = hasattr(profile, "as_strategy")
Expand Down
Loading
Loading