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
6 changes: 6 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
is found or another solver event is emitted.
- Added `Game.make_outcome`, which creates an outcome with the given payoffs and attaches it to a
set of nodes (extensive) or contingencies (strategic) in a single operation. (#1061)
- Added `Game.make_outcome_null`, which resets a set of nodes (extensive) or contingencies
(strategic) to the null outcome, removing the previously-attached outcome if this was its
last reference. (#1061)
- Added `MixedStrategyProfile.as_float` and `MixedBehaviorProfile.as_float`, converting a
rational-precision profile to floating-point precision. `liap_solve`, `liap_agent_solve`,
`logit_estimate`, `ipa_solve`, and `gnm_solve` now also accept a rational-precision profile
Expand Down Expand Up @@ -68,6 +71,9 @@
LaTeX installation, so this lets the tutorials be run without that heavier local setup.

### Removed
- `Game.add_outcome`, `Game.delete_outcome`, and `Game.set_outcome` have been removed; use
`Game.make_outcome`/`Game.make_outcome_null`, which create-and-attach or reset an outcome in
a single operation and never leave an unattached outcome in the game. (#1061)
- `gtdraw` is no longer part of the `doc` optional-dependency group. Install it separately
(`pip install gtdraw`) to run tutorials locally or build the documentation.
- `Game.contingencies` now yields contingencies as a mapping from player label to strategy
Expand Down
6 changes: 2 additions & 4 deletions doc/pygambit.api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ Transforming game information structure
:toctree: api/

Game.make_infoset
Game.make_outcome
Game.make_event
Game.relabel_actions
Game.set_move_actions
Expand All @@ -87,11 +86,10 @@ Transforming game components

Game.relabel_players
Game.set_players
Game.add_outcome
Game.delete_outcome
Game.set_outcome
Game.relabel_strategies
Game.set_strategies
Game.make_outcome
Game.make_outcome_null


Information about the game
Expand Down
21 changes: 3 additions & 18 deletions doc/tutorials/02_extensive_form.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,7 @@
"id": "716e9b9a",
"metadata": {},
"outputs": [],
"source": [
"g.set_outcome(\n",
" g.root.children[\"Trust\"].children[\"Honor\"],\n",
" outcome=g.add_outcome(\"Trustworthy\", [1, 1])\n",
")"
]
"source": "g.make_outcome(\n g.root.children[\"Trust\"].children[\"Honor\"],\n {\"Buyer\": 1, \"Seller\": 1},\n \"Trustworthy\"\n)"
},
{
"cell_type": "code",
Expand All @@ -203,12 +198,7 @@
"id": "695b1aad",
"metadata": {},
"outputs": [],
"source": [
"g.set_outcome(\n",
" g.root.children[\"Trust\"].children[\"Abuse\"],\n",
" outcome=g.add_outcome(\"Untrustworthy\", [-1, 2])\n",
")"
]
"source": "g.make_outcome(\n g.root.children[\"Trust\"].children[\"Abuse\"],\n {\"Buyer\": -1, \"Seller\": 2},\n \"Untrustworthy\"\n)"
},
{
"cell_type": "code",
Expand All @@ -234,12 +224,7 @@
"id": "0704ef86",
"metadata": {},
"outputs": [],
"source": [
"g.set_outcome(\n",
" g.root.children[\"Not trust\"],\n",
" g.add_outcome(\"Opt-out\", [0, 0])\n",
")"
]
"source": "g.make_outcome(\n g.root.children[\"Not trust\"],\n {\"Buyer\": 0, \"Seller\": 0},\n \"Opt-out\"\n)"
},
{
"cell_type": "code",
Expand Down
52 changes: 5 additions & 47 deletions doc/tutorials/03_stripped_down_poker.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,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 @@ -207,57 +207,15 @@
"cell_type": "markdown",
"id": "c4eeb65f",
"metadata": {},
"source": [
"In game theory terms, this creates \"imperfect information\".\n",
"Bob cannot distinguish between these two nodes in the game tree, so he must use the same same probabilities for Call vs. Fold in both situations.\n",
"\n",
"This is crucial in games where players must make decisions without full knowledge of the state of the game.\n",
"\n",
"Let's now set up the four possible payoff outcomes for the game. We'll label them according to player 1 (Alice):"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "87c988be",
"metadata": {},
"outputs": [],
"source": [
"win_big = g.add_outcome(\"Win Big\", [2, -2])\n",
"win = g.add_outcome(\"Win\", [1, -1])\n",
"lose_big = g.add_outcome(\"Lose Big\", [-2, 2])\n",
"lose = g.add_outcome(\"Lose\", [-1, 1])"
]
},
{
"cell_type": "markdown",
"id": "467a2c39",
"metadata": {},
"source": [
"Finally, we should assign an outcome to each of the terminal nodes in the game tree:"
]
"source": "In game theory terms, this creates \"imperfect information\".\nBob cannot distinguish between these two nodes in the game tree, so he must use the same same probabilities for Call vs. Fold in both situations.\n\nThis is crucial in games where players must make decisions without full knowledge of the state of the game.\n\nLet's now set up the four possible payoff outcomes for the game, assigning each directly to the terminal node(s) it results at.\nWe'll label them according to player 1 (Alice):"
},
{
"cell_type": "code",
"execution_count": null,
"id": "29aa60a0",
"metadata": {},
"outputs": [],
"source": [
"# Alice folds, Bob wins small\n",
"g.set_outcome(g.root.children[\"King\"].children[\"Fold\"], lose)\n",
"g.set_outcome(g.root.children[\"Queen\"].children[\"Fold\"], lose)\n",
"\n",
"# Bob sees Alice Bet and calls, correctly believing she is bluffing, Bob wins big\n",
"g.set_outcome(g.root.children[\"Queen\"].children[\"Bet\"].children[\"Call\"], lose_big)\n",
"\n",
"# Bob sees Alice Bet and calls, incorrectly believing she is bluffing, Alice wins big\n",
"g.set_outcome(g.root.children[\"King\"].children[\"Bet\"].children[\"Call\"], win_big)\n",
"\n",
"# Bob does not call Alice's Bet, Alice wins small\n",
"g.set_outcome(g.root.children[\"King\"].children[\"Bet\"].children[\"Fold\"], win)\n",
"g.set_outcome(g.root.children[\"Queen\"].children[\"Bet\"].children[\"Fold\"], win)"
]
"source": "# Alice folds, Bob wins small\ng.make_outcome(\n [g.root.children[\"King\"].children[\"Fold\"], g.root.children[\"Queen\"].children[\"Fold\"]],\n {\"Alice\": -1, \"Bob\": 1},\n \"Lose\"\n)\n\n# Bob sees Alice Bet and calls, correctly believing she is bluffing, Bob wins big\ng.make_outcome(\n g.root.children[\"Queen\"].children[\"Bet\"].children[\"Call\"],\n {\"Alice\": -2, \"Bob\": 2},\n \"Lose Big\"\n)\n\n# Bob sees Alice Bet and calls, incorrectly believing she is bluffing, Alice wins big\ng.make_outcome(\n g.root.children[\"King\"].children[\"Bet\"].children[\"Call\"],\n {\"Alice\": 2, \"Bob\": -2},\n \"Win Big\"\n)\n\n# Bob does not call Alice's Bet, Alice wins small\ng.make_outcome(\n [g.root.children[\"King\"].children[\"Bet\"].children[\"Fold\"],\n g.root.children[\"Queen\"].children[\"Bet\"].children[\"Fold\"]],\n {\"Alice\": 1, \"Bob\": -1},\n \"Win\"\n)"
},
{
"cell_type": "code",
Expand Down Expand Up @@ -404,7 +362,7 @@
"id": "1f121d48",
"metadata": {},
"source": [
"Now let's look at Bob\u2019s strategy:"
"Now let's look at Bob’s strategy:"
]
},
{
Expand All @@ -422,7 +380,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
2 changes: 1 addition & 1 deletion doc/tutorials/interoperability_tutorials/openspiel.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@
"id": "77dc34c8",
"metadata": {},
"outputs": [],
"source": "gbt_one_card_poker = gbt.Game.new_tree(\n players=[\"Alice\", \"Bob\"],\n title=\"Stripped-Down Poker: a simple game of one-card poker from Reiley et al (2008).\"\n)\n\ngbt_one_card_poker.append_event(\n gbt_one_card_poker.root,\n actions=[\"King\", \"Queen\"],\n probs=[gbt.Rational(1, 2), gbt.Rational(1, 2)]\n)\n\nfor node in gbt_one_card_poker.root.children:\n gbt_one_card_poker.append_move(\n node,\n player=\"Alice\",\n actions=[\"Bet\", \"Fold\"]\n )\n\ngbt_one_card_poker.append_move(\n [\n gbt_one_card_poker.root.children[\"King\"].children[\"Bet\"],\n gbt_one_card_poker.root.children[\"Queen\"].children[\"Bet\"]\n ],\n player=\"Bob\",\n actions=[\"Call\", \"Fold\"]\n)\n\nwin_big = gbt_one_card_poker.add_outcome(\"Win Big\", [2, -2])\nwin = gbt_one_card_poker.add_outcome(\"Win\", [1, -1])\nlose_big = gbt_one_card_poker.add_outcome(\"Lose Big\", [-2, 2])\nlose = gbt_one_card_poker.add_outcome(\"Lose\", [-1, 1])\n\n# Alice folds, Bob wins small\ngbt_one_card_poker.set_outcome(\n gbt_one_card_poker.root.children[\"King\"].children[\"Fold\"],\n lose\n)\ngbt_one_card_poker.set_outcome(\n gbt_one_card_poker.root.children[\"Queen\"].children[\"Fold\"],\n lose\n)\n\n# Bob sees Alice Bet and calls, correctly believing she is bluffing, Bob wins big\ngbt_one_card_poker.set_outcome(\n gbt_one_card_poker.root.children[\"Queen\"].children[\"Bet\"].children[\"Call\"],\n lose_big\n)\n\n# Bob sees Alice Bet and calls, incorrectly believing she is bluffing, Alice wins big\ngbt_one_card_poker.set_outcome(\n gbt_one_card_poker.root.children[\"King\"].children[\"Bet\"].children[\"Call\"],\n win_big\n)\n\n# Bob does not call Alice's Bet, Alice wins small\ngbt_one_card_poker.set_outcome(\n gbt_one_card_poker.root.children[\"King\"].children[\"Bet\"].children[\"Fold\"],\n win\n)\ngbt_one_card_poker.set_outcome(\n gbt_one_card_poker.root.children[\"Queen\"].children[\"Bet\"].children[\"Fold\"],\n win\n)"
"source": "gbt_one_card_poker = gbt.Game.new_tree(\n players=[\"Alice\", \"Bob\"],\n title=\"Stripped-Down Poker: a simple game of one-card poker from Reiley et al (2008).\"\n)\n\ngbt_one_card_poker.append_event(\n gbt_one_card_poker.root,\n actions=[\"King\", \"Queen\"],\n probs=[gbt.Rational(1, 2), gbt.Rational(1, 2)]\n)\n\nfor node in gbt_one_card_poker.root.children:\n gbt_one_card_poker.append_move(\n node,\n player=\"Alice\",\n actions=[\"Bet\", \"Fold\"]\n )\n\ngbt_one_card_poker.append_move(\n [\n gbt_one_card_poker.root.children[\"King\"].children[\"Bet\"],\n gbt_one_card_poker.root.children[\"Queen\"].children[\"Bet\"]\n ],\n player=\"Bob\",\n actions=[\"Call\", \"Fold\"]\n)\n\n# Alice folds, Bob wins small\ngbt_one_card_poker.make_outcome(\n [\n gbt_one_card_poker.root.children[\"King\"].children[\"Fold\"],\n gbt_one_card_poker.root.children[\"Queen\"].children[\"Fold\"]\n ],\n {\"Alice\": -1, \"Bob\": 1},\n \"Lose\"\n)\n\n# Bob sees Alice Bet and calls, correctly believing she is bluffing, Bob wins big\ngbt_one_card_poker.make_outcome(\n gbt_one_card_poker.root.children[\"Queen\"].children[\"Bet\"].children[\"Call\"],\n {\"Alice\": -2, \"Bob\": 2},\n \"Lose Big\"\n)\n\n# Bob sees Alice Bet and calls, incorrectly believing she is bluffing, Alice wins big\ngbt_one_card_poker.make_outcome(\n gbt_one_card_poker.root.children[\"King\"].children[\"Bet\"].children[\"Call\"],\n {\"Alice\": 2, \"Bob\": -2},\n \"Win Big\"\n)\n\n# Bob does not call Alice's Bet, Alice wins small\ngbt_one_card_poker.make_outcome(\n [\n gbt_one_card_poker.root.children[\"King\"].children[\"Bet\"].children[\"Fold\"],\n gbt_one_card_poker.root.children[\"Queen\"].children[\"Bet\"].children[\"Fold\"]\n ],\n {\"Alice\": 1, \"Bob\": -1},\n \"Win\"\n)"
},
{
"cell_type": "code",
Expand Down
59 changes: 32 additions & 27 deletions src/games/file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -493,27 +493,31 @@ void ParseOutcomeBody(GameFileLexer &p_parser, Game &p_nfg)
}
}
NormalizeLabelStrings(labels);
std::map<int, GameOutcome> created;
auto label_it = labels.begin();
for (size_t i = 0; i < records.m_labels.size(); ++i) {
if (!referenced.contains(static_cast<int>(i) + 1)) {
continue;
}
auto outcome = p_nfg->NewOutcome(*label_it++);
auto player_it = p_nfg->GetPlayers().begin();
for (const auto &payoff : records.m_payoffs[i]) {
outcome->SetPayoff(*player_it, payoff);
++player_it;
std::map<int, std::string> labelById;
{
auto label_it = labels.begin();
for (size_t i = 0; i < records.m_labels.size(); ++i) {
if (referenced.contains(static_cast<int>(i) + 1)) {
labelById.emplace(static_cast<int>(i) + 1, *label_it++);
}
}
created.emplace(static_cast<int>(i) + 1, outcome);
}
// Second pass: attach.
// Second pass: group each referenced contingency by the outcome id attached to it.
std::map<int, std::vector<std::vector<GameStrategy>>> contingenciesById;
auto id_it = ids.begin();
for (const auto &profile : StrategyContingencies(p_nfg)) {
if (const int outcomeId = *id_it++) {
profile->SetOutcome(created.at(outcomeId));
std::vector<GameStrategy> strategies;
strategies.reserve(p_nfg->NumPlayers());
for (const auto &player : p_nfg->GetPlayers()) {
strategies.push_back(profile->GetStrategy(player));
}
contingenciesById[outcomeId].push_back(strategies);
}
}
for (const auto &[outcomeId, contingencies] : contingenciesById) {
p_nfg->MakeOutcome(contingencies, records.m_payoffs[outcomeId - 1], labelById.at(outcomeId));
}
}

void ParsePayoffBody(GameFileLexer &p_parser, Game &p_nfg)
Expand Down Expand Up @@ -580,7 +584,7 @@ Game BuildNfg(GameFileLexer &p_parser, TableFileGame &p_data)

/// An outcome definition encountered during the parse. Outcomes are not
/// created until the whole tree has been read, so that their labels can be
/// normalized in one pass before creation, as NewOutcome enforces
/// normalized in one pass before creation, as MakeOutcome enforces
/// the label requirements at creation time.
struct OutcomeRecord {
std::string m_label;
Expand Down Expand Up @@ -688,7 +692,7 @@ void ParseOutcome(GameFileLexer &p_state, Game &p_game, TreeData &p_treeData, Ga

/// Create the game's outcomes from the definitions buffered during the parse.
/// Labels are normalized in first-occurrence order before creation, so that
/// the label requirements enforced by NewOutcome (nonempty, unique) are
/// the label requirements enforced by MakeOutcome (nonempty, unique) are
/// satisfied; this matches the treatment of outcome labels read from .nfg
/// files, and produces the same labels the previous post-parse normalization
/// pass produced.
Expand All @@ -699,20 +703,21 @@ void CreateOutcomes(const Game &p_game, const TreeData &p_treeData)
labels.push_back(p_treeData.m_outcomeRecords.at(id).m_label);
}
NormalizeLabelStrings(labels);

std::map<int, GameOutcome> created;
auto label_it = labels.begin();
for (const int id : p_treeData.m_outcomeOrder) {
auto outcome = p_game->NewOutcome(*label_it++);
auto player_it = p_game->GetPlayers().begin();
for (const auto &payoff : p_treeData.m_outcomeRecords.at(id).m_payoffs) {
outcome->SetPayoff(*player_it, payoff);
++player_it;
std::map<int, std::string> labelById;
{
auto label_it = labels.begin();
for (const int id : p_treeData.m_outcomeOrder) {
labelById.emplace(id, *label_it++);
}
created.emplace(id, outcome);
}

std::map<int, std::vector<GameNode>> nodesById;
for (const auto &[node, id] : p_treeData.m_nodeOutcomes) {
p_game->SetOutcome(node, created.at(id));
nodesById[id].push_back(node);
}
for (const int id : p_treeData.m_outcomeOrder) {
p_game->MakeOutcome(nodesById.at(id), p_treeData.m_outcomeRecords.at(id).m_payoffs,
labelById.at(id));
}
}

Expand Down
18 changes: 7 additions & 11 deletions src/games/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -681,8 +681,6 @@ class GameNodeRep : public std::enable_shared_from_this<GameNodeRep> {
GameOutcomeRep *m_outcome;
std::vector<std::shared_ptr<GameNodeRep>> m_children;

void DeleteOutcome(GameOutcomeRep *outc);

public:
using Children = ElementCollection<GameNode, GameNodeRep>;

Expand Down Expand Up @@ -1337,11 +1335,6 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
{
throw UndefinedException();
}
virtual void SetOutcome(const GameNode &p_node, const GameOutcome &p_outcome)
{
throw UndefinedException();
}

virtual PureStrategyProfile NewPureStrategyProfile() const = 0;
virtual MixedStrategyProfile<double> NewMixedStrategyProfile(double) const = 0;
virtual MixedStrategyProfile<Rational> NewMixedStrategyProfile(const Rational &) const = 0;
Expand Down Expand Up @@ -1434,8 +1427,6 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
{
return Outcomes(std::const_pointer_cast<GameRep>(shared_from_this()), &m_outcomes);
}
/// Creates a new outcome in the game
virtual GameOutcome NewOutcome(const std::string &p_label) { throw UndefinedException(); }
/// Creates an outcome with the given payoffs and label for the specified nodes.
virtual GameOutcome MakeOutcome(const std::vector<GameNode> &, const std::vector<Number> &,
const std::string &)
Expand All @@ -1448,8 +1439,13 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
{
throw UndefinedException();
}
/// Deletes the specified outcome from the game
virtual void DeleteOutcome(const GameOutcome &) { throw UndefinedException(); }
/// Resets the outcome at the specified nodes to the null outcome.
virtual void MakeOutcomeNull(const std::vector<GameNode> &) { throw UndefinedException(); }
/// Resets the outcome at the specified contingencies to the null outcome.
virtual void MakeOutcomeNull(const std::vector<std::vector<GameStrategy>> &)
{
throw UndefinedException();
}
//@}

/// @name Nodes
Expand Down
1 change: 0 additions & 1 deletion src/games/gameagg.cc
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ class AGGPureStrategyProfileRep : public PureStrategyProfileRep {
}

GameOutcome GetOutcome() const override { throw UndefinedException(); }
void SetOutcome(GameOutcome p_outcome) override { throw UndefinedException(); }
Rational GetPayoff(const GamePlayer &) const override;
Rational GetStrategyValue(const GameStrategy &) const override;
};
Expand Down
1 change: 0 additions & 1 deletion src/games/gamebagg.cc
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ class BAGGPureStrategyProfileRep : public PureStrategyProfileRep {
return std::make_shared<BAGGPureStrategyProfileRep>(*this);
}
GameOutcome GetOutcome() const override { throw UndefinedException(); }
void SetOutcome(GameOutcome p_outcome) override { throw UndefinedException(); }
Rational GetPayoff(const GamePlayer &) const override;
Rational GetStrategyValue(const GameStrategy &) const override;
};
Expand Down
11 changes: 0 additions & 11 deletions src/games/gameexpl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,6 @@ Rational GameExplicitRep::GetMaxPayoff() const
});
}

//------------------------------------------------------------------------
// GameExplicitRep: Outcomes
//------------------------------------------------------------------------

GameOutcome GameExplicitRep::NewOutcome(const std::string &p_label)
{
CheckOutcomeLabel(p_label);
m_outcomes.push_back(std::make_shared<GameOutcomeRep>(this, m_outcomes.size() + 1, p_label));
return m_outcomes.back();
}

//------------------------------------------------------------------------
// GameExplicitRep: Writing data files
//------------------------------------------------------------------------
Expand Down
5 changes: 0 additions & 5 deletions src/games/gameexpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ class GameExplicitRep : public GameRep {
Rational GetMaxPayoff() const override;
//@}

/// @name Outcomes
//@{
/// Creates a new outcome in the game
GameOutcome NewOutcome(const std::string &p_label) override;

/// @name Writing data files
//@{
void Write(std::ostream &p_stream, const std::string &p_format = "native") const override;
Expand Down
Loading
Loading