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
24 changes: 24 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
`logit_estimate`, `ipa_solve`, and `gnm_solve` now also accept a rational-precision profile
directly (as a starting point or perturbation vector), converting it internally.
(#721, #458)
- Added `Game.get_outcome`, which returns the `Outcome` attached to a pure-strategy contingency
of a game in strategic (table) representation.
- Added `Game.get_payoffs`, which returns the payoff to each player at a pure-strategy
contingency, for a game in any representation.

### Changed
- Command-line tools are now implemented in `pygambit` rather than C++-based compiled programs.
Expand All @@ -25,6 +29,26 @@
- `Game.new_table` no longer creates an outcome for every contingency; a new strategic game has no outcomes,
and every contingency is initially null. Outcomes are created as they are needed;
games built by `Game.from_arrays` and `Game.from_dict` are unaffected. (#1061)
- `Game.contingencies` now yields contingencies as a mapping from player label to strategy
label, rather than a list of per-player strategy indices.
- `Player.strategies` now iterates strategy labels (`str`) rather than `Strategy` objects;
indexing by label is no longer supported (a label is already in hand once iterated) -- use
`in` to test membership.
- `Game.strategy_support_profile`'s `strategies` filter callable is now called as
`strategies(player, label)` (two positional arguments) rather than with a single `Strategy`.
- `Game.get_behavior`'s `player` and `strategy` parameters are now labels (`str`) rather than
accepting a `Player`/`Strategy` object; `StrategyBehavior.player`/`.strategy` now return
labels rather than `Player`/`Strategy` objects.

### Removed
- Removed `Game.__getitem__`, replaced by `Game.get_outcome`/`Game.get_payoffs`.
- Removed `Game.strategies`; iterate `Game.players` and each player's `strategies` instead.
- Removed `Strategy.action`; use `Game.get_behavior(player, strategy)[infoset]` (or `.get`),
already the documented fuller form of the same lookup.
- Removed `Strategy`. Strategies are now identified purely by label (`str`), as returned by
`Player.strategies`; a `Strategy` object carried no information a label didn't already
carry, now that strategy labels are unique within a player. `GameStrategyRep` in the C++
core is unaffected by this change (a separate, later piece of work).

## [17.0.0-alpha.2] - 2026-08-21

Expand Down
14 changes: 2 additions & 12 deletions doc/pygambit.api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ Representation of games
Node
Infoset
Action
Strategy
Subgame


Expand Down Expand Up @@ -109,12 +108,13 @@ Information about the game
Game.outcomes
Game.min_payoff
Game.max_payoff
Game.strategies
Game.root
Game.actions
Game.infosets
Game.nodes
Game.contingencies
Game.get_outcome
Game.get_payoffs
Game.subgames
Game.minimal_subgame

Expand Down Expand Up @@ -192,16 +192,6 @@ Information about the game
Action.prob
Action.plays

.. autosummary::

:toctree: api/

Strategy.label
Strategy.game
Strategy.player
Strategy.number
Strategy.action

.. autosummary::

:toctree: api/
Expand Down
17 changes: 2 additions & 15 deletions doc/tutorials/01_quickstart.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,7 @@
"id": "9d8203e8",
"metadata": {},
"outputs": [],
"source": [
"tom, jerry = g.players\n",
"g.relabel_players({tom.label: \"Tom\", jerry.label: \"Jerry\"})\n",
"\n",
"for player in g.players:\n",
" cooperate, defect = player.strategies\n",
" g.relabel_strategies(player, {cooperate.label: \"Cooperate\", defect.label: \"Defect\"})"
]
"source": "tom, jerry = g.players\ng.relabel_players({tom.label: \"Tom\", jerry.label: \"Jerry\"})\n\nfor player in g.players:\n cooperate, defect = player.strategies\n g.relabel_strategies(player, {cooperate: \"Cooperate\", defect: \"Defect\"})"
},
{
"cell_type": "markdown",
Expand All @@ -103,13 +96,7 @@
"id": "61030607",
"metadata": {},
"outputs": [],
"source": [
"# Each contingency gets an outcome, created and attached in one step\n",
"g.make_outcome((\"Cooperate\", \"Cooperate\"), {\"Tom\": -1, \"Jerry\": -1}, \"Both cooperate\")\n",
"g.make_outcome((\"Cooperate\", \"Defect\"), {\"Tom\": -3, \"Jerry\": 0}, \"Tom cooperates, Jerry defects\")\n",
"g.make_outcome((\"Defect\", \"Cooperate\"), {\"Tom\": 0, \"Jerry\": -3}, \"Tom defects, Jerry cooperates\")\n",
"g.make_outcome((\"Defect\", \"Defect\"), {\"Tom\": -2, \"Jerry\": -2}, \"Both defect\")"
]
"source": "# Each contingency gets an outcome, created and attached in one step\ng.make_outcome(\n {\"Tom\": \"Cooperate\", \"Jerry\": \"Cooperate\"}, {\"Tom\": -1, \"Jerry\": -1}, \"Both cooperate\"\n)\ng.make_outcome(\n {\"Tom\": \"Cooperate\", \"Jerry\": \"Defect\"},\n {\"Tom\": -3, \"Jerry\": 0},\n \"Tom cooperates, Jerry defects\",\n)\ng.make_outcome(\n {\"Tom\": \"Defect\", \"Jerry\": \"Cooperate\"},\n {\"Tom\": 0, \"Jerry\": -3},\n \"Tom defects, Jerry cooperates\",\n)\ng.make_outcome(\n {\"Tom\": \"Defect\", \"Jerry\": \"Defect\"}, {\"Tom\": -2, \"Jerry\": -2}, \"Both defect\"\n)"
},
{
"cell_type": "code",
Expand Down
12 changes: 5 additions & 7 deletions doc/tutorials/03_stripped_down_poker.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@
"source": [
"The loop above causes each of the newly-appended moves to be in new information sets, reflecting the fact that Alice's decision depends on the knowledge of which card she holds.\n",
"\n",
"In contrast, Bob does not know Alice\u2019s card, and therefore cannot distinguish between the two nodes at which he has to make his decision:\n",
"In contrast, Bob does not know Alice’s card, and therefore cannot distinguish between the two nodes at which he has to make his decision:\n",
"\n",
" - Chance player chooses King, then Alice Bets: `g.root.children[\"King\"].children[\"Bet\"]`\n",
" - Chance player chooses Queen, then Alice Bets: `g.root.children[\"Queen\"].children[\"Bet\"]`\n",
Expand Down Expand Up @@ -396,7 +396,7 @@
"id": "1f121d48",
"metadata": {},
"source": [
"Now let's look at Bob\u2019s strategy:"
"Now let's look at Bob’s strategy:"
]
},
{
Expand All @@ -414,7 +414,7 @@
"id": "e906c4c4",
"metadata": {},
"source": [
"Bob Calls Alice\u2019s Bet two-thirds of the time.\n",
"Bob Calls Alice’s Bet two-thirds of the time.\n",
"\n",
"Since Bob has just one information set, we can get its representative node and index\n",
"the profile directly by it to read off a single action's probability:"
Expand Down Expand Up @@ -576,9 +576,7 @@
"id": "d4ecff88",
"metadata": {},
"outputs": [],
"source": [
"[s.label for s in g.players[\"Alice\"].strategies]"
]
"source": "list(g.players[\"Alice\"].strategies)"
},
{
"cell_type": "markdown",
Expand Down Expand Up @@ -637,7 +635,7 @@
"id": "56e2f847",
"metadata": {},
"outputs": [],
"source": "gnm_payoffs = gnm_eqm.payoffs\ngnm_strategy_values = gnm_eqm.strategy_values\nfor player in g.players:\n print(\n f\"{player.label}'s expected payoffs playing:\"\n )\n for strategy in player.strategies:\n print(\n f\"Strategy {strategy.label}: {gnm_strategy_values[player.label][strategy.label]:.4f}\"\n )\n print(\n f\"{player.label}'s overall expected payoff: {gnm_payoffs[player.label]:.4f}\"\n )\n print()"
"source": "gnm_payoffs = gnm_eqm.payoffs\ngnm_strategy_values = gnm_eqm.strategy_values\nfor player in g.players:\n print(\n f\"{player.label}'s expected payoffs playing:\"\n )\n for strategy in player.strategies:\n print(\n f\"Strategy {strategy}: {gnm_strategy_values[player.label][strategy]:.4f}\"\n )\n print(\n f\"{player.label}'s overall expected payoff: {gnm_payoffs[player.label]:.4f}\"\n )\n print()"
},
{
"cell_type": "markdown",
Expand Down
2 changes: 1 addition & 1 deletion doc/tutorials/interoperability_tutorials/gamut.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@
"id": "gamut-bos-gen",
"metadata": {},
"outputs": [],
"source": "g_chicken = gbt.catalog.generate_gamut(\n \"Chicken\",\n params={\n \"int_payoffs\": True,\n \"int_mult\": 1,\n \"normalize\": True,\n \"min_payoff\": 0,\n \"max_payoff\": 4,\n },\n gamut_jar=\"~/Downloads/gamut.jar\",\n)\ng_chicken.title = \"Chicken\"\nfor player in g_chicken.players:\n labels = {strategy.label: label\n for strategy, label in zip(player.strategies, [\"Swerve\", \"Straight\"], strict=True)}\n g_chicken.relabel_strategies(player, labels)\ng_chicken"
"source": "g_chicken = gbt.catalog.generate_gamut(\n \"Chicken\",\n params={\n \"int_payoffs\": True,\n \"int_mult\": 1,\n \"normalize\": True,\n \"min_payoff\": 0,\n \"max_payoff\": 4,\n },\n gamut_jar=\"~/Downloads/gamut.jar\",\n)\ng_chicken.title = \"Chicken\"\nfor player in g_chicken.players:\n labels = {strategy: label\n for strategy, label in zip(player.strategies, [\"Swerve\", \"Straight\"], strict=True)}\n g_chicken.relabel_strategies(player, labels)\ng_chicken"
},
{
"cell_type": "markdown",
Expand Down
15 changes: 2 additions & 13 deletions doc/tutorials/interoperability_tutorials/openspiel.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
"id": "b684325e",
"metadata": {},
"outputs": [],
"source": "gbt_matrix_rps_game = gbt.catalog.generate_openspiel(\"matrix_rps\")\n\ngbt_matrix_rps_game.title = \"Rock-Paper-Scissors\"\n\nfor player in gbt_matrix_rps_game.players:\n names = [\"Rock\", \"Paper\", \"Scissors\"]\n labels = {strategy.label: name\n for strategy, name in zip(player.strategies, names, strict=True)}\n gbt_matrix_rps_game.relabel_strategies(player, labels)\n\ngbt_matrix_rps_game"
"source": "gbt_matrix_rps_game = gbt.catalog.generate_openspiel(\"matrix_rps\")\n\ngbt_matrix_rps_game.title = \"Rock-Paper-Scissors\"\n\nfor player in gbt_matrix_rps_game.players:\n names = [\"Rock\", \"Paper\", \"Scissors\"]\n labels = {strategy: name\n for strategy, name in zip(player.strategies, names, strict=True)}\n gbt_matrix_rps_game.relabel_strategies(player, labels)\n\ngbt_matrix_rps_game"
},
{
"cell_type": "markdown",
Expand Down Expand Up @@ -343,18 +343,7 @@
"id": "fcd42af0",
"metadata": {},
"outputs": [],
"source": [
"p1_payoffs, p2_payoffs = gbt_prisoners_dilemma_game.to_arrays(dtype=float)\n",
"p1, p2 = gbt_prisoners_dilemma_game.players\n",
"ops_prisoners_dilemma_game = pyspiel.create_matrix_game(\n",
" gbt_prisoners_dilemma_game.title,\n",
" \"Classic Prisoner's Dilemma\", # description\n",
" [strategy.label for strategy in p1.strategies],\n",
" [strategy.label for strategy in p2.strategies],\n",
" p1_payoffs,\n",
" p2_payoffs\n",
")"
]
"source": "p1_payoffs, p2_payoffs = gbt_prisoners_dilemma_game.to_arrays(dtype=float)\np1, p2 = gbt_prisoners_dilemma_game.players\nops_prisoners_dilemma_game = pyspiel.create_matrix_game(\n gbt_prisoners_dilemma_game.title,\n \"Classic Prisoner's Dilemma\", # description\n list(p1.strategies),\n list(p2.strategies),\n p1_payoffs,\n p2_payoffs\n)"
},
{
"cell_type": "markdown",
Expand Down
3 changes: 2 additions & 1 deletion src/pygambit/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,8 @@ def check_filters(game: gbt.Game) -> bool:
return False
if n_players is not None and len(game.players) != n_players:
return False
return not (n_strategies is not None and len(game.strategies) != n_strategies)
total_strategies = sum(len(list(p.strategies)) for p in game.players)
return not (n_strategies is not None and total_strategies != n_strategies)

def append_record(
slug: str,
Expand Down
11 changes: 5 additions & 6 deletions src/pygambit/cli/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ def render_support_csv(
else:
fields = [
"".join(
"1" if strategy.label in support[player.label] else "0"
"1" if strategy in support[player.label] else "0"
for strategy in player.strategies
)
for player in support.game.players
Expand Down Expand Up @@ -271,10 +271,9 @@ def _render_strategy_detail(profile: gbt.MixedStrategyProfile, decimals: int) ->
probs = profile[player.label]
values = profile.strategy_values[player.label]
for strategy in player.strategies:
name = _name_or_number(strategy)
prob = format_value(probs[strategy.label], decimals)
value = format_value(values[strategy.label], decimals)
lines.append(f"{name:>8} {prob:>10} {value:>11}")
prob = format_value(probs[strategy], decimals)
value = format_value(values[strategy], decimals)
lines.append(f"{strategy:>8} {prob:>10} {value:>11}")
return "\n".join(lines)


Expand Down Expand Up @@ -338,7 +337,7 @@ def read_strategy_profiles_csv(
raise ValueError(f"Error reading strategy profile from '{path}': {exc}") from None
profile = game.mixed_strategy_profile(rational=True)
for player in game.players:
profile[player.label] = {s.label: next(values) for s in player.strategies}
profile[player.label] = {s: next(values) for s in player.strategies}
profiles.append(profile)
return profiles

Expand Down
2 changes: 1 addition & 1 deletion src/pygambit/cli/logit.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def _read_frequencies(path: str, game: gbt.Game) -> gbt.MixedStrategyProfileDoub
frequencies = game.mixed_strategy_profile(rational=False)
it = iter(values)
for player in game.players:
frequencies[player.label] = {s.label: next(it) for s in player.strategies}
frequencies[player.label] = {s: next(it) for s in player.strategies}
return frequencies


Expand Down
2 changes: 1 addition & 1 deletion src/pygambit/cli/simpdiv.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def _default_start(game: gbt.Game) -> gbt.MixedStrategyProfileRational:
start = game.mixed_strategy_profile(rational=True)
for player in game.players:
first_strategy = next(iter(player.strategies))
start[player.label] = {first_strategy.label: 1}
start[player.label] = {first_strategy: 1}
return start


Expand Down
1 change: 0 additions & 1 deletion src/pygambit/gambit.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ def _resolve_by_label(collection, label: str, scope: str, kind: str, kind_plural


PlayerReference = Player | str
StrategyReference = Strategy | str
InfosetReference = Infoset | str
ActionReference = Action | str
NodeReference = Node | str
Expand Down
Loading
Loading