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
29 changes: 29 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,35 @@
### Added
- Added `Game.has_perfect_recall(player)`, and `GameRep::HasPerfectRecall(const GamePlayer &)`
in C++, reporting whether an individual player has perfect recall. (#1107)
- Added `Game.get_outcomes()`, returning a materialized list of outcome labels, replacing
`Game.outcomes`.
- Added `Game.get_outcome_payoffs(label)`, returning the payoff to each player at the outcome
labeled `label`.
- Added `Game.set_outcome_payoffs(label, payoffs)`, setting the payoffs at the outcome labeled
`label`; `payoffs` must be a complete mapping over the game's players, as with
`Game.make_outcome`.
- Added `Game.relabel_outcomes(labels, strict=True)`, simultaneously reassigning the labels of
the game's outcomes, following the same pattern as `Game.relabel_players`/
`Game.relabel_strategies`. Added `GameRep::RelabelOutcomes` in C++.

### Changed
- `Game.make_outcome` now returns `None` instead of the `Outcome` created; use the `label`
already passed in, together with `Game.get_outcome_payoffs`/`Game.relabel_outcomes`, to refer
to it afterward.

### Removed
- `Outcome` has been removed as a public-facing class in pygambit, following `Action`,
`Infoset`, `Player`, `Strategy`, and `Node`: no public method holds or returns a live handle
into a game's internal outcome list any longer. `Game.outcomes` has been removed; use
`Game.get_outcomes()`, `Game.get_outcome_payoffs()`, `Game.set_outcome_payoffs()`, and
`Game.relabel_outcomes()` instead.

### Fixed
- `GameTableRep`'s constructor (used by `Game.from_arrays`/`Game.from_dict`, which build a
non-sparse table with one outcome per contingency) gave every outcome the same empty label,
violating the invariant that every non-null outcome has a unique, nonempty label. Outcomes
are now labeled `"1"`, `"2"`, and so on, following the existing convention for default
player/strategy labels.

## [17.0.0-alpha.3] - 2026-08-31

Expand Down
13 changes: 4 additions & 9 deletions doc/pygambit.api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ Representation of games
:toctree: api/

Game
Outcome
Node
TreeLayout
TreeLayoutCoordinates
Expand Down Expand Up @@ -95,6 +94,8 @@ Transforming game components
Game.set_strategies
Game.make_outcome
Game.make_outcome_null
Game.relabel_outcomes
Game.set_outcome_payoffs


Information about the game
Expand All @@ -110,7 +111,6 @@ Information about the game
Game.is_perfect_recall
Game.has_perfect_recall
Game.players
Game.outcomes
Game.min_payoff
Game.max_payoff
Game.get_min_payoff
Expand All @@ -121,18 +121,13 @@ Information about the game
Game.get_sequences
Game.contingencies
Game.get_outcome
Game.get_outcomes
Game.get_outcome_payoffs
Game.get_payoffs
Game.get_subgame_roots
Game.get_minimal_subgame
Game.get_strategy_unreachable

.. autosummary::
:toctree: api/

Outcome.label
Outcome.number
Outcome.game

.. autosummary::
:toctree: api/

Expand Down
8 changes: 1 addition & 7 deletions doc/tutorials/02_extensive_form.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,7 @@
"cell_type": "markdown",
"id": "382ba37d",
"metadata": {},
"source": [
"Now that we have the moves of the game defined, we add payoffs.\n",
"\n",
"Payoffs are associated with an `Outcome`; each `Outcome` has a vector of payoffs, one for each player, and optionally an identifying text label.\n",
"\n",
"First we add the outcome associated with the Seller proving themselves trustworthy:"
]
"source": "Now that we have the moves of the game defined, we add payoffs.\n\nPayoffs are associated with an outcome, identified by a text label; each outcome has a vector of payoffs, one for each player.\n\nFirst we add the outcome associated with the Seller proving themselves trustworthy:"
},
{
"cell_type": "code",
Expand Down
11 changes: 1 addition & 10 deletions doc/tutorials/03_stripped_down_poker.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -851,16 +851,7 @@
"id": "a892dc2b",
"metadata": {},
"outputs": [],
"source": [
"for outcome in g.outcomes:\n",
" outcome[\"Alice\"] = outcome[\"Alice\"] * 2\n",
" outcome[\"Bob\"] = outcome[\"Bob\"] * 2\n",
"\n",
"(\n",
" gbt.nash.liap_solve(g.mixed_strategy_profile(), maxregret=1e-1)\n",
" .equilibria[0].max_regret() / (g.max_payoff - g.min_payoff)\n",
")"
]
"source": "for label in g.get_outcomes():\n payoffs = g.get_outcome_payoffs(label)\n g.set_outcome_payoffs(label, {player: value * 2 for player, value in payoffs})\n\n(\n gbt.nash.liap_solve(g.mixed_strategy_profile(), maxregret=1e-1)\n .equilibria[0].max_regret() / (g.max_payoff - g.min_payoff)\n)"
},
{
"cell_type": "markdown",
Expand Down
26 changes: 1 addition & 25 deletions doc/tutorials/advanced_tutorials/h_selector_prototype.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -198,31 +198,7 @@
}
},
"outputs": [],
"source": [
"def winner(h):\n",
" match h[2:]:\n",
" case (\"Check\", \"Check\") | (\"Check\", \"Bet\", \"Call\") | (\"Bet\", \"Call\"):\n",
" return \"Alice\" if CARD_VALUE[h[0]] > CARD_VALUE[h[1]] else \"Bob\"\n",
" case (\"Check\", \"Bet\", \"Fold\"):\n",
" return \"Bob\"\n",
" case (\"Bet\", \"Fold\"):\n",
" return \"Alice\"\n",
"\n",
"def pot_size(h):\n",
" match h[2:]:\n",
" case (\"Check\", \"Check\") | (\"Check\", \"Bet\", \"Fold\") | (\"Bet\", \"Fold\"):\n",
" return 1\n",
" case (\"Check\", \"Bet\", \"Call\") | (\"Bet\", \"Call\"):\n",
" return 2\n",
"\n",
"for (win, amount), group in g._get_groups(H.plays.by(lambda h: (winner(h), pot_size(h)))).items():\n",
" lose = \"Bob\" if win == \"Alice\" else \"Alice\"\n",
" g.make_outcome(\n",
" selector_for_histories(group), {win: amount, lose: -amount}, f\"{win} wins {amount}\"\n",
" )\n",
"\n",
"print(\"Total outcomes created:\", len(list(g.outcomes)))"
]
"source": "def winner(h):\n match h[2:]:\n case (\"Check\", \"Check\") | (\"Check\", \"Bet\", \"Call\") | (\"Bet\", \"Call\"):\n return \"Alice\" if CARD_VALUE[h[0]] > CARD_VALUE[h[1]] else \"Bob\"\n case (\"Check\", \"Bet\", \"Fold\"):\n return \"Bob\"\n case (\"Bet\", \"Fold\"):\n return \"Alice\"\n\ndef pot_size(h):\n match h[2:]:\n case (\"Check\", \"Check\") | (\"Check\", \"Bet\", \"Fold\") | (\"Bet\", \"Fold\"):\n return 1\n case (\"Check\", \"Bet\", \"Call\") | (\"Bet\", \"Call\"):\n return 2\n\nfor (win, amount), group in g._get_groups(H.plays.by(lambda h: (winner(h), pot_size(h)))).items():\n lose = \"Bob\" if win == \"Alice\" else \"Alice\"\n g.make_outcome(\n selector_for_histories(group), {win: amount, lose: -amount}, f\"{win} wins {amount}\"\n )\n\nprint(\"Total outcomes created:\", len(g.get_outcomes()))"
},
{
"cell_type": "markdown",
Expand Down
39 changes: 39 additions & 0 deletions src/games/game.cc
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,45 @@ void GameRep::RelabelPlayers(const std::map<std::string, std::string> &p_labels)
}
}

//------------------------------------------------------------------------
// GameRep: Outcomes
//------------------------------------------------------------------------

void GameRep::RelabelOutcomes(const std::map<std::string, std::string> &p_labels)
{
// Resolve each key to exactly one outcome of the game.
std::map<GameOutcomeRep *, std::string> assignment;
std::set<const GameOutcomeRep *> relabeled;
for (const auto &[old_label, new_label] : p_labels) {
GameOutcomeRep *match = nullptr;
for (const auto &outcome : m_outcomes) {
if (outcome->GetLabel() == old_label) {
if (match) {
throw ValueException("Outcome label '" + old_label + "' is ambiguous in this game");
}
match = outcome.get();
}
}
if (!match) {
throw ValueException("No outcome with label '" + old_label + "' in this game");
}
assignment[match] = new_label;
relabeled.insert(match);
}
// Replacement labels must be legal, unique against untouched outcomes, and pairwise distinct
std::set<std::string> targets;
for (const auto &[outcome, new_label] : assignment) {
CheckOutcomeLabel(new_label, relabeled);
if (!targets.insert(new_label).second) {
throw ValueException("Outcome label '" + new_label +
"' would be duplicated by the relabelling");
}
}
for (const auto &[outcome, new_label] : assignment) {
outcome->m_label = new_label;
}
}

//========================================================================
// MixedStrategyProfileRep<T>
//========================================================================
Expand Down
3 changes: 3 additions & 0 deletions src/games/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -1473,6 +1473,9 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
{
throw UndefinedException();
}
/// Reassign outcome labels. Keys of p_labels are current labels; values are their
/// replacements.
void RelabelOutcomes(const std::map<std::string, std::string> &p_labels);
//@}

/// @name Nodes
Expand Down
3 changes: 2 additions & 1 deletion src/games/gametable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,8 @@ GameTableRep::GameTableRep(const std::vector<int> &dim, bool p_sparseOutcomes /*
else {
m_outcomes = std::vector<std::shared_ptr<GameOutcomeRep>>(m_results.size());
std::generate(m_outcomes.begin(), m_outcomes.end(), [this, outc = 1]() mutable {
return std::make_shared<GameOutcomeRep>(this, outc++, "");
const auto number = outc++;
return std::make_shared<GameOutcomeRep>(this, number, std::to_string(number));
});
std::transform(m_outcomes.begin(), m_outcomes.end(), m_results.begin(),
[](const std::shared_ptr<GameOutcomeRep> &c) { return c.get(); });
Expand Down
2 changes: 1 addition & 1 deletion src/pygambit/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ def check_filters(game: gbt.Game) -> bool:
return False
if len(game.get_histories(gbt.H.after())) != n_nodes:
return False
if n_outcomes is not None and len(game.outcomes) != n_outcomes:
if n_outcomes is not None and len(game.get_outcomes()) != n_outcomes:
return False
if n_players is not None and len(game.players) != n_players:
return False
Expand Down
1 change: 1 addition & 0 deletions src/pygambit/gambit.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ cdef extern from "games/game.h":
int NumOutcomes() except +
c_GameOutcome GetOutcome(int) except +IndexError
Outcomes GetOutcomes() except +
void RelabelOutcomes(stdmap[string, string]) except +ValueError

int NumNodes() except +
c_GameNode GetRoot() except +
Expand Down
1 change: 0 additions & 1 deletion src/pygambit/gambit.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ class NodeIndexedVector(_LabeledVector):
######################

include "strategy.pxi"
include "outcome.pxi"
include "node.pxi"
include "hsel.pxi"
include "stratspt.pxi"
Expand Down
Loading
Loading