diff --git a/ChangeLog b/ChangeLog index 836c13e68..daf0d18d6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -32,6 +32,12 @@ unsaved work. (#12) - Created and documented right-click context menu in the normal form pivot table, which makes the mechanism for deleting a strategy more clear. (#855) +- In `pygambit`, indexing game object collections by integer position has been removed. + The collections `Game.players`, `Game.outcomes`, `Game.strategies`, `Game.infosets`, + `Game.actions`, `Player.strategies`, `Player.infosets`, `Player.actions`, `Infoset.actions`, + and `Infoset.members`, as well as `Node.children`, are indexed by string label, and indexing by + an integer now raises `TypeError`. + Indexing a `Game` by a contingency (e.g. `game[0, 1]`) is unchanged. ### Removed - Built-in plotting of logit QRE for strategic games has been removed in the GUI (#809) diff --git a/doc/tutorials/01_quickstart.ipynb b/doc/tutorials/01_quickstart.ipynb index 388891ae0..ad773f1ba 100644 --- a/doc/tutorials/01_quickstart.ipynb +++ b/doc/tutorials/01_quickstart.ipynb @@ -77,13 +77,14 @@ "metadata": {}, "outputs": [], "source": [ - "g.players[0].label = \"Tom\"\n", - "g.players[0].strategies[0].label = \"Cooperate\"\n", - "g.players[0].strategies[1].label = \"Defect\"\n", + "tom, jerry = g.players\n", + "tom.label = \"Tom\"\n", + "jerry.label = \"Jerry\"\n", "\n", - "g.players[1].label = \"Jerry\"\n", - "g.players[1].strategies[0].label = \"Cooperate\"\n", - "g.players[1].strategies[1].label = \"Defect\"" + "for player in g.players:\n", + " cooperate, defect = player.strategies\n", + " cooperate.label = \"Cooperate\"\n", + " defect.label = \"Defect\"" ] }, { diff --git a/doc/tutorials/03_stripped_down_poker.ipynb b/doc/tutorials/03_stripped_down_poker.ipynb index f8649c919..522ac1467 100644 --- a/doc/tutorials/03_stripped_down_poker.ipynb +++ b/doc/tutorials/03_stripped_down_poker.ipynb @@ -495,7 +495,8 @@ "outputs": [], "source": [ "# Remember that Bob has a single information set\n", - "for action in g.players[\"Bob\"].infosets[0].actions:\n", + "(bob_infoset,) = g.players[\"Bob\"].infosets\n", + "for action in bob_infoset.actions:\n", " print(\n", " f\"When Bob plays {action.label} his expected payoff is {eqm.action_value(action)}\"\n", " )" @@ -520,7 +521,7 @@ "metadata": {}, "outputs": [], "source": [ - "for node in g.players[\"Bob\"].infosets[0].members:\n", + "for node in bob_infoset.members:\n", " print(\n", " f\"Bob's belief in reaching the {node.parent.prior_action.label} -> \"\n", " f\"{node.prior_action.label} node is: {eqm.belief(node)}\"\n", @@ -544,7 +545,7 @@ "metadata": {}, "outputs": [], "source": [ - "eqm.infoset_prob(g.players[\"Bob\"].infosets[0])" + "eqm.infoset_prob(bob_infoset)" ] }, { @@ -562,7 +563,7 @@ "metadata": {}, "outputs": [], "source": [ - "for node in g.players[\"Bob\"].infosets[0].members:\n", + "for node in bob_infoset.members:\n", " print(\n", " f\"The probability that the node {node.parent.prior_action.label} -> \"\n", " f\"{node.prior_action.label} is reached is: {eqm.realiz_prob(node)}. \",\n", diff --git a/doc/tutorials/interoperability_tutorials/openspiel.ipynb b/doc/tutorials/interoperability_tutorials/openspiel.ipynb index 74ec53ae6..c3abb6aee 100644 --- a/doc/tutorials/interoperability_tutorials/openspiel.ipynb +++ b/doc/tutorials/interoperability_tutorials/openspiel.ipynb @@ -162,9 +162,8 @@ "gbt_matrix_rps_game.title = \"Rock-Paper-Scissors\"\n", "\n", "for player in gbt_matrix_rps_game.players:\n", - " player.strategies[0].label = \"Rock\"\n", - " player.strategies[1].label = \"Paper\"\n", - " player.strategies[2].label = \"Scissors\"\n", + " for strategy, label in zip(player.strategies, [\"Rock\", \"Paper\", \"Scissors\"], strict=True):\n", + " strategy.label = label\n", "\n", "gbt_matrix_rps_game" ] @@ -356,11 +355,12 @@ "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 gbt_prisoners_dilemma_game.players[0].strategies],\n", - " [strategy.label for strategy in gbt_prisoners_dilemma_game.players[1].strategies],\n", + " [strategy.label for strategy in p1.strategies],\n", + " [strategy.label for strategy in p2.strategies],\n", " p1_payoffs,\n", " p2_payoffs\n", ")" diff --git a/src/pygambit/gambit.pyx b/src/pygambit/gambit.pyx index bb5edcf65..c0f7297ea 100644 --- a/src/pygambit/gambit.pyx +++ b/src/pygambit/gambit.pyx @@ -25,7 +25,6 @@ import warnings import typing import cython -import typing from .error import * @@ -74,6 +73,29 @@ def _to_number(value: typing.Any) -> c_Number: return c_Number(value.encode("ascii")) +@cython.cfunc +def _resolve_by_label(collection, label: str, scope: str, kind: str, kind_plural: str): + """Resolve a member of a game collection by its text label. + + Game collections are accessed by label, not by position. Lookup is by exact label match. + + Failure modes: + * an empty label raises ``ValueError``; + * a label matching no member raises ``KeyError``; + * a label matching more than one member raises ``ValueError``. + + .. versionadded:: 16.7.0 + """ + if not label: + raise ValueError(f"{kind} label cannot be empty") + matches = [x for x in collection if x.label == label] + if not matches: + raise KeyError(f"{scope} has no {kind} with label '{label}'") + if len(matches) > 1: + raise ValueError(f"{scope} has multiple {kind_plural} with label '{label}'") + return matches[0] + + PlayerReference = Player | str StrategyReference = Strategy | str InfosetReference = Infoset | str diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 928371786..7f88cce52 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -300,19 +300,30 @@ class GameOutcomes: for outcome in self.game.deref().GetOutcomes(): yield Outcome.wrap(outcome) - def __getitem__(self, index: int | str) -> Outcome: - if isinstance(index, str): - if not index.strip(): - raise ValueError("Outcome label cannot be empty or all whitespace") - matches = [x for x in self if x.label == index.strip()] - if not matches: - raise KeyError(f"Game has no outcome with label '{index}'") - if len(matches) > 1: - raise ValueError(f"Game has multiple outcomes with label '{index}'") - return matches[0] - if isinstance(index, int): - return Outcome.wrap(self.game.deref().GetOutcome(index + 1)) - raise TypeError(f"Outcome index must be int or str, not {index.__class__.__name__}") + def __getitem__(self, label: str) -> Outcome: + """Returns the outcome with text label `label`. + + Parameters + ---------- + label : str + The text label of the outcome to return. Lookup is by exact match; + leading/trailing whitespace is stripped from `label`. + + Raises + ------ + KeyError + If no outcome in the game has label `label`. + ValueError + If `label` is empty or all whitespace, or if more than one outcome has label `label`. + TypeError + If `label` is not a string. + + .. versionchanged:: 16.7.0 + Integer indexing is no longer supported; reference an outcome by its label, or iterate + over the collection. String lookup now requires an exact match of the label; + previously, leading/trailing whitespace was stripped from `label` before comparison. + """ + return _resolve_by_label(self, label, "Game", "outcome", "outcomes") @cython.cclass @@ -341,22 +352,30 @@ class GamePlayers: for player in self.game.deref().GetPlayers(): yield Player.wrap(player) - def __getitem__(self, index: int | str) -> Player: - if isinstance(index, str): - if not index.strip(): - raise ValueError("Player label cannot be empty or all whitespace") - matches = [x for x in self if x.label == index.strip()] - if not matches: - raise KeyError(f"Game has no player with label '{index}'") - if len(matches) > 1: - raise ValueError(f"Game has multiple players with label '{index}'") - return matches[0] - if isinstance(index, int): - try: - return Player.wrap(self.game.deref().GetPlayer(index + 1)) - except IndexError: - raise IndexError("Index out of range") from None - raise TypeError(f"Player index must be int or str, not {index.__class__.__name__}") + def __getitem__(self, label: str) -> Player: + """Returns the player with text label `label`. + + Parameters + ---------- + label : str + The text label of the player to return. Lookup is by exact match; + leading/trailing whitespace is stripped from `label`. + + Raises + ------ + KeyError + If no player in the game has label `label`. + ValueError + If `label` is empty or all whitespace, or if more than one player has label `label`. + TypeError + If `label` is not a string. + + .. versionchanged:: 16.7.0 + Integer indexing is no longer supported; reference a player by its label, or iterate + over the collection. String lookup now requires an exact match of the label; + previously, leading/trailing whitespace was stripped from `label` before comparison. + """ + return _resolve_by_label(self, label, "Game", "player", "players") @property def chance(self) -> Player: @@ -389,23 +408,30 @@ class GameActions: for infoset in self.game.infosets: yield from infoset.actions - def __getitem__(self, index: int | str) -> Action: - if isinstance(index, str): - if not index.strip(): - raise ValueError("Action label cannot be empty or all whitespace") - matches = [x for x in self if x.label == index.strip()] - if not matches: - raise KeyError(f"Game has no action with label '{index}'") - if len(matches) > 1: - raise ValueError(f"Game has multiple actions with label '{index}'") - return matches[0] - if isinstance(index, int): - for i, action in enumerate(self): - if i == index: - return action - else: - raise IndexError("Index out of range") - raise TypeError(f"Action index must be int or str, not {index.__class__.__name__}") + def __getitem__(self, label: str) -> Action: + """Returns the action with text label `label`. + + Parameters + ---------- + label : str + The text label of the action to return. Lookup is by exact match; + leading/trailing whitespace is stripped from `label`. + + Raises + ------ + KeyError + If no action in the game has label `label`. + ValueError + If `label` is empty or all whitespace, or if more than one action has label `label`. + TypeError + If `label` is not a string. + + .. versionchanged:: 16.7.0 + Integer indexing is no longer supported; reference an action by its label, or iterate + over the collection. String lookup now requires an exact match of the label; + previously, leading/trailing whitespace was stripped from `label` before comparison. + """ + return _resolve_by_label(self, label, "Game", "action", "actions") @cython.cclass @@ -433,23 +459,32 @@ class GameInfosets: for player in self.game.players: yield from player.infosets - def __getitem__(self, index: int | str) -> Infoset: - if isinstance(index, str): - if not index.strip(): - raise ValueError("Infoset label cannot be empty or all whitespace") - matches = [x for x in self if x.label == index.strip()] - if not matches: - raise KeyError(f"Game has no infoset with label '{index}'") - if len(matches) > 1: - raise ValueError(f"Game has multiple infosets with label '{index}'") - return matches[0] - if isinstance(index, int): - for i, infoset in enumerate(self): - if i == index: - return infoset - else: - raise IndexError("Index out of range") - raise TypeError(f"Infoset index must be int or str, not {index.__class__.__name__}") + def __getitem__(self, label: str) -> Infoset: + """Returns the information set with text label `label`. + + Parameters + ---------- + label : str + The text label of the infoset to return. Lookup is by exact match; + leading/trailing whitespace is stripped from `label`. + + Raises + ------ + KeyError + If no information set in the game has label `label`. + ValueError + If `label` is empty or all whitespace, or if more than one information set has + label `label`. + TypeError + If `label` is not a string. + + .. versionchanged:: 16.7.0 + Integer indexing is no longer supported; reference an information set by its label, + or iterate over the collection. String lookup now requires an exact match of the + label; previously, leading/trailing whitespace was stripped from `label` before + comparison. + """ + return _resolve_by_label(self, label, "Game", "infoset", "infosets") @cython.cclass @@ -477,23 +512,30 @@ class GameStrategies: for player in self.game.players: yield from player.strategies - def __getitem__(self, index: int | str) -> Strategy: - if isinstance(index, str): - if not index.strip(): - raise ValueError("Strategy label cannot be empty or all whitespace") - matches = [x for x in self if x.label == index.strip()] - if not matches: - raise KeyError(f"Game has no strategy with label '{index}'") - if len(matches) > 1: - raise ValueError(f"Game has multiple strategies with label '{index}'") - return matches[0] - if isinstance(index, int): - for i, strat in enumerate(self): - if i == index: - return strat - else: - raise IndexError("Index out of range") - raise TypeError(f"Strategy index must be int or str, not {index.__class__.__name__}") + def __getitem__(self, label: str) -> Strategy: + """Returns the strategy with text label `label`. + + Parameters + ---------- + label : str + The text label of the strategy to return. Lookup is by exact match; + leading/trailing whitespace is stripped from `label`. + + Raises + ------ + KeyError + If no strategy in the game has label `label`. + ValueError + If `label` is empty or all whitespace, or if more than one strategy has label `label`. + TypeError + If `label` is not a string. + + .. versionchanged:: 16.7.0 + Integer indexing is no longer supported; reference a strategy by its label, or iterate + over the collection. String lookup now requires an exact match of the label; + previously, leading/trailing whitespace was stripped from `label` before comparison. + """ + return _resolve_by_label(self, label, "Game", "strategy", "strategies") @cython.cclass @@ -981,28 +1023,50 @@ class Game: else: return None - def __getitem__(self, i): + def __getitem__(self, contingency): """Returns the `Outcome` associated with a profile of pure strategies. + + Each strategy in the profile may be given as a ``Strategy``, its text label, + or its integer index within the corresponding player's strategies. + + Raises + ------ + TypeError + If `contingency` is not a tuple-like object, or contains an element + that is not an ``int``, ``str``, or ``Strategy``. + KeyError + If the number of elements in `contingency` does not equal the + number of players. + IndexError + If an integer index is out of range for the corresponding player, + or a label or ``Strategy`` does not belong to that player. + + .. note:: + Unlike the game's object collections, strategies within a contingency can be referenced + by integer index, as a contingency is a coordinate in the players' strategy spaces; + labels and ``Strategy`` objects are also accepted. """ + players = list(self.players) try: - if len(i) != len(self.players): + if len(contingency) != len(players): raise KeyError("Number of strategies is not equal to the number of players") except TypeError: - raise TypeError("contingency must be a tuple-like object") - cont = [0 for _ in self.players] - for (pl, st) in enumerate(i): + raise TypeError("contingency must be a tuple-like object") from None + cont = [0 for _ in players] + for (pl, st) in enumerate(contingency): + player = players[pl] if isinstance(st, int): - if st < 0 or st >= len(self.players[pl].strategies): + if st < 0 or st >= len(player.strategies): raise IndexError(f"Provided strategy index {st} out of range for player {pl}") cont[pl] = st elif isinstance(st, str): try: - cont[pl] = [s.label for s in self.players[pl].strategies].index(st) + cont[pl] = [s.label for s in player.strategies].index(st) except ValueError: raise IndexError(f"Provided strategy label '{st}' not defined") elif isinstance(st, Strategy): try: - cont[pl] = list(self.players[pl].strategies).index(st) + cont[pl] = list(player.strategies).index(st) except ValueError: raise IndexError(f"Provided strategy '{st}' not available to player") else: diff --git a/src/pygambit/gameiter.py b/src/pygambit/gameiter.py index 2cbeddeb2..0f719c878 100644 --- a/src/pygambit/gameiter.py +++ b/src/pygambit/gameiter.py @@ -2,7 +2,7 @@ # This file is part of Gambit # Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) # -# FILE: src/python/gambit/gameiter.py +# FILE: src/pygambit/gameiter.py # Iteration tools over games in pure Python # # This program is free software; you can redistribute it and/or modify @@ -52,7 +52,8 @@ def __iter__(self): yield [list(player.strategies).index(self.cont[player]) for player in self.game.players] else: - nextpl = min(pl for (pl, player) in enumerate(self.game.players) + players = list(self.game.players) + nextpl = min(pl for (pl, player) in enumerate(players) if player not in self.cont) - for strategy in self.game.players[nextpl].strategies: + for strategy in players[nextpl].strategies: yield from self[strategy] diff --git a/src/pygambit/infoset.pxi b/src/pygambit/infoset.pxi index da4b89b92..82a0f7098 100644 --- a/src/pygambit/infoset.pxi +++ b/src/pygambit/infoset.pxi @@ -45,19 +45,30 @@ class InfosetMembers: for member in self.infoset.deref().GetMembers(): yield Node.wrap(member) - def __getitem__(self, index: int | str) -> Node: - if isinstance(index, str): - if not index.strip(): - raise ValueError("Node label cannot be empty or all whitespace") - matches = [x for x in self if x.label == index.strip()] - if not matches: - raise KeyError(f"Infoset has no member with label '{index}'") - if len(matches) > 1: - raise ValueError(f"Infoset has multiple members with label '{index}'") - return matches[0] - if isinstance(index, int): - return Node.wrap(self.infoset.deref().GetMember(index + 1)) - raise TypeError(f"Member index must be int or str, not {index.__class__.__name__}") + def __getitem__(self, label: str) -> Node: + """Returns the member node with text label `label`. + + Parameters + ---------- + label : str + The text label of the member node to return. Lookup is by exact match; + leading/trailing whitespace is stripped from `label`. + + Raises + ------ + KeyError + If the information set has no member with label `label`. + ValueError + If `label` is empty or all whitespace, or if more than one member has label `label`. + TypeError + If `label` is not a string. + + .. versionchanged:: 16.7.0 + Integer indexing is no longer supported; reference a member by its label, or iterate + over the collection. String lookup now requires an exact match of the label; + previously, leading/trailing whitespace was stripped from `label` before comparison. + """ + return _resolve_by_label(self, label, "Infoset", "member", "members") @cython.cclass @@ -86,19 +97,31 @@ class InfosetActions: for action in self.infoset.deref().GetActions(): yield Action.wrap(action) - def __getitem__(self, index: int | str) -> Action: - if isinstance(index, str): - if not index.strip(): - raise ValueError("Action label cannot be empty or all whitespace") - matches = [x for x in self if x.label == index.strip()] - if not matches: - raise KeyError(f"Infoset has no action with label '{index}'") - if len(matches) > 1: - raise ValueError(f"Infoset has multiple actions with label '{index}'") - return matches[0] - if isinstance(index, int): - return Action.wrap(self.infoset.deref().GetAction(index + 1)) - raise TypeError(f"Action index must be int or str, not {index.__class__.__name__}") + def __getitem__(self, label: str) -> Action: + """Returns the action at the information set with text label `label`. + + Parameters + ---------- + label : str + The text label of the action to return. Lookup is by exact match; + leading/trailing whitespace is stripped from `label`. + + Raises + ------ + KeyError + If the information set has no action with label `label`. + ValueError + If `label` is empty or all whitespace, or if more than one action at the information + set has label `label`. + TypeError + If `label` is not a string. + + .. versionchanged:: 16.7.0 + Integer indexing is no longer supported; reference an action by its label, or iterate + over the collection. String lookup now requires an exact match of the label; + previously, leading/trailing whitespace was stripped from `label` before comparison. + """ + return _resolve_by_label(self, label, "Infoset", "action", "actions") @cython.cclass diff --git a/src/pygambit/nash.py b/src/pygambit/nash.py index 5c7fe1a36..96201df0b 100644 --- a/src/pygambit/nash.py +++ b/src/pygambit/nash.py @@ -513,7 +513,7 @@ def ipa_solve( for strategy in game.strategies: perturbation[strategy] = 0.0 for player in game.players: - perturbation[player.strategies[0]] = 1.0 + perturbation[next(iter(player.strategies))] = 1.0 elif isinstance(perturbation, libgbt.MixedStrategyProfileDouble): game = perturbation.game else: @@ -595,7 +595,7 @@ def gnm_solve( for strategy in game.strategies: perturbation[strategy] = 0.0 for player in game.players: - perturbation[player.strategies[0]] = 1.0 + perturbation[next(iter(player.strategies))] = 1.0 elif isinstance(perturbation, libgbt.MixedStrategyProfileDouble): game = perturbation.game else: diff --git a/src/pygambit/node.pxi b/src/pygambit/node.pxi index 0f082d3b1..ec30b40bf 100644 --- a/src/pygambit/node.pxi +++ b/src/pygambit/node.pxi @@ -45,8 +45,21 @@ class NodeChildren: for child in self.parent.deref().GetChildren(): yield Node.wrap(child) - def __getitem__(self, action: int | str | Action) -> Node: - """Returns the successor node which is reached after 'action' is played. + def __getitem__(self, action: str | Action) -> Node: + """Returns the successor node which is reached after `action` is played. + + `action` may be an ``Action`` at this node's information set, or its label. + + Raises + ------ + KeyError + If `action` is a string and no action with that label exists at the node's + information set, or if the node is terminal. + ValueError + If `action` is an empty or all-whitespace string, or is an ``Action`` + from a different information set. + TypeError + If `action` is not a ``str`` or an ``Action``. .. versionchanged:: 16.5.0 Previously indexing by string searched the labels of the child nodes, @@ -54,28 +67,32 @@ class NodeChildren: interpretation that strings refer to action labels. Relatedly, the collection can now be indexed by an Action object. + + .. versionchanged:: 16.7.0 + Integer indexing is no longer supported; index by the ``Action`` taken, or its label. + A label matching no action now raises ``KeyError``. """ if isinstance(action, str): if not action.strip(): raise ValueError("Action label cannot be empty or all whitespace") if self.parent.deref().GetInfoset() == cython.cast(c_GameInfoset, NULL): - raise ValueError(f"No action with label '{action}' at node") + raise KeyError(f"No action with label '{action}' at node") for act in self.parent.deref().GetInfoset().deref().GetActions(): if act.deref().GetLabel().decode("ascii") == cython.cast(str, action): return Node.wrap(self.parent.deref().GetChild(act)) - raise ValueError(f"No action with label '{action}' at node") + raise KeyError(f"No action with label '{action}' at node") if isinstance(action, Action): try: return Node.wrap(self.parent.deref().GetChild(cython.cast(Action, action).action)) except IndexError: - raise ValueError(f"Action is from a different information set than node") + raise ValueError("Action is from a different information set than node") from None if isinstance(action, int): - if self.parent.deref().GetInfoset() == cython.cast(c_GameInfoset, NULL): - raise IndexError("Index out of range") - return Node.wrap(self.parent.deref().GetChild( - self.parent.deref().GetInfoset().deref().GetAction(action + 1) - )) - raise TypeError(f"Index must be int, str, or Action, not {action.__class__.__name__}") + raise TypeError( + "node children cannot be indexed by position; index by the action taken " + "(an Action or its label), or iterate. " + "(Integer indexing was removed in 16.7.0.)" + ) + raise TypeError(f"Index must be a str label or an Action, not {action.__class__.__name__}") @cython.cclass diff --git a/src/pygambit/player.pxi b/src/pygambit/player.pxi index 90483f25f..1b5823168 100644 --- a/src/pygambit/player.pxi +++ b/src/pygambit/player.pxi @@ -48,19 +48,32 @@ class PlayerInfosets: for infoset in self.player.deref().GetInfosets(): yield Infoset.wrap(infoset) - def __getitem__(self, index: int | str) -> Infoset: - if isinstance(index, str): - if not index.strip(): - raise ValueError("Infoset label cannot be empty or all whitespace") - matches = [x for x in self if x.label == index.strip()] - if not matches: - raise KeyError(f"Player has no infoset with label '{index}'") - if len(matches) > 1: - raise ValueError(f"Player has multiple infosets with label '{index}'") - return matches[0] - if isinstance(index, int): - return Infoset.wrap(self.player.deref().GetInfoset(index + 1)) - raise TypeError(f"Infoset index must be int or str, not {index.__class__.__name__}") + def __getitem__(self, label: str) -> Infoset: + """Returns the player's information set with text label `label`. + + Parameters + ---------- + label : str + The text label of the infoset to return. Lookup is by exact match; + leading/trailing whitespace is stripped from `label`. + + Raises + ------ + KeyError + If the player has no information set with label `label`. + ValueError + If `label` is empty or all whitespace, or if more than one of the player's + information sets has label `label`. + TypeError + If `label` is not a string. + + .. versionchanged:: 16.7.0 + Integer indexing is no longer supported; reference an information set by its label, + or iterate over the collection. String lookup now requires an exact match of the + label; previously, leading/trailing whitespace was stripped from `label` before + comparison. + """ + return _resolve_by_label(self, label, "Player", "infoset", "infosets") @cython.cclass @@ -88,23 +101,31 @@ class PlayerActions: for infoset in self.player.infosets: yield from infoset.actions - def __getitem__(self, index: int | str) -> Action: - if isinstance(index, str): - if not index.strip(): - raise ValueError("Action label cannot be empty or all whitespace") - matches = [x for x in self if x.label == index.strip()] - if not matches: - raise KeyError(f"Player has no action with label '{index}'") - if len(matches) > 1: - raise ValueError(f"Player has multiple actions with label '{index}'") - return matches[0] - if isinstance(index, int): - for i, action in enumerate(self): - if i == index: - return action - else: - raise IndexError("Index out of range") - raise TypeError(f"Action index must be int or str, not {index.__class__.__name__}") + def __getitem__(self, label: str) -> Action: + """Returns the player's action with text label `label`. + + Parameters + ---------- + label : str + The text label of the action to return. Lookup is by exact match; + leading/trailing whitespace is stripped from `label`. + + Raises + ------ + KeyError + If the player has no action with label `label`. + ValueError + If `label` is empty or all whitespace, or if more than one of the player's actions + has label `label`. + TypeError + If `label` is not a string. + + .. versionchanged:: 16.7.0 + Integer indexing is no longer supported; reference an action by its label, or iterate + over the collection. String lookup now requires an exact match of the label; + previously, leading/trailing whitespace was stripped from `label` before comparison. + """ + return _resolve_by_label(self, label, "Player", "action", "actions") @cython.cclass @@ -133,19 +154,31 @@ class PlayerStrategies: for strategy in self.player.deref().GetStrategies(): yield Strategy.wrap(strategy) - def __getitem__(self, index: int | str) -> Strategy: - if isinstance(index, str): - if not index.strip(): - raise ValueError("Strategy label cannot be empty or all whitespace") - matches = [x for x in self if x.label == index.strip()] - if not matches: - raise KeyError(f"Player has no strategy with label '{index}'") - if len(matches) > 1: - raise ValueError(f"Player has multiple strategies with label '{index}'") - return matches[0] - if isinstance(index, int): - return Strategy.wrap(self.player.deref().GetStrategy(index + 1)) - raise TypeError(f"Strategy index must be int or str, not {index.__class__.__name__}") + def __getitem__(self, label: str) -> Strategy: + """Returns the player's strategy with text label `label`. + + Parameters + ---------- + label : str + The text label of the strategy to return. Lookup is by exact match; + leading/trailing whitespace is stripped from `label`. + + Raises + ------ + KeyError + If the player has no strategy with label `label`. + ValueError + If `label` is empty or all whitespace, or if more than one of the player's strategies + has label `label`. + TypeError + If `label` is not a string. + + .. versionchanged:: 16.7.0 + Integer indexing is no longer supported; reference a strategy by its label, or iterate + over the collection. String lookup now requires an exact match of the label; + previously, leading/trailing whitespace was stripped from `label` before comparison. + """ + return _resolve_by_label(self, label, "Player", "strategy", "strategies") @cython.cclass diff --git a/tests/games.py b/tests/games.py index 312854b43..09579f6c3 100644 --- a/tests/games.py +++ b/tests/games.py @@ -34,7 +34,10 @@ def create_efg_corresponding_to_bimatrix_game( g.append_move(g.root, "1", actions1) g.append_move(g.root.children, "2", actions2) for i, j in itertools.product(range(m), range(n)): - g.set_outcome(g.root.children[i].children[j], g.add_outcome([A[i, j], B[i, j]])) + g.set_outcome( + g.root.children[str(i)].children[str(j)], + g.add_outcome([A[i, j], B[i, j]]) + ) return g @@ -60,10 +63,10 @@ def create_2x2_zero_sum_efg(variant: None | str = None) -> gbt.Game: g = create_efg_corresponding_to_bimatrix_game(A, B, title) if variant == "missing term outcome": - g.delete_outcome(g.root.children[0].children[1].outcome) + g.delete_outcome(g.root.children["0"].children["1"].outcome) elif variant == "with neutral outcome": neutral = g.add_outcome([0, 0], label="neutral") - g.set_outcome(g.root.children[0], neutral) + g.set_outcome(g.root.children["0"], neutral) return g @@ -378,17 +381,25 @@ def create_one_shot_trust_efg(unique_NE_variant: bool = False) -> gbt.Game: players=["Buyer", "Seller"], title="One-shot trust game, after Kreps (1990)" ) g.append_move(g.root, "Buyer", ["Trust", "Not trust"]) - g.append_move(g.root.children[0], "Seller", ["Honor", "Abuse"]) - g.set_outcome(g.root.children[0].children[0], g.add_outcome([1, 1], label="Trustworthy")) + g.append_move(g.root.children["Trust"], "Seller", ["Honor", "Abuse"]) + g.set_outcome( + g.root.children["Trust"].children["Honor"], + g.add_outcome([1, 1], label="Trustworthy") + ) if unique_NE_variant: g.set_outcome( - g.root.children[0].children[1], g.add_outcome(["1/2", 2], label="Untrustworthy") + g.root.children["Trust"].children["Abuse"], + g.add_outcome(["1/2", 2], label="Untrustworthy") ) else: g.set_outcome( - g.root.children[0].children[1], g.add_outcome([-1, 2], label="Untrustworthy") + g.root.children["Trust"].children["Abuse"], + g.add_outcome([-1, 2], label="Untrustworthy") + ) + g.set_outcome( + g.root.children["Not trust"], + g.add_outcome([0, 0], label="Opt-out") ) - g.set_outcome(g.root.children[1], g.add_outcome([0, 0], label="Opt-out")) return g @@ -486,13 +497,13 @@ def gbt_game(self): payoffs = [2**t * self.m0, 2**t * self.m1] # take payoffs if current_player == "2": payoffs.reverse() - g.set_outcome(current_node.children[0], g.add_outcome(payoffs)) + g.set_outcome(current_node.children["Take"], g.add_outcome(payoffs)) if t == self.N - 1: # for last round, push payoffs payoffs = [2 ** (t + 1) * self.m1, 2 ** (t + 1) * self.m0] if current_player == "2": payoffs.reverse() - g.set_outcome(current_node.children[1], g.add_outcome(payoffs)) - current_node = current_node.children[1] + g.set_outcome(current_node.children["Push"], g.add_outcome(payoffs)) + current_node = current_node.children["Push"] current_player = "2" if current_player == "1" else "1" return g @@ -628,8 +639,8 @@ def gbt_game(self): ) self.create_binary_tree(g, g.root, 0, 0, self.level) for n in g.nodes: - if not n.is_terminal and not n.children[0].is_terminal: - g.set_infoset(n.children[1], n.children[0].infoset) + if not n.is_terminal and not n.children["L"].is_terminal: + g.set_infoset(n.children["R"], n.children["L"].infoset) return g def reduced_strategic_form(self): diff --git a/tests/test_actions.py b/tests/test_actions.py index 28989e33e..2a3debf3a 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -5,45 +5,49 @@ from . import games -@pytest.mark.parametrize( - "game,label", - [(games.create_stripped_down_poker_efg(), "random label")] -) +@pytest.mark.parametrize("game,label", [(games.create_stripped_down_poker_efg(), "random label")]) def test_set_action_label(game: gbt.Game, label: str): - game.root.infoset.actions[0].label = label - assert game.root.infoset.actions[0].label == label + action = next(iter(game.root.infoset.actions)) + action.label = label + assert action.label == label def test_set_empty_action_futurewarning(): game = games.create_stripped_down_poker_efg() with pytest.warns(FutureWarning): - game.root.infoset.actions[0].label = "" + next(iter(game.root.infoset.actions)).label = "" def test_set_duplicate_action_futurewarning(): game = games.create_stripped_down_poker_efg() with pytest.warns(FutureWarning): - game.root.infoset.actions[0].label = "Queen" + next(iter(game.root.infoset.actions)).label = "Queen" @pytest.mark.parametrize( "game,inprobs,outprobs", - [(games.create_stripped_down_poker_efg(), - [0.75, 0.25], [0.75, 0.25]), - (games.create_stripped_down_poker_efg(), - ["16/17", "1/17"], [gbt.Rational("16/17"), gbt.Rational("1/17")])] + [ + (games.create_stripped_down_poker_efg(), [0.75, 0.25], [0.75, 0.25]), + ( + games.create_stripped_down_poker_efg(), + ["16/17", "1/17"], + [gbt.Rational("16/17"), gbt.Rational("1/17")], + ), + ], ) def test_set_chance_valid_probability(game: gbt.Game, inprobs: list, outprobs: list): game.set_chance_probs(game.root.infoset, inprobs) - for (action, prob) in zip(game.root.infoset.actions, outprobs, strict=True): + for action, prob in zip(game.root.infoset.actions, outprobs, strict=True): assert action.prob == prob @pytest.mark.parametrize( "game,inprobs", - [(games.create_stripped_down_poker_efg(), [0.75, -0.10]), - (games.create_stripped_down_poker_efg(), [0.75, 0.40]), - (games.create_stripped_down_poker_efg(), ["foo", "bar"])] + [ + (games.create_stripped_down_poker_efg(), [0.75, -0.10]), + (games.create_stripped_down_poker_efg(), [0.75, 0.40]), + (games.create_stripped_down_poker_efg(), ["foo", "bar"]), + ], ) def test_set_chance_improper_probability(game: gbt.Game, inprobs: list): with pytest.raises(ValueError): @@ -52,139 +56,135 @@ def test_set_chance_improper_probability(game: gbt.Game, inprobs: list): @pytest.mark.parametrize( "game,inprobs", - [(games.create_stripped_down_poker_efg(), [0.25, 0.75, 0.25]), - (games.create_stripped_down_poker_efg(), [1.00])] + [ + (games.create_stripped_down_poker_efg(), [0.25, 0.75, 0.25]), + (games.create_stripped_down_poker_efg(), [1.00]), + ], ) def test_set_chance_bad_dimension(game: gbt.Game, inprobs: list): with pytest.raises(IndexError): game.set_chance_probs(game.root.infoset, inprobs) -@pytest.mark.parametrize( - "game", - [games.create_stripped_down_poker_efg()] -) +@pytest.mark.parametrize("game", [games.create_stripped_down_poker_efg()]) def test_set_chance_personal(game: gbt.Game): with pytest.raises(gbt.UndefinedOperationError): - game.set_chance_probs(game.players[0].infosets[0], [0.75, 0.25]) + personal_infoset = next(iter(game.players["Alice"].infosets)) + game.set_chance_probs(personal_infoset, [0.75, 0.25]) -@pytest.mark.parametrize( - "game", - [games.create_stripped_down_poker_efg()] -) +@pytest.mark.parametrize("game", [games.create_stripped_down_poker_efg()]) def test_action_precedes(game: gbt.Game): - child = game.root.children[0] - assert game.root.infoset.actions[0].precedes(child) - assert not game.root.infoset.actions[1].precedes(child) + child = game.root.children["King"] + assert game.root.infoset.actions["King"].precedes(child) + assert not game.root.infoset.actions["Queen"].precedes(child) -@pytest.mark.parametrize( - "game", - [games.create_stripped_down_poker_efg()] -) +@pytest.mark.parametrize("game", [games.create_stripped_down_poker_efg()]) def test_action_precedes_nonnode(game: gbt.Game): + action = next(iter(game.root.infoset.actions)) with pytest.raises(TypeError): - game.root.infoset.actions[0].precedes(game) + action.precedes(game) -@pytest.mark.parametrize( - "game", - [games.create_stripped_down_poker_efg()] -) +@pytest.mark.parametrize("game", [games.create_stripped_down_poker_efg()]) def test_action_delete_personal(game: gbt.Game): - node = game.players[0].infosets[0].members[0] + node = next(iter(game.players["Alice"].infosets["Alice has King"].members)) action_count = len(node.infoset.actions) - game.delete_action(node.infoset.actions[0]) + game.delete_action(next(iter(node.infoset.actions))) assert len(node.infoset.actions) == action_count - 1 assert len(node.children) == action_count - 1 -@pytest.mark.parametrize( - "game", - [games.create_stripped_down_poker_efg()] -) +@pytest.mark.parametrize("game", [games.create_stripped_down_poker_efg()]) def test_action_delete_last(game: gbt.Game): - node = game.players[0].infosets[0].members[0] - while len(node.infoset.actions) > 1: - game.delete_action(node.infoset.actions[0]) + infoset = game.players["Alice"].infosets["Alice has King"] + while len(infoset.actions) > 1: + game.delete_action(next(iter(infoset.actions))) with pytest.raises(gbt.UndefinedOperationError): - game.delete_action(node.infoset.actions[0]) + game.delete_action(next(iter(infoset.actions))) @pytest.mark.parametrize( "game", - [games.read_from_file("chance_root_3_moves_only_one_nonzero_prob.efg"), - games.create_stripped_down_poker_efg(), - games.read_from_file("chance_root_5_moves_no_nonterm_player_nodes.efg")] + [ + games.read_from_file("chance_root_3_moves_only_one_nonzero_prob.efg"), + games.create_stripped_down_poker_efg(), + games.read_from_file("chance_root_5_moves_no_nonterm_player_nodes.efg"), + ], ) def test_action_delete_chance(game: gbt.Game): """Test the renormalization of action probabilities when an action is deleted at a chance node """ - chance_iset = game.players.chance.infosets[0] - while len(chance_iset.actions) > 1: - old_probs = [a.prob for a in chance_iset.actions] - game.delete_action(chance_iset.actions[0]) - new_probs = [a.prob for a in chance_iset.actions] + chance_infoset = next(iter(game.players.chance.infosets)) + while len(chance_infoset.actions) > 1: + old_probs = [a.prob for a in chance_infoset.actions] + game.delete_action(next(iter(chance_infoset.actions))) + new_probs = [a.prob for a in chance_infoset.actions] assert sum(new_probs) == 1 if sum(old_probs[1:]) == 0: for p in new_probs: - assert p == 1/len(new_probs) + assert p == 1 / len(new_probs) else: for p1, p2 in zip(old_probs[1:], new_probs, strict=True): if p1 == 0: assert p2 == 0 else: - assert p2 == p1 / (1-old_probs[0]) + assert p2 == p1 / (1 - old_probs[0]) with pytest.raises(gbt.UndefinedOperationError): - game.delete_action(chance_iset.actions[0]) + game.delete_action(next(iter(chance_infoset.actions))) def test_action_plays(): - """Verify `action.plays` returns plays reachable from a given action. - """ + """Verify `action.plays` returns plays reachable from a given action.""" game = gbt.catalog.load("journals/ijgt/selten1975/fig1") - list_nodes = list(game.nodes) - list_infosets = list(game.infosets) - test_action = list_infosets[2].actions[0] # members' paths=[0, 1, 0], [0, 1] + def node_at(path: list[str]) -> gbt.Node: + node = game.root + for action_label in path: + node = node.children[action_label] + return node + + test_action = node_at(["L"]).infoset.actions["R"] - expected_set_of_plays = { - list_nodes[4], list_nodes[7] # paths=[0, 1, 0], [0, 1] - } + expected_set_of_plays = {node_at(["R", "L", "R"]), node_at(["L", "R"])} assert set(test_action.plays) == expected_set_of_plays @pytest.mark.parametrize( - "game, player_ind, str_ind, infoset_ind, expected_action_ind", + "game, player_label, strategy_label, infoset_path, expected_action_label", [ - (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 0, 0, 0, 0), - (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 0, 1, 0, 1), - (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 1, 0, 1, 0), - (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 1, 1, 1, 1), - (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 2, 0, 2, 0), - (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 2, 1, 2, 1), - (gbt.catalog.load("journals/ijgt/selten1975/fig2"), 0, 0, 0, 0), - (gbt.catalog.load("journals/ijgt/selten1975/fig2"), 0, 1, 0, 1), - (gbt.catalog.load("journals/ijgt/selten1975/fig2"), 1, 0, 2, 0), - (gbt.catalog.load("journals/ijgt/selten1975/fig2"), 1, 1, 2, 1), - (games.read_from_file("basic_extensive_game.efg"), 0, 0, 0, 0), - (games.read_from_file("basic_extensive_game.efg"), 0, 1, 0, 1), - (games.read_from_file("basic_extensive_game.efg"), 1, 0, 1, 0), - (games.read_from_file("basic_extensive_game.efg"), 1, 1, 1, 1), - (games.read_from_file("basic_extensive_game.efg"), 2, 0, 2, 0), - (games.read_from_file("basic_extensive_game.efg"), 2, 1, 2, 1), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), "Player 1", "1", [], "R"), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), "Player 1", "2", [], "L"), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), "Player 2", "1", ["R"], "R"), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), "Player 2", "2", ["R"], "L"), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), "Player 3", "1", ["R", "L"], "R"), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), "Player 3", "2", ["R", "L"], "L"), + (gbt.catalog.load("journals/ijgt/selten1975/fig2"), "Player 1", "1*", [], "R"), + (gbt.catalog.load("journals/ijgt/selten1975/fig2"), "Player 1", "21", [], "L"), + (gbt.catalog.load("journals/ijgt/selten1975/fig2"), "Player 2", "1", ["L"], "R"), + (gbt.catalog.load("journals/ijgt/selten1975/fig2"), "Player 2", "2", ["L"], "L"), + (games.read_from_file("basic_extensive_game.efg"), "Player 1", "1", [], "U1"), + (games.read_from_file("basic_extensive_game.efg"), "Player 1", "2", [], "D1"), + (games.read_from_file("basic_extensive_game.efg"), "Player 2", "1", ["U1"], "U2"), + (games.read_from_file("basic_extensive_game.efg"), "Player 2", "2", ["U1"], "D2"), + (games.read_from_file("basic_extensive_game.efg"), "Player 3", "1", ["U1", "U2"], "U3"), + (games.read_from_file("basic_extensive_game.efg"), "Player 3", "2", ["U1", "U2"], "D3"), ], ) -def test_strategy_action_defined(game, player_ind, str_ind, infoset_ind, expected_action_ind): - """Verify `Strategy.action` retrieves the correct action for defined actions. - """ - player = game.players[player_ind] - strategy = player.strategies[str_ind] - infoset = game.infosets[infoset_ind] - expected_action = infoset.actions[expected_action_ind] +def test_strategy_action_defined( + game, player_label, strategy_label, infoset_path, expected_action_label +): + """Verify `Strategy.action` retrieves the correct action for defined actions.""" + player = game.players[player_label] + strategy = player.strategies[strategy_label] + node = game.root + for action_label in infoset_path: + node = node.children[action_label] + infoset = node.infoset + expected_action = infoset.actions[expected_action_label] prescribed_action = strategy.action(infoset) @@ -192,23 +192,30 @@ def test_strategy_action_defined(game, player_ind, str_ind, infoset_ind, expecte @pytest.mark.parametrize( - "game, player_ind, str_ind, infoset_ind", + "game, player_label, strategy_label, infoset_label, infoset_path", [ - (gbt.catalog.load("journals/ijgt/selten1975/fig2"), 0, 0, 1), - (games.read_from_file("cent3.efg"), 0, 0, 1), - (games.read_from_file("cent3.efg"), 0, 0, 2), - (games.read_from_file("cent3.efg"), 0, 1, 2), - (games.read_from_file("cent3.efg"), 1, 0, 7), - (games.read_from_file("cent3.efg"), 1, 0, 7), - (games.read_from_file("cent3.efg"), 1, 1, 8), + (gbt.catalog.load("journals/ijgt/selten1975/fig2"), "Player 1", "1*", None, ["L", "L"]), + (games.read_from_file("cent3.efg"), "Player 1", "1**111", "(1,3)", None), + (games.read_from_file("cent3.efg"), "Player 1", "1**111", "(1,5)", None), + (games.read_from_file("cent3.efg"), "Player 1", "21*111", "(1,5)", None), + (games.read_from_file("cent3.efg"), "Player 2", "1**111", "(2,4)", None), + (games.read_from_file("cent3.efg"), "Player 2", "1**111", "(2,4)", None), + (games.read_from_file("cent3.efg"), "Player 2", "21*111", "(2,5)", None), ], ) -def test_strategy_action_undefined_returns_none(game, player_ind, str_ind, infoset_ind): - """Verify `Strategy.action` returns None when called on an unreached player's infoset - """ - player = game.players[player_ind] - strategy = player.strategies[str_ind] - infoset = game.infosets[infoset_ind] +def test_strategy_action_undefined_returns_none( + game, player_label, strategy_label, infoset_label, infoset_path +): + """Verify `Strategy.action` returns None when called on an unreached player's infoset""" + player = game.players[player_label] + strategy = player.strategies[strategy_label] + if infoset_label is not None: + infoset = game.infosets[infoset_label] + else: + node = game.root + for action_label in infoset_path: + node = node.children[action_label] + infoset = node.infoset prescribed_action = strategy.action(infoset) @@ -216,38 +223,42 @@ def test_strategy_action_undefined_returns_none(game, player_ind, str_ind, infos @pytest.mark.parametrize( - "game, player_ind, infoset_ind", + "game, player_label, other_infoset_path", [ - (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 0, 1), - (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 1, 0), - (gbt.catalog.load("journals/ijgt/selten1975/fig2"), 0, 2), - (gbt.catalog.load("journals/ijgt/selten1975/fig2"), 1, 0), - (games.read_from_file("basic_extensive_game.efg"), 0, 1), - (games.read_from_file("basic_extensive_game.efg"), 1, 2), - (games.read_from_file("basic_extensive_game.efg"), 2, 0), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), "Player 1", ["R"]), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), "Player 2", []), + (gbt.catalog.load("journals/ijgt/selten1975/fig2"), "Player 1", ["L"]), + (gbt.catalog.load("journals/ijgt/selten1975/fig2"), "Player 2", []), + (games.read_from_file("basic_extensive_game.efg"), "Player 1", ["U1"]), + (games.read_from_file("basic_extensive_game.efg"), "Player 2", ["U1", "U2"]), + (games.read_from_file("basic_extensive_game.efg"), "Player 3", []), ], ) -def test_strategy_action_raises_value_error_for_wrong_player(game, player_ind, infoset_ind): +def test_strategy_action_raises_value_error_for_wrong_player( + game, player_label, other_infoset_path +): """ Verify `Strategy.action` raises ValueError when the infoset belongs to a different player than the strategy. """ - player = game.players[player_ind] - strategy = player.strategies[0] - other_players_infoset = game.infosets[infoset_ind] + player = game.players[player_label] + strategy = next(iter(player.strategies)) + node = game.root + for action_label in other_infoset_path: + node = node.children[action_label] + other_players_infoset = node.infoset with pytest.raises(ValueError): strategy.action(other_players_infoset) def test_strategy_action_raises_error_for_strategic_game(): - """Verify `Strategy.action` retrieves the action prescribed by the strategy - """ + """Verify `Strategy.action` retrieves the action prescribed by the strategy""" game_efg = gbt.catalog.load("journals/ijgt/selten1975/fig2") game_nfg = game_efg.from_arrays(game_efg.to_arrays()[0], game_efg.to_arrays()[1]) - alice = game_nfg.players[0] - strategy = alice.strategies[0] - test_infoset = game_efg.infosets[0] + alice = next(iter(game_nfg.players)) + strategy = next(iter(alice.strategies)) + test_infoset = next(iter(game_efg.infosets)) with pytest.raises(gbt.UndefinedOperationError): strategy.action(test_infoset) diff --git a/tests/test_behav.py b/tests/test_behav.py index 54dcd1c3a..b4d786e28 100644 --- a/tests/test_behav.py +++ b/tests/test_behav.py @@ -15,32 +15,9 @@ def _set_action_probs(profile: gbt.MixedBehaviorProfile, probs: list, rational_f """Set the action probabilities in a behavior profile called ```profile``` according to a list with probabilities in the order of ```profile.game.actions``` """ - for i, p in enumerate(probs): + for action, p in zip(profile.game.actions, probs, strict=True): # assumes rationals given as strings - profile[profile.game.actions[i]] = gbt.Rational(p) if rational_flag else p - - -@pytest.mark.parametrize( - "game,player_idx,payoff,rational_flag", - [ - (games.read_from_file("mixed_behavior_game.efg"), 0, 3.0, False), - (games.read_from_file("mixed_behavior_game.efg"), 1, 3.0, False), - (games.read_from_file("mixed_behavior_game.efg"), 2, 3.25, False), - (games.read_from_file("mixed_behavior_game.efg"), 0, "3", True), - (games.read_from_file("mixed_behavior_game.efg"), 1, "3", True), - (games.read_from_file("mixed_behavior_game.efg"), 2, "13/4", True), - (games.create_stripped_down_poker_efg(), 0, -0.25, False), - (games.create_stripped_down_poker_efg(), 1, 0.25, True), - (games.create_stripped_down_poker_efg(), 0, "-1/4", True), - (games.create_stripped_down_poker_efg(), 1, "1/4", True), - ], -) -def test_payoff_reference( - game: gbt.Game, player_idx: int, payoff: str | float, rational_flag: bool -): - profile = game.mixed_behavior_profile(rational=rational_flag) - payoff = gbt.Rational(payoff) if rational_flag else payoff - assert profile.payoff(game.players[player_idx]) == payoff + profile[action] = gbt.Rational(p) if rational_flag else p @pytest.mark.parametrize( @@ -106,44 +83,128 @@ def test_is_defined_at_by_label(game: gbt.Game, label: str, rational_flag: bool) @pytest.mark.parametrize( - "game,player_idx,infoset_idx,action_idx,prob,rational_flag", + "game,player_label,infoset_label,action_label,prob,rational_flag", [ - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, 0, 0.5, False), - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, 1, 0.5, False), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, 0, 0.5, False), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, 1, 0.5, False), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, 0, 0.5, False), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, 1, 0.5, False), - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, 0, "1/2", True), - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, 1, "1/2", True), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, 0, "1/2", True), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, 1, "1/2", True), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, 0, "1/2", True), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, 1, "1/2", True), - (games.create_stripped_down_poker_efg(), 0, 0, 0, 0.5, False), - (games.create_stripped_down_poker_efg(), 0, 0, 1, 0.5, False), - (games.create_stripped_down_poker_efg(), 0, 1, 0, 0.5, False), - (games.create_stripped_down_poker_efg(), 0, 1, 1, 0.5, False), - (games.create_stripped_down_poker_efg(), 1, 0, 0, 0.5, False), - (games.create_stripped_down_poker_efg(), 1, 0, 1, 0.5, False), - (games.create_stripped_down_poker_efg(), 0, 0, 0, "1/2", True), - (games.create_stripped_down_poker_efg(), 0, 0, 1, "1/2", True), - (games.create_stripped_down_poker_efg(), 0, 1, 0, "1/2", True), - (games.create_stripped_down_poker_efg(), 0, 1, 1, "1/2", True), - (games.create_stripped_down_poker_efg(), 1, 0, 0, "1/2", True), - (games.create_stripped_down_poker_efg(), 1, 0, 1, "1/2", True), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + "U1", + 0.5, + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + "D1", + 0.5, + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + "U2", + 0.5, + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + "D2", + 0.5, + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + "U3", + 0.5, + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + "D3", + 0.5, + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + "U1", + "1/2", + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + "D1", + "1/2", + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + "U2", + "1/2", + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + "D2", + "1/2", + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + "U3", + "1/2", + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + "D3", + "1/2", + True, + ), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", "Bet", 0.5, False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", "Fold", 0.5, False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", "Bet", 0.5, False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", "Fold", 0.5, False), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", "Call", 0.5, False), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", "Fold", 0.5, False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", "Bet", "1/2", True), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", "Fold", "1/2", True), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", "Bet", "1/2", True), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", "Fold", "1/2", True), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", "Call", "1/2", True), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", "Fold", "1/2", True), ], ) -def test_profile_indexing_by_player_infoset_action_idx_reference( +def test_profile_indexing_by_player_infoset_action_reference( game: gbt.Game, - player_idx: int, - infoset_idx: int, - action_idx: int, + player_label: str, + infoset_label: str, + action_label: str, prob: str | float, rational_flag: bool, ): profile = game.mixed_behavior_profile(rational=rational_flag) - action = game.players[player_idx].infosets[infoset_idx].actions[action_idx] + action = game.players[player_label].infosets[infoset_label].actions[action_label] prob = gbt.Rational(prob) if rational_flag else prob assert profile[action] == prob @@ -332,53 +393,125 @@ def test_profile_indexing_by_invalid_infoset_or_action_label( @pytest.mark.parametrize( - "game,player_idx,infoset_idx,probs,rational_flag", + "game,player_label,infoset_label,probs,rational_flag", [ - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, [0.5, 0.5], False), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, [0.5, 0.5], False), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, [0.5, 0.5], False), - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, ["1/2", "1/2"], True), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, ["1/2", "1/2"], True), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, ["1/2", "1/2"], True), - (games.create_stripped_down_poker_efg(), 0, 0, [0.5, 0.5], False), - (games.create_stripped_down_poker_efg(), 0, 1, [0.5, 0.5], False), - (games.create_stripped_down_poker_efg(), 1, 0, [0.5, 0.5], False), - (games.create_stripped_down_poker_efg(), 0, 0, ["1/2", "1/2"], True), - (games.create_stripped_down_poker_efg(), 0, 1, ["1/2", "1/2"], True), - (games.create_stripped_down_poker_efg(), 1, 0, ["1/2", "1/2"], True), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + [0.5, 0.5], + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + [0.5, 0.5], + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + [0.5, 0.5], + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + ["1/2", "1/2"], + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + ["1/2", "1/2"], + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + ["1/2", "1/2"], + True, + ), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", [0.5, 0.5], False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", [0.5, 0.5], False), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", [0.5, 0.5], False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", ["1/2", "1/2"], True), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", ["1/2", "1/2"], True), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", ["1/2", "1/2"], True), ], ) -def test_profile_indexing_by_player_and_infoset_idx_reference( - game: gbt.Game, player_idx: int, infoset_idx: int, probs: list, rational_flag: bool +def test_profile_indexing_by_player_and_infoset_reference( + game: gbt.Game, player_label: str, infoset_label: str, probs: list, rational_flag: bool ): profile = game.mixed_behavior_profile(rational=rational_flag) - infoset = game.players[player_idx].infosets[infoset_idx] + infoset = game.players[player_label].infosets[infoset_label] probs = [gbt.Rational(prob) for prob in probs] if rational_flag else probs assert profile[infoset] == probs @pytest.mark.parametrize( - "game,player_idx,infoset_label,probs,rational_flag", + "game,player_label,infoset_label,probs,rational_flag", [ - (games.read_from_file("mixed_behavior_game.efg"), 0, "Infoset 1:1", [0.5, 0.5], False), - (games.read_from_file("mixed_behavior_game.efg"), 1, "Infoset 2:1", [0.5, 0.5], False), - (games.read_from_file("mixed_behavior_game.efg"), 2, "Infoset 3:1", [0.5, 0.5], False), - (games.read_from_file("mixed_behavior_game.efg"), 0, "Infoset 1:1", ["1/2", "1/2"], True), - (games.read_from_file("mixed_behavior_game.efg"), 1, "Infoset 2:1", ["1/2", "1/2"], True), - (games.read_from_file("mixed_behavior_game.efg"), 2, "Infoset 3:1", ["1/2", "1/2"], True), - (games.create_stripped_down_poker_efg(), 0, "Alice has King", [0.5, 0.5], False), - (games.create_stripped_down_poker_efg(), 0, "Alice has Queen", [0.5, 0.5], False), - (games.create_stripped_down_poker_efg(), 1, "Bob's response", [0.5, 0.5], False), - (games.create_stripped_down_poker_efg(), 0, "Alice has King", ["1/2", "1/2"], True), - (games.create_stripped_down_poker_efg(), 0, "Alice has Queen", ["1/2", "1/2"], True), - (games.create_stripped_down_poker_efg(), 1, "Bob's response", ["1/2", "1/2"], True), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + [0.5, 0.5], + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + [0.5, 0.5], + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + [0.5, 0.5], + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + ["1/2", "1/2"], + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + ["1/2", "1/2"], + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + ["1/2", "1/2"], + True, + ), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", [0.5, 0.5], False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", [0.5, 0.5], False), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", [0.5, 0.5], False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", ["1/2", "1/2"], True), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", ["1/2", "1/2"], True), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", ["1/2", "1/2"], True), ], ) -def test_profile_indexing_by_player_idx_infoset_label_reference( - game: gbt.Game, player_idx: int, infoset_label: str, probs: list, rational_flag: bool +def test_profile_indexing_by_player_and_infoset_label_reference( + game: gbt.Game, player_label: str, infoset_label: str, probs: list, rational_flag: bool ): profile = game.mixed_behavior_profile(rational=rational_flag) - player = game.players[player_idx] + player = game.players[player_label] probs = [gbt.Rational(prob) for prob in probs] if rational_flag else probs assert profile[player][infoset_label] == probs assert profile[infoset_label] == probs @@ -423,31 +556,6 @@ def test_profile_indexing_by_player_and_invalid_action_label( game.mixed_behavior_profile(rational=rational_flag)[player_label][action_label] -@pytest.mark.parametrize( - "game,player_idx,behav_data,rational_flag", - [ - (games.read_from_file("mixed_behavior_game.efg"), 0, [[0.5, 0.5]], False), - (games.read_from_file("mixed_behavior_game.efg"), 1, [[0.5, 0.5]], False), - (games.read_from_file("mixed_behavior_game.efg"), 2, [[0.5, 0.5]], False), - (games.read_from_file("mixed_behavior_game.efg"), 0, [["1/2", "1/2"]], True), - (games.read_from_file("mixed_behavior_game.efg"), 1, [["1/2", "1/2"]], True), - (games.read_from_file("mixed_behavior_game.efg"), 2, [["1/2", "1/2"]], True), - (games.create_stripped_down_poker_efg(), 0, [[0.5, 0.5], [0.5, 0.5]], False), - (games.create_stripped_down_poker_efg(), 1, [[0.5, 0.5]], False), - (games.create_stripped_down_poker_efg(), 0, [["1/2", "1/2"], ["1/2", "1/2"]], True), - (games.create_stripped_down_poker_efg(), 1, [["1/2", "1/2"]], True), - ], -) -def test_profile_indexing_by_player_idx_reference( - game: gbt.Game, player_idx: int, behav_data: list, rational_flag: bool -): - profile = game.mixed_behavior_profile(rational=rational_flag) - player = game.players[player_idx] - if rational_flag: - behav_data = [[gbt.Rational(prob) for prob in probs] for probs in behav_data] - assert profile[player] == behav_data - - @pytest.mark.parametrize( "game,player_label,behav_data,rational_flag", [ @@ -473,41 +581,41 @@ def test_profile_indexing_by_player_label_reference( @pytest.mark.parametrize( - "game,action_idx,prob,rational_flag", + "game,infoset_label,action_label,prob,rational_flag", [ - (games.read_from_file("mixed_behavior_game.efg"), 0, 0.72, False), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0.28, False), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0.42, False), - (games.read_from_file("mixed_behavior_game.efg"), 3, 0.58, False), - (games.read_from_file("mixed_behavior_game.efg"), 4, 0.02, False), - (games.read_from_file("mixed_behavior_game.efg"), 5, 0.98, False), - (games.read_from_file("mixed_behavior_game.efg"), 0, "2/9", True), - (games.read_from_file("mixed_behavior_game.efg"), 1, "7/9", True), - (games.read_from_file("mixed_behavior_game.efg"), 2, "4/13", True), - (games.read_from_file("mixed_behavior_game.efg"), 3, "9/13", True), - (games.read_from_file("mixed_behavior_game.efg"), 4, "1/98", True), - (games.read_from_file("mixed_behavior_game.efg"), 5, "97/98", True), - (games.create_stripped_down_poker_efg(), 0, 0.1, False), - (games.create_stripped_down_poker_efg(), 1, 0.2, False), - (games.create_stripped_down_poker_efg(), 2, 0.3, False), - (games.create_stripped_down_poker_efg(), 3, 0.4, False), - (games.create_stripped_down_poker_efg(), 4, 0.5, False), - (games.create_stripped_down_poker_efg(), 5, 0.6, False), - (games.create_stripped_down_poker_efg(), 0, "1/10", True), - (games.create_stripped_down_poker_efg(), 1, "2/10", True), - (games.create_stripped_down_poker_efg(), 2, "3/10", True), - (games.create_stripped_down_poker_efg(), 3, "4/10", True), - (games.create_stripped_down_poker_efg(), 4, "5/10", True), - (games.create_stripped_down_poker_efg(), 5, "6/10", True), + (games.read_from_file("mixed_behavior_game.efg"), "Infoset 1:1", "U1", 0.72, False), + (games.read_from_file("mixed_behavior_game.efg"), "Infoset 1:1", "D1", 0.28, False), + (games.read_from_file("mixed_behavior_game.efg"), "Infoset 2:1", "U2", 0.42, False), + (games.read_from_file("mixed_behavior_game.efg"), "Infoset 2:1", "D2", 0.58, False), + (games.read_from_file("mixed_behavior_game.efg"), "Infoset 3:1", "U3", 0.02, False), + (games.read_from_file("mixed_behavior_game.efg"), "Infoset 3:1", "D3", 0.98, False), + (games.read_from_file("mixed_behavior_game.efg"), "Infoset 1:1", "U1", "2/9", True), + (games.read_from_file("mixed_behavior_game.efg"), "Infoset 1:1", "D1", "7/9", True), + (games.read_from_file("mixed_behavior_game.efg"), "Infoset 2:1", "U2", "4/13", True), + (games.read_from_file("mixed_behavior_game.efg"), "Infoset 2:1", "D2", "9/13", True), + (games.read_from_file("mixed_behavior_game.efg"), "Infoset 3:1", "U3", "1/98", True), + (games.read_from_file("mixed_behavior_game.efg"), "Infoset 3:1", "D3", "97/98", True), + (games.create_stripped_down_poker_efg(), "Alice has King", "Bet", 0.1, False), + (games.create_stripped_down_poker_efg(), "Alice has King", "Fold", 0.2, False), + (games.create_stripped_down_poker_efg(), "Alice has Queen", "Bet", 0.3, False), + (games.create_stripped_down_poker_efg(), "Alice has Queen", "Fold", 0.4, False), + (games.create_stripped_down_poker_efg(), "Bob's response", "Call", 0.5, False), + (games.create_stripped_down_poker_efg(), "Bob's response", "Fold", 0.6, False), + (games.create_stripped_down_poker_efg(), "Alice has King", "Bet", "1/10", True), + (games.create_stripped_down_poker_efg(), "Alice has King", "Fold", "2/10", True), + (games.create_stripped_down_poker_efg(), "Alice has Queen", "Bet", "3/10", True), + (games.create_stripped_down_poker_efg(), "Alice has Queen", "Fold", "4/10", True), + (games.create_stripped_down_poker_efg(), "Bob's response", "Call", "5/10", True), + (games.create_stripped_down_poker_efg(), "Bob's response", "Fold", "6/10", True), ], ) def test_set_probabilities_action( - game: gbt.Game, action_idx: int, prob: str | float, rational_flag: bool + game: gbt.Game, infoset_label: str, action_label: str, prob: str | float, rational_flag: bool ): - """Test to set probabilities of actions by action index""" + """Test to set probabilities of actions by infoset and action label""" profile = game.mixed_behavior_profile(rational=rational_flag) prob = gbt.Rational(prob) if rational_flag else prob - action = game.actions[action_idx] + action = game.infosets[infoset_label].actions[action_label] profile[action] = prob assert profile[action] == prob @@ -541,29 +649,77 @@ def test_set_probabilities_action_by_label( @pytest.mark.parametrize( - "game,player_idx,infoset_idx,probs,rational_flag", + "game,player_label,infoset_label,probs,rational_flag", [ - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, [0.72, 0.28], False), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, [0.42, 0.58], False), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, [0.02, 0.98], False), - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, ["7/9", "2/9"], True), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, ["4/13", "9/13"], True), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, ["1/98", "97/98"], True), - (games.create_stripped_down_poker_efg(), 0, 0, [0.1, 0.9], False), - (games.create_stripped_down_poker_efg(), 0, 1, [0.2, 0.8], False), - (games.create_stripped_down_poker_efg(), 1, 0, [0.3, 0.7], False), - (games.create_stripped_down_poker_efg(), 0, 0, ["1/10", "9/10"], True), - (games.create_stripped_down_poker_efg(), 0, 1, ["2/10", "8/10"], True), - (games.create_stripped_down_poker_efg(), 1, 0, ["3/10", "7/10"], True), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + [0.72, 0.28], + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + [0.42, 0.58], + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + [0.02, 0.98], + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + ["7/9", "2/9"], + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + ["4/13", "9/13"], + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + ["1/98", "97/98"], + True, + ), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", [0.1, 0.9], False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", [0.2, 0.8], False), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", [0.3, 0.7], False), + ( + games.create_stripped_down_poker_efg(), + "Alice", + "Alice has King", + ["1/10", "9/10"], + True, + ), + ( + games.create_stripped_down_poker_efg(), + "Alice", + "Alice has Queen", + ["2/10", "8/10"], + True, + ), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", ["3/10", "7/10"], True), ], ) def test_set_probabilities_infoset( - game: gbt.Game, player_idx: int, infoset_idx: int, probs: list, rational_flag: bool + game: gbt.Game, player_label: str, infoset_label: str, probs: list, rational_flag: bool ): profile = game.mixed_behavior_profile(rational=rational_flag) if rational_flag: probs = [gbt.Rational(p) for p in probs] - infoset = game.players[player_idx].infosets[infoset_idx] + infoset = game.players[player_label].infosets[infoset_label] profile[infoset] = probs assert profile[infoset] == probs @@ -595,32 +751,6 @@ def test_set_probabilities_infoset_by_label( assert profile[infoset_label] == probs -@pytest.mark.parametrize( - "game,player_idx,behav_data,rational_flag", - [ - (games.read_from_file("mixed_behavior_game.efg"), 0, [[0.72, 0.28]], False), - (games.read_from_file("mixed_behavior_game.efg"), 1, [[0.42, 0.58]], False), - (games.read_from_file("mixed_behavior_game.efg"), 2, [[0.02, 0.98]], False), - (games.read_from_file("mixed_behavior_game.efg"), 0, [["7/9", "2/9"]], True), - (games.read_from_file("mixed_behavior_game.efg"), 1, [["4/13", "9/13"]], True), - (games.read_from_file("mixed_behavior_game.efg"), 2, [["1/98", "97/98"]], True), - (games.create_stripped_down_poker_efg(), 0, [[0.1, 0.9], [0.5, 0.5]], False), - (games.create_stripped_down_poker_efg(), 1, [[0.6, 0.4]], False), - (games.create_stripped_down_poker_efg(), 0, [["1/3", "2/3"], ["1/2", "1/2"]], True), - (games.create_stripped_down_poker_efg(), 1, [["2/3", "1/3"]], True), - ], -) -def test_set_probabilities_player( - game: gbt.Game, player_idx: int, behav_data: list, rational_flag: bool -): - player = game.players[player_idx] - profile = game.mixed_behavior_profile(rational=rational_flag) - if rational_flag: - behav_data = [[gbt.Rational(prob) for prob in probs] for probs in behav_data] - profile[player] = behav_data - assert profile[player] == behav_data - - @pytest.mark.parametrize( "game,player_label,behav_data,rational_flag", [ @@ -647,98 +777,102 @@ def test_set_probabilities_player_by_label( @pytest.mark.parametrize( - "game,node_idx,realiz_prob,rational_flag", + "game,path,realiz_prob,rational_flag", [ - (games.read_from_file("mixed_behavior_game.efg"), 0, "1", True), - (games.read_from_file("mixed_behavior_game.efg"), 1, "1/2", True), - (games.read_from_file("mixed_behavior_game.efg"), 2, "1/4", True), - (games.read_from_file("mixed_behavior_game.efg"), 3, "1/8", True), - (games.read_from_file("mixed_behavior_game.efg"), 4, "1/8", True), - (games.read_from_file("mixed_behavior_game.efg"), 5, "1/4", True), - (games.read_from_file("mixed_behavior_game.efg"), 6, "1/8", True), - (games.read_from_file("mixed_behavior_game.efg"), 7, "1/8", True), - (games.read_from_file("mixed_behavior_game.efg"), 8, "1/2", True), - (games.read_from_file("mixed_behavior_game.efg"), 9, "1/4", True), - (games.read_from_file("mixed_behavior_game.efg"), 10, "1/8", True), - (games.read_from_file("mixed_behavior_game.efg"), 11, "1/8", True), - (games.read_from_file("mixed_behavior_game.efg"), 12, "1/4", True), - (games.read_from_file("mixed_behavior_game.efg"), 13, "1/8", True), - (games.read_from_file("mixed_behavior_game.efg"), 14, "1/8", True), - (games.read_from_file("mixed_behavior_game.efg"), 0, 1.0, False), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0.5, False), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0.25, False), - (games.read_from_file("mixed_behavior_game.efg"), 3, 0.125, False), - (games.read_from_file("mixed_behavior_game.efg"), 4, 0.125, False), - (games.read_from_file("mixed_behavior_game.efg"), 5, 0.25, False), - (games.read_from_file("mixed_behavior_game.efg"), 6, 0.125, False), - (games.read_from_file("mixed_behavior_game.efg"), 7, 0.125, False), - (games.read_from_file("mixed_behavior_game.efg"), 8, 0.5, False), - (games.read_from_file("mixed_behavior_game.efg"), 9, 0.25, False), - (games.read_from_file("mixed_behavior_game.efg"), 10, 0.125, False), - (games.read_from_file("mixed_behavior_game.efg"), 11, 0.125, False), - (games.read_from_file("mixed_behavior_game.efg"), 12, 0.25, False), - (games.read_from_file("mixed_behavior_game.efg"), 13, 0.125, False), - (games.read_from_file("mixed_behavior_game.efg"), 14, 0.125, False), - (games.create_stripped_down_poker_efg(), 0, "1", True), - (games.create_stripped_down_poker_efg(), 1, "1/2", True), - (games.create_stripped_down_poker_efg(), 2, "1/4", True), - (games.create_stripped_down_poker_efg(), 3, "1/8", True), - (games.create_stripped_down_poker_efg(), 4, "1/8", True), - (games.create_stripped_down_poker_efg(), 5, "1/4", True), - (games.create_stripped_down_poker_efg(), 6, "1/2", True), - (games.create_stripped_down_poker_efg(), 7, "1/4", True), - (games.create_stripped_down_poker_efg(), 8, "1/8", True), - (games.create_stripped_down_poker_efg(), 9, "1/8", True), - (games.create_stripped_down_poker_efg(), 10, "1/4", True), - (games.create_stripped_down_poker_efg(), 0, 1.0, False), - (games.create_stripped_down_poker_efg(), 1, 0.5, False), - (games.create_stripped_down_poker_efg(), 2, 0.25, False), - (games.create_stripped_down_poker_efg(), 3, 0.125, False), - (games.create_stripped_down_poker_efg(), 4, 0.125, False), - (games.create_stripped_down_poker_efg(), 5, 0.25, False), - (games.create_stripped_down_poker_efg(), 6, 0.5, False), - (games.create_stripped_down_poker_efg(), 7, 0.25, False), - (games.create_stripped_down_poker_efg(), 8, 0.125, False), - (games.create_stripped_down_poker_efg(), 9, 0.125, False), - (games.create_stripped_down_poker_efg(), 10, 0.25, False), + (games.read_from_file("mixed_behavior_game.efg"), [], "1", True), + (games.read_from_file("mixed_behavior_game.efg"), ["U1"], "1/2", True), + (games.read_from_file("mixed_behavior_game.efg"), ["U1", "U2"], "1/4", True), + (games.read_from_file("mixed_behavior_game.efg"), ["U1", "U2", "U3"], "1/8", True), + (games.read_from_file("mixed_behavior_game.efg"), ["U1", "U2", "D3"], "1/8", True), + (games.read_from_file("mixed_behavior_game.efg"), ["U1", "D2"], "1/4", True), + (games.read_from_file("mixed_behavior_game.efg"), ["U1", "D2", "U3"], "1/8", True), + (games.read_from_file("mixed_behavior_game.efg"), ["U1", "D2", "D3"], "1/8", True), + (games.read_from_file("mixed_behavior_game.efg"), ["D1"], "1/2", True), + (games.read_from_file("mixed_behavior_game.efg"), ["D1", "U2"], "1/4", True), + (games.read_from_file("mixed_behavior_game.efg"), ["D1", "U2", "U3"], "1/8", True), + (games.read_from_file("mixed_behavior_game.efg"), ["D1", "U2", "D3"], "1/8", True), + (games.read_from_file("mixed_behavior_game.efg"), ["D1", "D2"], "1/4", True), + (games.read_from_file("mixed_behavior_game.efg"), ["D1", "D2", "U3"], "1/8", True), + (games.read_from_file("mixed_behavior_game.efg"), ["D1", "D2", "D3"], "1/8", True), + (games.read_from_file("mixed_behavior_game.efg"), [], 1.0, False), + (games.read_from_file("mixed_behavior_game.efg"), ["U1"], 0.5, False), + (games.read_from_file("mixed_behavior_game.efg"), ["U1", "U2"], 0.25, False), + (games.read_from_file("mixed_behavior_game.efg"), ["U1", "U2", "U3"], 0.125, False), + (games.read_from_file("mixed_behavior_game.efg"), ["U1", "U2", "D3"], 0.125, False), + (games.read_from_file("mixed_behavior_game.efg"), ["U1", "D2"], 0.25, False), + (games.read_from_file("mixed_behavior_game.efg"), ["U1", "D2", "U3"], 0.125, False), + (games.read_from_file("mixed_behavior_game.efg"), ["U1", "D2", "D3"], 0.125, False), + (games.read_from_file("mixed_behavior_game.efg"), ["D1"], 0.5, False), + (games.read_from_file("mixed_behavior_game.efg"), ["D1", "U2"], 0.25, False), + (games.read_from_file("mixed_behavior_game.efg"), ["D1", "U2", "U3"], 0.125, False), + (games.read_from_file("mixed_behavior_game.efg"), ["D1", "U2", "D3"], 0.125, False), + (games.read_from_file("mixed_behavior_game.efg"), ["D1", "D2"], 0.25, False), + (games.read_from_file("mixed_behavior_game.efg"), ["D1", "D2", "U3"], 0.125, False), + (games.read_from_file("mixed_behavior_game.efg"), ["D1", "D2", "D3"], 0.125, False), + (games.create_stripped_down_poker_efg(), [], "1", True), + (games.create_stripped_down_poker_efg(), ["King"], "1/2", True), + (games.create_stripped_down_poker_efg(), ["King", "Bet"], "1/4", True), + (games.create_stripped_down_poker_efg(), ["King", "Bet", "Call"], "1/8", True), + (games.create_stripped_down_poker_efg(), ["King", "Bet", "Fold"], "1/8", True), + (games.create_stripped_down_poker_efg(), ["King", "Fold"], "1/4", True), + (games.create_stripped_down_poker_efg(), ["Queen"], "1/2", True), + (games.create_stripped_down_poker_efg(), ["Queen", "Bet"], "1/4", True), + (games.create_stripped_down_poker_efg(), ["Queen", "Bet", "Call"], "1/8", True), + (games.create_stripped_down_poker_efg(), ["Queen", "Bet", "Fold"], "1/8", True), + (games.create_stripped_down_poker_efg(), ["Queen", "Fold"], "1/4", True), + (games.create_stripped_down_poker_efg(), [], 1.0, False), + (games.create_stripped_down_poker_efg(), ["King"], 0.5, False), + (games.create_stripped_down_poker_efg(), ["King", "Bet"], 0.25, False), + (games.create_stripped_down_poker_efg(), ["King", "Bet", "Call"], 0.125, False), + (games.create_stripped_down_poker_efg(), ["King", "Bet", "Fold"], 0.125, False), + (games.create_stripped_down_poker_efg(), ["King", "Fold"], 0.25, False), + (games.create_stripped_down_poker_efg(), ["Queen"], 0.5, False), + (games.create_stripped_down_poker_efg(), ["Queen", "Bet"], 0.25, False), + (games.create_stripped_down_poker_efg(), ["Queen", "Bet", "Call"], 0.125, False), + (games.create_stripped_down_poker_efg(), ["Queen", "Bet", "Fold"], 0.125, False), + (games.create_stripped_down_poker_efg(), ["Queen", "Fold"], 0.25, False), ], ) def test_realiz_prob_nodes_reference( - game: gbt.Game, node_idx: int, realiz_prob: str | float, rational_flag: bool + game: gbt.Game, path: list[str], realiz_prob: str | float, rational_flag: bool ): + # nodes have no labels, so each node is reached by walking the action-label + # path from the root (an empty path is the root itself) profile = game.mixed_behavior_profile(rational=rational_flag) realiz_prob = gbt.Rational(realiz_prob) if rational_flag else realiz_prob - node = list(game.nodes)[node_idx] + node = game.root + for action_label in path: + node = node.children[action_label] assert profile.realiz_prob(node) == realiz_prob @pytest.mark.parametrize( - "game,player_idx,infoset_idx,prob,rational_flag", + "game,player_label,infoset_label,prob,rational_flag", [ - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, 1.0, False), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, 1.0, False), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, 1.0, False), - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, "1", True), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, "1", True), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, "1", True), - (games.create_stripped_down_poker_efg(), 0, 0, 0.5, False), - (games.create_stripped_down_poker_efg(), 0, 1, 0.5, False), - (games.create_stripped_down_poker_efg(), 1, 0, 0.5, False), - (games.create_stripped_down_poker_efg(), 0, 0, "1/2", True), - (games.create_stripped_down_poker_efg(), 0, 1, "1/2", True), - (games.create_stripped_down_poker_efg(), 1, 0, "1/2", True), + (games.read_from_file("mixed_behavior_game.efg"), "Player 1", "Infoset 1:1", 1.0, False), + (games.read_from_file("mixed_behavior_game.efg"), "Player 2", "Infoset 2:1", 1.0, False), + (games.read_from_file("mixed_behavior_game.efg"), "Player 3", "Infoset 3:1", 1.0, False), + (games.read_from_file("mixed_behavior_game.efg"), "Player 1", "Infoset 1:1", "1", True), + (games.read_from_file("mixed_behavior_game.efg"), "Player 2", "Infoset 2:1", "1", True), + (games.read_from_file("mixed_behavior_game.efg"), "Player 3", "Infoset 3:1", "1", True), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", 0.5, False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", 0.5, False), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", 0.5, False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", "1/2", True), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", "1/2", True), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", "1/2", True), ], ) def test_infoset_prob_reference( - game: gbt.Game, player_idx: int, infoset_idx: int, prob: str | float, rational_flag: bool + game: gbt.Game, player_label: str, infoset_label: str, prob: str | float, rational_flag: bool ): profile = game.mixed_behavior_profile(rational=rational_flag) - ip = profile.infoset_prob(game.players[player_idx].infosets[infoset_idx]) + ip = profile.infoset_prob(game.players[player_label].infosets[infoset_label]) assert ip == (gbt.Rational(prob) if rational_flag else prob) @pytest.mark.parametrize( - "game,label,prob,rational_flag,", + "game,label,prob,rational_flag", [ (games.read_from_file("mixed_behavior_game.efg"), "Infoset 1:1", 1.0, False), (games.read_from_file("mixed_behavior_game.efg"), "Infoset 2:1", 1.0, False), @@ -762,27 +896,27 @@ def test_infoset_prob_by_label_reference( @pytest.mark.parametrize( - "game,player_idx,infoset_idx,payoff,rational_flag", + "game,player_label,infoset_label,payoff,rational_flag", [ - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, 3.0, False), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, 3.0, False), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, 3.25, False), - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, "3", True), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, "3", True), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, "13/4", True), - (games.create_stripped_down_poker_efg(), 0, 0, 0.25, False), - (games.create_stripped_down_poker_efg(), 0, 1, -0.75, False), - (games.create_stripped_down_poker_efg(), 1, 0, -0.5, False), - (games.create_stripped_down_poker_efg(), 0, 0, "1/4", True), - (games.create_stripped_down_poker_efg(), 0, 1, "-3/4", True), - (games.create_stripped_down_poker_efg(), 1, 0, "-1/2", True), + (games.read_from_file("mixed_behavior_game.efg"), "Player 1", "Infoset 1:1", 3.0, False), + (games.read_from_file("mixed_behavior_game.efg"), "Player 2", "Infoset 2:1", 3.0, False), + (games.read_from_file("mixed_behavior_game.efg"), "Player 3", "Infoset 3:1", 3.25, False), + (games.read_from_file("mixed_behavior_game.efg"), "Player 1", "Infoset 1:1", "3", True), + (games.read_from_file("mixed_behavior_game.efg"), "Player 2", "Infoset 2:1", "3", True), + (games.read_from_file("mixed_behavior_game.efg"), "Player 3", "Infoset 3:1", "13/4", True), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", 0.25, False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", -0.75, False), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", -0.5, False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", "1/4", True), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", "-3/4", True), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", "-1/2", True), ], ) def test_infoset_payoff_reference( - game: gbt.Game, player_idx: int, infoset_idx: int, payoff: str | float, rational_flag: bool + game: gbt.Game, player_label: str, infoset_label: str, payoff: str | float, rational_flag: bool ): profile = game.mixed_behavior_profile(rational=rational_flag) - iv = profile.infoset_value(game.players[player_idx].infosets[infoset_idx]) + iv = profile.infoset_value(game.players[player_label].infosets[infoset_label]) assert iv == (gbt.Rational(payoff) if rational_flag else payoff) @@ -811,45 +945,138 @@ def test_infoset_payoff_by_label_reference( @pytest.mark.parametrize( - "game,player_idx,infoset_idx,action_idx,payoff,rational_flag", + "game,player_label,infoset_label,action_label,payoff,rational_flag", [ - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, 0, 3.0, False), - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, 1, 3.0, False), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, 0, 3.0, False), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, 1, 3.0, False), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, 0, 3.5, False), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, 1, 3.0, False), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, 1, 3.0, False), - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, 0, "3/1", True), - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, 1, "3/1", True), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, 0, "3/1", True), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, 1, "3/1", True), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, 0, "7/2", True), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, 1, "3/1", True), - (games.create_stripped_down_poker_efg(), 0, 0, 0, 1.5, False), - (games.create_stripped_down_poker_efg(), 0, 0, 1, -1, False), - (games.create_stripped_down_poker_efg(), 0, 1, 0, -0.5, False), - (games.create_stripped_down_poker_efg(), 0, 1, 1, -1, False), - (games.create_stripped_down_poker_efg(), 1, 0, 0, 0, False), - (games.create_stripped_down_poker_efg(), 1, 0, 1, -1, False), - (games.create_stripped_down_poker_efg(), 0, 0, 0, "3/2", True), - (games.create_stripped_down_poker_efg(), 0, 0, 1, -1, True), - (games.create_stripped_down_poker_efg(), 0, 1, 0, "-1/2", True), - (games.create_stripped_down_poker_efg(), 0, 1, 1, -1, True), - (games.create_stripped_down_poker_efg(), 1, 0, 0, 0, True), - (games.create_stripped_down_poker_efg(), 1, 0, 1, -1, True), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + "U1", + 3.0, + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + "D1", + 3.0, + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + "U2", + 3.0, + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + "D2", + 3.0, + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + "U3", + 3.5, + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + "D3", + 3.0, + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + "D3", + 3.0, + False, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + "U1", + "3/1", + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + "D1", + "3/1", + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + "U2", + "3/1", + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + "D2", + "3/1", + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + "U3", + "7/2", + True, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + "D3", + "3/1", + True, + ), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", "Bet", 1.5, False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", "Fold", -1, False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", "Bet", -0.5, False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", "Fold", -1, False), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", "Call", 0, False), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", "Fold", -1, False), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", "Bet", "3/2", True), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has King", "Fold", -1, True), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", "Bet", "-1/2", True), + (games.create_stripped_down_poker_efg(), "Alice", "Alice has Queen", "Fold", -1, True), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", "Call", 0, True), + (games.create_stripped_down_poker_efg(), "Bob", "Bob's response", "Fold", -1, True), ], ) def test_action_payoff_reference( game: gbt.Game, - player_idx: int, - infoset_idx: int, - action_idx: int, + player_label: str, + infoset_label: str, + action_label: str, payoff: str | float, rational_flag: bool, ): profile = game.mixed_behavior_profile(rational=rational_flag) - av = profile.action_value(game.players[player_idx].infosets[infoset_idx].actions[action_idx]) + av = profile.action_value( + game.players[player_label].infosets[infoset_label].actions[action_label] + ) assert av == (gbt.Rational(payoff) if rational_flag else payoff) @@ -957,30 +1184,75 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): @pytest.mark.parametrize( - "game,player_idx,infoset_idx,action_idx,action_probs,rational_flag,tol,value", + "game,player_label,infoset_label,action_label,action_probs,rational_flag,tol,value", [ # uniform - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, 0, None, False, TOL, 0), - (games.read_from_file("mixed_behavior_game.efg"), 0, 0, 1, None, False, TOL, 0), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, 0, None, False, TOL, 0), - (games.read_from_file("mixed_behavior_game.efg"), 1, 0, 1, None, False, TOL, 0), - (games.read_from_file("mixed_behavior_game.efg"), 2, 0, 0, None, False, TOL, 0), ( games.read_from_file("mixed_behavior_game.efg"), - 2, + "Player 1", + "Infoset 1:1", + "U1", + None, + False, + TOL, 0, - 1, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + "D1", None, False, TOL, - 0.5, - ), # 3.5 - 3 - # U1 U2 U3 + 0, + ), ( games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + "U2", + None, + False, + TOL, 0, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 2", + "Infoset 2:1", + "D2", + None, + False, + TOL, 0, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + "U3", + None, + False, + TOL, 0, + ), + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 3", + "Infoset 3:1", + "D3", + None, + False, + TOL, + 0.5, + ), # 3.5 - 3 + # U1 U2 U3 + ( + games.read_from_file("mixed_behavior_game.efg"), + "Player 1", + "Infoset 1:1", + "U1", [1, 0, 1, 0, 1, 0], False, TOL, @@ -988,9 +1260,9 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): ), ( games.read_from_file("mixed_behavior_game.efg"), - 0, - 0, - 0, + "Player 1", + "Infoset 1:1", + "U1", [1, 0, 1, 0, 1, 0], True, ZERO, @@ -998,9 +1270,9 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): ), ( games.read_from_file("mixed_behavior_game.efg"), - 0, - 0, - 1, + "Player 1", + "Infoset 1:1", + "D1", [1, 0, 1, 0, 1, 0], False, TOL, @@ -1008,9 +1280,9 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): ), ( games.read_from_file("mixed_behavior_game.efg"), - 0, - 0, - 1, + "Player 1", + "Infoset 1:1", + "D1", [1, 0, 1, 0, 1, 0], True, ZERO, @@ -1018,9 +1290,9 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): ), ( games.read_from_file("mixed_behavior_game.efg"), - 1, - 0, - 0, + "Player 2", + "Infoset 2:1", + "U2", [1, 0, 1, 0, 1, 0], False, TOL, @@ -1028,9 +1300,9 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): ), ( games.read_from_file("mixed_behavior_game.efg"), - 1, - 0, - 0, + "Player 2", + "Infoset 2:1", + "U2", [1, 0, 1, 0, 1, 0], True, ZERO, @@ -1038,9 +1310,9 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): ), ( games.read_from_file("mixed_behavior_game.efg"), - 1, - 0, - 1, + "Player 2", + "Infoset 2:1", + "D2", [1, 0, 1, 0, 1, 0], False, TOL, @@ -1048,9 +1320,9 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): ), ( games.read_from_file("mixed_behavior_game.efg"), - 1, - 0, - 1, + "Player 2", + "Infoset 2:1", + "D2", [1, 0, 1, 0, 1, 0], True, ZERO, @@ -1059,9 +1331,9 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): # Mixed Nash equilibrium ( games.read_from_file("mixed_behavior_game.efg"), - 0, - 0, - 0, + "Player 1", + "Infoset 1:1", + "U1", ["2/5", "3/5", "1/2", "1/2", "1/3", "2/3"], True, ZERO, @@ -1069,9 +1341,9 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): ), ( games.read_from_file("mixed_behavior_game.efg"), - 0, - 0, - 1, + "Player 1", + "Infoset 1:1", + "D1", ["2/5", "3/5", "1/2", "1/2", "1/3", "2/3"], True, ZERO, @@ -1079,9 +1351,9 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): ), ( games.read_from_file("mixed_behavior_game.efg"), - 1, - 0, - 0, + "Player 2", + "Infoset 2:1", + "U2", ["2/5", "3/5", "1/2", "1/2", "1/3", "2/3"], True, ZERO, @@ -1089,9 +1361,9 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): ), ( games.read_from_file("mixed_behavior_game.efg"), - 1, - 0, - 1, + "Player 2", + "Infoset 2:1", + "D2", ["2/5", "3/5", "1/2", "1/2", "1/3", "2/3"], True, ZERO, @@ -1099,9 +1371,9 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): ), ( games.read_from_file("mixed_behavior_game.efg"), - 2, - 0, - 0, + "Player 3", + "Infoset 3:1", + "U3", ["2/5", "3/5", "1/2", "1/2", "1/3", "2/3"], True, ZERO, @@ -1109,27 +1381,81 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): ), ( games.read_from_file("mixed_behavior_game.efg"), - 2, - 0, - 1, + "Player 3", + "Infoset 3:1", + "D3", ["2/5", "3/5", "1/2", "1/2", "1/3", "2/3"], True, ZERO, 0, ), # uniform - (games.create_stripped_down_poker_efg(), 0, 0, 0, None, False, TOL, 0), - (games.create_stripped_down_poker_efg(), 0, 0, 1, None, False, TOL, 2.5), # 1.5 - (-1) - (games.create_stripped_down_poker_efg(), 0, 1, 0, None, False, TOL, 0), - (games.create_stripped_down_poker_efg(), 0, 1, 1, None, False, TOL, 0.5), # -0.5 - (-1) - (games.create_stripped_down_poker_efg(), 1, 0, 0, None, False, TOL, 0), - (games.create_stripped_down_poker_efg(), 1, 0, 1, None, False, TOL, 1), # -0 - (-1) - # mixed Nash equilibrium ( games.create_stripped_down_poker_efg(), + "Alice", + "Alice has King", + "Bet", + None, + False, + TOL, 0, + ), + ( + games.create_stripped_down_poker_efg(), + "Alice", + "Alice has King", + "Fold", + None, + False, + TOL, + 2.5, + ), # 1.5 - (-1) + ( + games.create_stripped_down_poker_efg(), + "Alice", + "Alice has Queen", + "Bet", + None, + False, + TOL, 0, + ), + ( + games.create_stripped_down_poker_efg(), + "Alice", + "Alice has Queen", + "Fold", + None, + False, + TOL, + 0.5, + ), # -0.5 - (-1) + ( + games.create_stripped_down_poker_efg(), + "Bob", + "Bob's response", + "Call", + None, + False, + TOL, 0, + ), + ( + games.create_stripped_down_poker_efg(), + "Bob", + "Bob's response", + "Fold", + None, + False, + TOL, + 1, + ), # -0 - (-1) + # mixed Nash equilibrium + ( + games.create_stripped_down_poker_efg(), + "Alice", + "Alice has King", + "Bet", ["1", "0", "1/3", "2/3", "2/3", "1/3"], True, ZERO, @@ -1137,9 +1463,9 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): ), ( games.create_stripped_down_poker_efg(), - 0, - 0, - 1, + "Alice", + "Alice has King", + "Fold", ["1", "0", "1/3", "2/3", "2/3", "1/3"], True, ZERO, @@ -1149,15 +1475,15 @@ def test_agent_max_regret_consistency(game: gbt.Game, rational_flag: bool): ) def test_action_regret_reference( game: gbt.Game, - player_idx: int, - infoset_idx: int, - action_idx: int, + player_label: str, + infoset_label: str, + action_label: str, action_probs: None | list, rational_flag: bool, tol: gbt.Rational | float, value: str | float, ): - action = game.players[player_idx].infosets[infoset_idx].actions[action_idx] + action = game.players[player_label].infosets[infoset_label].actions[action_label] profile = game.mixed_behavior_profile(rational=rational_flag) if action_probs: _set_action_probs(profile, action_probs, rational_flag) @@ -1329,14 +1655,13 @@ def test_agent_max_regret_versus_non_agent( @pytest.mark.parametrize( - "game,tol,probs,infoset_idx,member_idx,value,rational_flag", + "game,tol,probs,path,value,rational_flag", [ ( games.read_from_file("mixed_behavior_game.efg"), TOL, [0.8, 0.2, 0.4, 0.6, 0.0, 1.0], - 0, - 0, + [], 1.0, False, ), @@ -1344,8 +1669,7 @@ def test_agent_max_regret_versus_non_agent( games.read_from_file("mixed_behavior_game.efg"), TOL, [0.8, 0.2, 0.4, 0.6, 0.0, 1.0], - 1, - 0, + ["U1"], 0.8, False, ), @@ -1353,8 +1677,7 @@ def test_agent_max_regret_versus_non_agent( games.read_from_file("mixed_behavior_game.efg"), TOL, [0.8, 0.2, 0.4, 0.6, 0.0, 1.0], - 1, - 1, + ["D1"], 0.2, False, ), @@ -1362,8 +1685,7 @@ def test_agent_max_regret_versus_non_agent( games.read_from_file("mixed_behavior_game.efg"), TOL, [0.8, 0.2, 0.4, 0.6, 0.0, 1.0], - 2, - 0, + ["U1", "U2"], 0.32, False, ), @@ -1371,8 +1693,7 @@ def test_agent_max_regret_versus_non_agent( games.read_from_file("mixed_behavior_game.efg"), TOL, [0.8, 0.2, 0.4, 0.6, 0.0, 1.0], - 2, - 1, + ["U1", "D2"], 0.48, False, ), @@ -1380,8 +1701,7 @@ def test_agent_max_regret_versus_non_agent( games.read_from_file("mixed_behavior_game.efg"), ZERO, ["4/5", "1/5", "2/5", "3/5", "0", "1"], - 0, - 0, + [], "1", True, ), @@ -1389,8 +1709,7 @@ def test_agent_max_regret_versus_non_agent( games.read_from_file("mixed_behavior_game.efg"), ZERO, ["4/5", "1/5", "2/5", "3/5", "0", "1"], - 1, - 0, + ["U1"], "4/5", True, ), @@ -1398,8 +1717,7 @@ def test_agent_max_regret_versus_non_agent( games.read_from_file("mixed_behavior_game.efg"), ZERO, ["4/5", "1/5", "2/5", "3/5", "0", "1"], - 1, - 1, + ["D1"], "1/5", True, ), @@ -1407,8 +1725,7 @@ def test_agent_max_regret_versus_non_agent( games.read_from_file("mixed_behavior_game.efg"), ZERO, ["4/5", "1/5", "2/5", "3/5", "0", "1"], - 2, - 0, + ["U1", "U2"], "8/25", True, ), @@ -1416,8 +1733,7 @@ def test_agent_max_regret_versus_non_agent( games.read_from_file("mixed_behavior_game.efg"), ZERO, ["4/5", "1/5", "2/5", "3/5", "0", "1"], - 2, - 1, + ["U1", "D2"], "12/25", True, ), @@ -1425,8 +1741,7 @@ def test_agent_max_regret_versus_non_agent( games.create_stripped_down_poker_efg(), ZERO, ["4/5", "1/5", "2/5", "3/5", "0", "1"], - 0, - 0, + ["King"], "1", True, ), @@ -1434,8 +1749,7 @@ def test_agent_max_regret_versus_non_agent( games.create_stripped_down_poker_efg(), ZERO, ["4/5", "1/5", "2/5", "3/5", "0", "1"], - 1, - 0, + ["Queen"], "1", True, ), @@ -1443,8 +1757,7 @@ def test_agent_max_regret_versus_non_agent( games.create_stripped_down_poker_efg(), ZERO, ["4/5", "1/5", "2/5", "3/5", "0", "1"], - 2, - 0, + ["King", "Bet"], "2/3", True, ), @@ -1452,8 +1765,7 @@ def test_agent_max_regret_versus_non_agent( games.create_stripped_down_poker_efg(), ZERO, ["4/5", "1/5", "2/5", "3/5", "0", "1"], - 2, - 1, + ["Queen", "Bet"], "1/3", True, ), @@ -1461,8 +1773,7 @@ def test_agent_max_regret_versus_non_agent( games.create_stripped_down_poker_efg(), ZERO, ["1", "0", "2/5", "3/5", "0", "1"], - 2, - 0, + ["King", "Bet"], "5/7", True, ), @@ -1470,8 +1781,7 @@ def test_agent_max_regret_versus_non_agent( games.create_stripped_down_poker_efg(), ZERO, ["1", "0", "2/5", "3/5", "0", "1"], - 2, - 1, + ["Queen", "Bet"], "2/7", True, ), @@ -1481,14 +1791,17 @@ def test_node_belief_reference( game: gbt.Game, tol: gbt.Rational | float, probs: list, - infoset_idx: int, - member_idx: int, + path: list[str], value: str | float, rational_flag: bool, ): + # nodes have no labels, so each belief node is reached by walking the + # action-label path from the root (an empty path is the root itself) profile = game.mixed_behavior_profile(rational=rational_flag) _set_action_probs(profile, probs, rational_flag) - node = game.infosets[infoset_idx].members[member_idx] + node = game.root + for action_label in path: + node = node.children[action_label] value = gbt.Rational(value) if rational_flag else value assert abs(profile.belief(node) - value) <= tol @@ -1516,7 +1829,7 @@ def test_payoff_value_error_with_chance_player(game: gbt.Game, rational_flag: bo ) def test_infoset_value_error_with_chance_player_infoset(game: gbt.Game, rational_flag: bool): """Ensure a value error is raised when we call action value for a chance action""" - chance_infoset = game.players.chance.infosets[0] + chance_infoset = next(iter(game.players.chance.infosets)) with pytest.raises(ValueError): game.mixed_behavior_profile(rational=rational_flag).infoset_value(chance_infoset) @@ -1530,7 +1843,7 @@ def test_infoset_value_error_with_chance_player_infoset(game: gbt.Game, rational ) def test_action_value_error_with_chance_player_action(game: gbt.Game, rational_flag: bool): """Ensure a value error is raised when we call action value for a chance action""" - chance_action = game.players.chance.infosets[0].actions[0] + chance_action = next(iter(next(iter(game.players.chance.infosets)).actions)) with pytest.raises(ValueError): game.mixed_behavior_profile(rational=rational_flag).action_value(chance_action) @@ -2101,7 +2414,8 @@ def test_tree_representation_error(game: gbt.Game, rational_flag: bool, data: li def test_undefined_action_value(): """Test that undefined action values return `None`.""" game = gbt.catalog.load("journals/ijgt/selten1975/fig1") - action = game.players[2].infosets[0].actions[0] + *_, p3 = game.players + action = next(iter(next(iter(p3.infosets)).actions)) for rat in [False, True]: profile = game.mixed_behavior_profile([[[1, 0]], [[1, 0]], [[1, 0]]], rational=rat) assert profile.action_value(action) is None @@ -2110,7 +2424,8 @@ def test_undefined_action_value(): def test_undefined_belief(): """Test that undefined beliefs return `None`.""" game = gbt.catalog.load("journals/ijgt/selten1975/fig1") - node = game.players[2].infosets[0].members[0] + *_, p3 = game.players + node = next(iter(next(iter(p3.infosets)).members)) for rat in [False, True]: profile = game.mixed_behavior_profile([[[1, 0]], [[1, 0]], [[1, 0]]], rational=rat) assert profile.belief(node) is None @@ -2119,7 +2434,8 @@ def test_undefined_belief(): def test_undefined_infoset_value(): """Test that undefined infoset values return `None`.""" game = gbt.catalog.load("journals/ijgt/selten1975/fig1") - infoset = game.players[2].infosets[0] + *_, p3 = game.players + infoset = next(iter(p3.infosets)) for rat in [False, True]: profile = game.mixed_behavior_profile([[[1, 0]], [[1, 0]], [[1, 0]]], rational=rat) assert profile.infoset_value(infoset) is None diff --git a/tests/test_extensive.py b/tests/test_extensive.py index 2ccfe97fa..e5873e41e 100644 --- a/tests/test_extensive.py +++ b/tests/test_extensive.py @@ -99,8 +99,8 @@ def test_getting_payoff_by_label_string(): def test_getting_payoff_by_player(): game = games.read_from_file("sample_extensive_game.efg") - player1 = game.players[0] - player2 = game.players[1] + player1 = game.players["Player 1"] + player2 = game.players["Player 2"] assert game[[0, 0]][player1] == 2 assert game[[0, 1]][player1] == 2 assert game[[1, 0]][player1] == 4 @@ -390,11 +390,11 @@ def test_reduced_strategic_form( """ arrays = game.to_arrays() - for i, player in enumerate(game.players): - assert strategy_labels[i] == [s.label for s in player.strategies] - # convert strings to rationals - exp_array = games.vectorized_make_rational(np_arrays_of_rsf[i]) - assert (arrays[i] == exp_array).all() + for player, labels, exp_raw, arr in zip( + game.players, strategy_labels, np_arrays_of_rsf, arrays, strict=True + ): + assert labels == [s.label for s in player.strategies] + assert (arr == games.vectorized_make_rational(exp_raw)).all() @pytest.mark.parametrize( diff --git a/tests/test_game.py b/tests/test_game.py index 1a7e7c3b5..4999885e2 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -14,9 +14,10 @@ def test_constructor_fail(): def test_from_arrays(): m = np.array([[8, 2], [10, 5]]) game = gbt.Game.from_arrays(m, m.transpose()) + pl1, pl2 = game.players assert len(game.players) == 2 - assert len(game.players[0].strategies) == 2 - assert len(game.players[1].strategies) == 2 + assert len(pl1.strategies) == 2 + assert len(pl2.strategies) == 2 def test_empty_array_to_arrays(): @@ -88,23 +89,25 @@ def test_3d_to_arrays(): def test_from_dict(): m = np.array([[8, 2], [10, 5]]) game = gbt.Game.from_dict({"a": m, "b": m.transpose()}) + pl1, pl2 = game.players assert len(game.players) == 2 - assert len(game.players[0].strategies) == 2 - assert len(game.players[1].strategies) == 2 - assert game.players[0].label == "a" - assert game.players[1].label == "b" + assert len(pl1.strategies) == 2 + assert len(pl2.strategies) == 2 + assert pl1.label == "a" + assert pl2.label == "b" def test_game_get_outcome_by_index(): game = gbt.Game.new_table([2, 2]) - assert game[0, 0] == game.outcomes[0] + assert game[0, 0] == next(iter(game.outcomes)) def test_game_get_outcome_by_label(): game = gbt.Game.new_table([2, 2]) - game.players[0].strategies[0].label = "defect" - game.players[1].strategies[0].label = "cooperate" - assert game["defect", "cooperate"] == game.outcomes[0] + pl1, pl2 = game.players + next(iter(pl1.strategies)).label = "defect" + next(iter(pl2.strategies)).label = "cooperate" + assert game["defect", "cooperate"] == next(iter(game.outcomes)) def test_game_get_outcome_invalid_tuple_size(): @@ -133,36 +136,45 @@ def test_game_get_outcome_index_out_of_range(): def test_game_get_outcome_unmatched_label(): game = gbt.Game.new_table([2, 2]) - game.players[0].strategies[0].label = "defect" - game.players[1].strategies[0].label = "cooperate" + pl1, pl2 = game.players + next(iter(pl1.strategies)).label = "defect" + next(iter(pl2.strategies)).label = "cooperate" with pytest.raises(IndexError): _ = game["defect", "defect"] def test_game_get_outcome_with_strategies(): game = gbt.Game.new_table([2, 2]) - assert game[game.players[0].strategies[0], game.players[1].strategies[0]] == game.outcomes[0] + pl1, pl2 = game.players + assert ( + game[next(iter(pl1.strategies)), next(iter(pl2.strategies))] + == next(iter(game.outcomes)) + ) def test_game_get_outcome_with_bad_strategies(): game = gbt.Game.new_table([2, 2]) + player = next(iter(game.players)) + strategy = next(iter(player.strategies)) with pytest.raises(IndexError): - _ = game[game.players[0].strategies[0], game.players[0].strategies[0]] + _ = game[strategy, strategy] def test_game_dereference_invalid(): game = gbt.Game.new_tree() - game.add_player("One") - strategy = game.players[0].strategies[0] - game.append_move(game.root, game.players[0], ["a", "b"]) + player = game.add_player("One") + strategy = next(iter(player.strategies)) + game.append_move(game.root, player, ["a", "b"]) with pytest.raises(RuntimeError): _ = strategy.label def test_mixed_strategy_profile_game_structure_changed_no_tree(): - g = gbt.Game.from_arrays([[2, 2], [0, 0]], [[0, 0], [1, 1]]) - profiles = [g.mixed_strategy_profile(rational=b) for b in [False, True]] - g.outcomes[0][g.players[0]] = 3 + game = gbt.Game.from_arrays([[2, 2], [0, 0]], [[0, 0], [1, 1]]) + profiles = [game.mixed_strategy_profile(rational=b) for b in [False, True]] + player = next(iter(game.players)) + strategy1, strategy2, *_ = game.strategies + next(iter(game.outcomes))[player] = 3 for profile in profiles: with pytest.raises(gbt.GameStructureChangedError): profile.copy() @@ -176,39 +188,31 @@ def test_mixed_strategy_profile_game_structure_changed_no_tree(): with pytest.raises(gbt.GameStructureChangedError): profile.normalize() with pytest.raises(gbt.GameStructureChangedError): - profile.copy() - with pytest.raises(gbt.GameStructureChangedError): - profile.liap_value() - with pytest.raises(gbt.GameStructureChangedError): - profile.max_regret() - with pytest.raises(gbt.GameStructureChangedError): - # triggers error via __getitem__ - next(profile.mixed_strategies()) - with pytest.raises(gbt.GameStructureChangedError): - profile.normalize() - with pytest.raises(gbt.GameStructureChangedError): - profile.payoff(g.players[0]) + profile.payoff(player) with pytest.raises(gbt.GameStructureChangedError): - profile.player_regret(g.players[0]) + profile.player_regret(player) with pytest.raises(gbt.GameStructureChangedError): - profile.strategy_regret(g.strategies[0]) + profile.strategy_regret(strategy1) with pytest.raises(gbt.GameStructureChangedError): - profile.strategy_value(g.strategies[0]) + profile.strategy_value(strategy1) with pytest.raises(gbt.GameStructureChangedError): - profile.strategy_value_deriv(g.strategies[0], g.strategies[1]) + profile.strategy_value_deriv(strategy1, strategy2) with pytest.raises(gbt.GameStructureChangedError): # triggers error via __getitem__ next(profile.__iter__()) with pytest.raises(gbt.GameStructureChangedError): - profile.__setitem__(g.strategies[0], 0) + profile.__setitem__(strategy1, 0) with pytest.raises(gbt.GameStructureChangedError): - profile.__getitem__(g.strategies[0]) + profile.__getitem__(strategy1) def test_mixed_strategy_profile_game_structure_changed_tree(): - g = games.read_from_file("basic_extensive_game.efg") - profiles = [g.mixed_strategy_profile(rational=b) for b in [False, True]] - g.delete_action(g.players[0].infosets[0].actions[0]) + game = games.read_from_file("basic_extensive_game.efg") + profiles = [game.mixed_strategy_profile(rational=b) for b in [False, True]] + player = next(iter(game.players)) + action_to_delete = game.root.infoset.actions["U1"] + game.delete_action(action_to_delete) + strategy1, strategy2, *_ = game.strategies for profile in profiles: with pytest.raises(gbt.GameStructureChangedError): profile.as_behavior() @@ -224,47 +228,52 @@ def test_mixed_strategy_profile_game_structure_changed_tree(): with pytest.raises(gbt.GameStructureChangedError): profile.normalize() with pytest.raises(gbt.GameStructureChangedError): - profile.payoff(g.players[0]) + profile.payoff(player) with pytest.raises(gbt.GameStructureChangedError): - profile.player_regret(g.players[0]) + profile.player_regret(player) with pytest.raises(gbt.GameStructureChangedError): - profile.strategy_regret(g.strategies[0]) + profile.strategy_regret(strategy1) with pytest.raises(gbt.GameStructureChangedError): - profile.strategy_value(g.strategies[0]) + profile.strategy_value(strategy1) with pytest.raises(gbt.GameStructureChangedError): - profile.strategy_value_deriv(g.strategies[0], g.strategies[1]) + profile.strategy_value_deriv(strategy1, strategy2) with pytest.raises(gbt.GameStructureChangedError): # triggers error via __getitem__ next(profile.__iter__()) with pytest.raises(gbt.GameStructureChangedError): - profile.__setitem__(g.strategies[0], 0) + profile.__setitem__(strategy1, 0) with pytest.raises(gbt.GameStructureChangedError): - profile.__getitem__(g.strategies[0]) + profile.__getitem__(strategy1) def test_mixed_behavior_profile_game_structure_changed(): - g = games.read_from_file("basic_extensive_game.efg") - profiles = [g.mixed_behavior_profile(rational=b) for b in [False, True]] - g.delete_action(g.players[0].infosets[0].actions[0]) + game = games.read_from_file("basic_extensive_game.efg") + profiles = [game.mixed_behavior_profile(rational=b) for b in [False, True]] + player = next(iter(game.players)) + action_to_delete = game.root.infoset.actions["U1"] + game.delete_action(action_to_delete) + action = next(iter(game.actions)) + infoset = next(iter(game.infosets)) + infoset_action = next(iter(infoset.actions)) for profile in profiles: with pytest.raises(gbt.GameStructureChangedError): - profile.action_regret(g.actions[0]) + profile.action_regret(action) with pytest.raises(gbt.GameStructureChangedError): - profile.action_value(g.actions[0]) + profile.action_value(action) with pytest.raises(gbt.GameStructureChangedError): profile.as_strategy() with pytest.raises(gbt.GameStructureChangedError): - profile.belief(list(g.nodes)[0]) + profile.belief(next(iter(game.nodes))) with pytest.raises(gbt.GameStructureChangedError): profile.copy() with pytest.raises(gbt.GameStructureChangedError): - profile.infoset_prob(g.infosets[0]) + profile.infoset_prob(infoset) with pytest.raises(gbt.GameStructureChangedError): - profile.infoset_regret(g.infosets[0]) + profile.infoset_regret(infoset) with pytest.raises(gbt.GameStructureChangedError): - profile.infoset_value(g.infosets[0]) + profile.infoset_value(infoset) with pytest.raises(gbt.GameStructureChangedError): - profile.is_defined_at(g.infosets[0]) + profile.is_defined_at(infoset) with pytest.raises(gbt.GameStructureChangedError): profile.agent_liap_value() with pytest.raises(gbt.GameStructureChangedError): @@ -280,33 +289,40 @@ def test_mixed_behavior_profile_game_structure_changed(): # triggers error via __getitem__ next(profile.mixed_behaviors()) with pytest.raises(gbt.GameStructureChangedError): - profile.node_value(g.players[0], g.root) + profile.node_value(player, game.root) with pytest.raises(gbt.GameStructureChangedError): profile.normalize() with pytest.raises(gbt.GameStructureChangedError): - profile.payoff(g.players[0]) + profile.payoff(player) with pytest.raises(gbt.GameStructureChangedError): - profile.liap_value() - with pytest.raises(gbt.GameStructureChangedError): - profile.max_regret() - with pytest.raises(gbt.GameStructureChangedError): - # triggers error via __getitem__ - next(profile.mixed_actions()) - with pytest.raises(gbt.GameStructureChangedError): - # triggers error via __getitem__ - next(profile.mixed_behaviors()) - with pytest.raises(gbt.GameStructureChangedError): - profile.node_value(g.players[0], g.root) - with pytest.raises(gbt.GameStructureChangedError): - profile.normalize() - with pytest.raises(gbt.GameStructureChangedError): - profile.payoff(g.players[0]) - with pytest.raises(gbt.GameStructureChangedError): - profile.realiz_prob(g.root) + profile.realiz_prob(game.root) with pytest.raises(gbt.GameStructureChangedError): # triggers error via __getitem__ next(profile.__iter__()) with pytest.raises(gbt.GameStructureChangedError): - profile.__setitem__(g.infosets[0].actions[0], 0) + profile.__setitem__(infoset_action, 0) with pytest.raises(gbt.GameStructureChangedError): - profile.__getitem__(g.infosets[0]) + profile.__getitem__(infoset) + + +COLLECTION_GETTERS = [ + pytest.param(lambda g: g.players, id="GamePlayers"), + pytest.param(lambda g: g.outcomes, id="GameOutcomes"), + pytest.param(lambda g: g.strategies, id="GameStrategies"), + pytest.param(lambda g: g.infosets, id="GameInfosets"), + pytest.param(lambda g: g.actions, id="GameActions"), + pytest.param(lambda g: g.players["Alice"].strategies, id="PlayerStrategies"), + pytest.param(lambda g: g.players["Alice"].infosets, id="PlayerInfosets"), + pytest.param(lambda g: g.players["Alice"].actions, id="PlayerActions"), + pytest.param(lambda g: g.players["Bob"].infosets["Bob's response"].actions, + id="InfosetActions"), + pytest.param(lambda g: g.players["Bob"].infosets["Bob's response"].members, + id="InfosetMembers"), +] + + +@pytest.mark.parametrize("getter", COLLECTION_GETTERS) +def test_collection_rejects_integer_indexing(getter): + collection = getter(games.create_stripped_down_poker_efg()) + with pytest.raises(TypeError): + _ = collection[0] diff --git a/tests/test_infosets.py b/tests/test_infosets.py index e4edae3d2..ae57d9c87 100644 --- a/tests/test_infosets.py +++ b/tests/test_infosets.py @@ -11,52 +11,55 @@ def test_infoset_set_label(): game = games.read_from_file("basic_extensive_game.efg") - game.players[0].infosets[0].label = "infoset 1" - assert game.players[0].infosets[0].label == "infoset 1" + game.root.infoset.label = "infoset 1" + assert game.root.infoset.label == "infoset 1" def test_infoset_player_retrieval(): game = games.read_from_file("basic_extensive_game.efg") - assert game.players[0] == game.players[0].infosets[0].player + p1, *_ = game.players + assert p1 == game.root.infoset.player def test_infoset_node_precedes(): game = games.read_from_file("basic_extensive_game.efg") - assert not game.players[0].infosets[0].precedes(game.root) - assert game.players[1].infosets[0].precedes(game.root.children[0]) + assert not game.root.infoset.precedes(game.root) + assert game.root.children["U1"].infoset.precedes(game.root.children["U1"]) def test_infoset_set_player(): game = games.read_from_file("basic_extensive_game.efg") - game.set_player(game.root.infoset, game.players[1]) - assert game.root.infoset.player == game.players[1] + _, p2, *_ = game.players + game.set_player(game.root.infoset, p2) + assert game.root.infoset.player == p2 def test_infoset_set_player_mismatch(): game = games.read_from_file("basic_extensive_game.efg") game2 = gbt.Game.new_tree(["Frank"]) with pytest.raises(gbt.MismatchError): - game.set_player(game.root.infoset, game2.players[0]) + game.set_player(game.root.infoset, next(iter(game2.players))) def test_infoset_add_action_end(): game = games.read_from_file("basic_extensive_game.efg") - actions = list(game.players[0].infosets[0].actions) - game.add_action(game.players[0].infosets[0]) - assert list(game.players[0].infosets[0].actions)[:-1] == actions + actions = list(game.root.infoset.actions) + game.add_action(game.root.infoset) + assert list(game.root.infoset.actions)[:-1] == actions def test_infoset_add_action_before(): game = games.read_from_file("basic_extensive_game.efg") - actions = list(game.players[0].infosets[0].actions) - game.add_action(game.players[0].infosets[0], actions[0]) - assert list(game.players[0].infosets[0].actions)[1:] == actions + actions = list(game.root.infoset.actions) + game.add_action(game.root.infoset, actions[0]) + assert list(game.root.infoset.actions)[1:] == actions def test_infoset_add_action_error(): game = games.read_from_file("basic_extensive_game.efg") + _, p2, *_ = game.players with pytest.raises(gbt.MismatchError): - game.add_action(game.players[0].infosets[0], game.players[1].infosets[0].actions[0]) + game.add_action(game.root.infoset, next(iter(p2.infosets)).actions["U2"]) def test_infoset_plays(): @@ -79,7 +82,8 @@ def test_infoset_plays(): class PriorActionsTestCase: """TestCase for testing own_prior_actions.""" factory: typing.Callable[[], gbt.Game] - expected_results: list[tuple] + # each tuple is (action_path_to_a_member_node, expected_prior_actions_set) + expected_results: list[tuple[list[str], set]] @dataclasses.dataclass @@ -94,10 +98,10 @@ class AbsentMindednessTestCase: PriorActionsTestCase( factory=functools.partial(games.read_from_file, "binary_3_levels_generic_payoffs.efg"), expected_results=[ - ("Player 1", 0, {None}), - ("Player 1", 1, {("Player 1", 0, "Left")}), - ("Player 1", 2, {("Player 1", 0, "Right")}), - ("Player 2", 0, {None}), + ([], {None}), + (["Left", "Left"], {("Player 1", 0, "Left")}), + (["Right", "Left"], {("Player 1", 0, "Right")}), + (["Left"], {None}), ] ), id="perfect_recall" @@ -106,9 +110,9 @@ class AbsentMindednessTestCase: PriorActionsTestCase( factory=functools.partial(gbt.catalog.load, "journals/geb/wichardt2008"), expected_results=[ - ("Player 1", 0, {None}), - ("Player 1", 1, {("Player 1", 0, "L"), ("Player 1", 0, "R")}), - ("Player 2", 0, {None}), + ([], {None}), + (["R"], {("Player 1", 0, "L"), ("Player 1", 0, "R")}), + (["R", "r"], {None}), ] ), id="wichardt_forgetting_action" @@ -117,19 +121,19 @@ class AbsentMindednessTestCase: PriorActionsTestCase( factory=functools.partial(games.read_from_file, "subgames.efg"), expected_results=[ - ("Player 1", 0, {None}), - ("Player 1", 1, {None}), - ("Player 1", 2, {("Player 1", 1, "1")}), - ("Player 1", 3, {("Player 1", 5, "1"), ("Player 1", 1, "2")}), - ("Player 1", 4, {("Player 1", 1, "2")}), - ("Player 1", 5, {("Player 1", 4, "2")}), - ("Player 1", 6, {("Player 1", 1, "2")}), - ("Player 2", 0, {None}), - ("Player 2", 1, {("Player 2", 0, "2")}), - ("Player 2", 2, {("Player 2", 1, "1")}), - ("Player 2", 3, {("Player 2", 2, "1")}), - ("Player 2", 4, {("Player 2", 2, "2")}), - ("Player 2", 5, {("Player 2", 4, "1")}), + (["1"], {None}), + (["2"], {None}), + (["2", "1", "2"], {("Player 1", 1, "1")}), + (["2", "2", "1", "1"], {("Player 1", 5, "1"), ("Player 1", 1, "2")}), + (["2", "2", "1", "2"], {("Player 1", 1, "2")}), + (["2", "2", "1", "2", "2", "1"], {("Player 1", 4, "2")}), + (["2", "2", "2"], {("Player 1", 1, "2")}), + ([], {None}), + (["2", "1"], {("Player 2", 0, "2")}), + (["2", "2", "1"], {("Player 2", 1, "1")}), + (["2", "2", "1", "1", "1"], {("Player 2", 2, "1")}), + (["2", "2", "1", "2", "1"], {("Player 2", 2, "2")}), + (["2", "2", "1", "2", "2", "1", "1"], {("Player 2", 4, "1")}), ] ), id="four_subgames" @@ -138,8 +142,8 @@ class AbsentMindednessTestCase: PriorActionsTestCase( factory=functools.partial(games.read_from_file, "AM-driver-subgame.efg"), expected_results=[ - ("Player 1", 0, {None, ("Player 1", 0, "S")}), - ("Player 2", 0, {None}), + ([], {None, ("Player 1", 0, "S")}), + (["S", "T"], {None}), ] ), id="AM_driver" @@ -224,13 +228,16 @@ def test_infoset_own_prior_actions(test_case: PriorActionsTestCase): Test `infoset.own_prior_actions`. Verifies that the set of prior actions (as player-infoset-label tuples) - matches the expected results. + matches the expected results. Each infoset is identified by an action + path to one of its member nodes. """ game = test_case.factory() - for player_label, infoset_num, expected_set in test_case.expected_results: - player = game.players[player_label] - infoset = player.infosets[infoset_num] + for path, expected_set in test_case.expected_results: + node = game.root + for action_label in path: + node = node.children[action_label] + infoset = node.infoset actual_actions = infoset.own_prior_actions diff --git a/tests/test_io.py b/tests/test_io.py index 575abe5a3..e0107f23a 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -115,8 +115,9 @@ def test_write_efg_as_nfg(): def test_write_html(): game = gbt.Game.new_table([2, 2]) - game.players[0].label = "Alice" - game.players[1].label = "Bob" + alice, bob = game.players + alice.label = "Alice" + bob.label = "Bob" serialized_game = game.to_html() assert isinstance(serialized_game, str) assert "Alice" in serialized_game @@ -125,8 +126,9 @@ def test_write_html(): def test_write_latex(): game = gbt.Game.new_table([2, 2], title="Game title") - game.players[0].label = "Alice" - game.players[1].label = "Bob" + alice, bob = game.players + alice.label = "Alice" + bob.label = "Bob" serialized_game = game.to_latex() assert "\\begin{game}" in serialized_game assert "[\\textbf{Alice}][\\textbf{Bob}]" in serialized_game diff --git a/tests/test_mixed.py b/tests/test_mixed.py index 4187aa36a..0de526f84 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -18,9 +18,9 @@ def _set_action_probs(profile: gbt.MixedStrategyProfile, probs: list, rational_f """Set the action probabilities in a strategy profile called ```profile``` according to a list with probabilities in the order of ```profile.game.strategies``` """ - for i, p in enumerate(probs): + for strategy, p in zip(profile.game.strategies, probs, strict=True): # assumes rationals given as strings - profile[profile.game.strategies[i]] = gbt.Rational(p) if rational_flag else p + profile[strategy] = gbt.Rational(p) if rational_flag else p @pytest.mark.parametrize( @@ -1613,10 +1613,10 @@ def _get_and_check_answers( # For 4x4 coord nfg: -PROBS_1A_doub = (0.25, 0.25, 0.25, 0.25) -PROBS_2A_doub = (0.5, 0, 0.5, 0) -PROBS_1A_rat = ("1/4", "1/4", "1/4", "1/4") -PROBS_2A_rat = ("1/2", "0", "1/2", "0") +PROBS_1A_doub = (0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25) +PROBS_2A_doub = (0.5, 0, 0.5, 0, 0.5, 0, 0.5, 0) +PROBS_1A_rat = ("1/4", "1/4", "1/4", "1/4", "1/4", "1/4", "1/4", "1/4") +PROBS_2A_rat = ("1/2", "0", "1/2", "0", "1/2", "0", "1/2", "0") # For 2x2x2 nfg and stripped_down_poker efg (both have 6 strategies in total): PROBS_1B_doub = (0.5, 0.5, 0.5, 0.5, 0.5, 0.5) PROBS_2B_doub = (1.0, 0.0, 1.0, 0.0, 1.0, 0.0) diff --git a/tests/test_node.py b/tests/test_node.py index e77fa3c89..ea148839a 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -14,29 +14,32 @@ def test_get_infoset(): """Test to ensure that we can retrieve an infoset for a given node""" game = games.read_from_file("basic_extensive_game.efg") assert game.root.infoset is not None - assert game.root.children[0].infoset is not None - assert game.root.children[0].children[1].children[0].infoset is None + assert game.root.children["U1"].infoset is not None + assert game.root.children["U1"].children["D2"].children["U3"].infoset is None def test_get_outcome(): """Test to ensure that we can retrieve an outcome for a given node""" game = games.read_from_file("basic_extensive_game.efg") - assert game.root.children[0].children[1].children[0].outcome == game.outcomes["Outcome 1"] + assert ( + game.root.children["U1"].children["D2"].children["U3"].outcome + == game.outcomes["Outcome 1"] + ) assert game.root.outcome is None def test_set_outcome_null(): """Test to set an outcome to the null outcome.""" game = games.read_from_file("basic_extensive_game.efg") - game.set_outcome(game.root.children[0].children[0].children[0], None) - assert game.root.children[0].children[0].children[0].outcome is None + game.set_outcome(game.root.children["U1"].children["U2"].children["U3"], None) + assert game.root.children["U1"].children["U2"].children["U3"].outcome is None def test_get_player(): """Test to ensure that we can retrieve a player for a given node""" game = games.read_from_file("basic_extensive_game.efg") - assert game.root.player == game.players[0] - assert game.root.children[0].children[1].children[0].player is None + assert game.root.player == game.players["Player 1"] + assert game.root.children["U1"].children["D2"].children["U3"].player is None def test_get_game(): @@ -48,36 +51,36 @@ def test_get_game(): def test_get_parent(): """Test to ensure that we can retrieve a parent node for a given node""" game = games.read_from_file("basic_extensive_game.efg") - assert game.root.children[0].parent == game.root + assert game.root.children["U1"].parent == game.root assert game.root.parent is None def test_get_prior_action(): """Test to ensure that we can retrieve the prior action for a given node""" game = games.read_from_file("basic_extensive_game.efg") - assert game.root.children[0].prior_action == game.root.infoset.actions[0] + assert game.root.children["U1"].prior_action == game.root.infoset.actions["U1"] assert game.root.prior_action is None def test_get_prior_sibling(): """Test to ensure that we can retrieve a prior sibling of a given node""" game = games.read_from_file("basic_extensive_game.efg") - assert game.root.children[1].prior_sibling == game.root.children[0] - assert game.root.children[0].prior_sibling is None + assert game.root.children["D1"].prior_sibling == game.root.children["U1"] + assert game.root.children["U1"].prior_sibling is None def test_get_next_sibling(): """Test to ensure that we can retrieve a next sibling of a given node""" game = games.read_from_file("basic_extensive_game.efg") - assert game.root.children[0].next_sibling == game.root.children[1] - assert game.root.children[1].next_sibling is None + assert game.root.children["U1"].next_sibling == game.root.children["D1"] + assert game.root.children["D1"].next_sibling is None def test_is_terminal(): """Test to ensure that we can check if a given node is a terminal node""" game = games.read_from_file("basic_extensive_game.efg") assert game.root.is_terminal is False - assert game.root.children[0].children[0].children[0].is_terminal is True + assert game.root.children["U1"].children["U2"].children["U3"].is_terminal is True def test_is_successor_of(): @@ -85,14 +88,14 @@ def test_is_successor_of(): successor of a supplied node """ game = games.read_from_file("basic_extensive_game.efg") - assert game.root.children[0].is_successor_of(game.root) - assert not game.root.is_successor_of(game.root.children[0]) + assert game.root.children["U1"].is_successor_of(game.root) + assert not game.root.is_successor_of(game.root.children["U1"]) with pytest.raises(TypeError): game.root.is_successor_of(9) with pytest.raises(TypeError): game.root.is_successor_of("Test") with pytest.raises(TypeError): - game.root.is_successor_of(game.players[0]) + game.root.is_successor_of(game.players["Player 1"]) def _get_path_of_action_labels(node: gbt.Node) -> list[str]: @@ -496,7 +499,7 @@ def test_append_move_error_player_actions(): """Test to ensure there are actions when appending with a player""" game = games.read_from_file("basic_extensive_game.efg") with pytest.raises(gbt.UndefinedOperationError): - game.append_move(game.root, game.players[0], []) + game.append_move(game.root, game.players["Player 1"], []) def test_append_move_error_player_mismatch(): @@ -504,7 +507,7 @@ def test_append_move_error_player_mismatch(): game1 = gbt.Game.new_tree() game2 = games.read_from_file("basic_extensive_game.efg") with pytest.raises(gbt.MismatchError): - game1.append_move(game1.root, game2.players[0], ["a"]) + game1.append_move(game1.root, game2.players["Player 1"], ["a"]) def test_append_move_error_infoset_mismatch(): @@ -512,14 +515,14 @@ def test_append_move_error_infoset_mismatch(): game1 = gbt.Game.new_tree() game2 = games.read_from_file("basic_extensive_game.efg") with pytest.raises(gbt.MismatchError): - game1.append_infoset(game1.root, game2.players[0].infosets[0]) + game1.append_infoset(game1.root, game2.root.infoset) def test_insert_move_error_player_actions(): """Test to ensure there are actions when inserting with a player""" game = games.read_from_file("basic_extensive_game.efg") with pytest.raises(gbt.UndefinedOperationError): - game.insert_move(game.root, game.players[0], 0) + game.insert_move(game.root, game.players["Player 1"], 0) def test_insert_move_error_player_mismatch(): @@ -527,21 +530,22 @@ def test_insert_move_error_player_mismatch(): game1 = gbt.Game.new_tree() game2 = games.read_from_file("basic_extensive_game.efg") with pytest.raises(gbt.MismatchError): - game1.insert_move(game1.root, game2.players[0], 1) + game1.insert_move(game1.root, game2.players["Player 1"], 1) def test_node_leave_infoset(): """Test to ensure it's possible to remove a node from an infoset""" game = games.read_from_file("basic_extensive_game.efg") - assert len(game.infosets[1].members) == 2 - game.leave_infoset(game.root.children[0]) - assert len(game.infosets[1].members) == 1 + p2_infoset = game.root.children["U1"].infoset + assert len(p2_infoset.members) == 2 + game.leave_infoset(game.root.children["U1"]) + assert len(p2_infoset.members) == 1 def test_node_delete_parent(): """Test to ensure deleting a parent node works""" game = games.read_from_file("basic_extensive_game.efg") - node = game.root.children[0] + node = game.root.children["U1"] game.delete_parent(node) assert game.root == node @@ -549,7 +553,7 @@ def test_node_delete_parent(): def test_node_delete_tree(): """Test to ensure deleting every child of a node works""" game = games.read_from_file("basic_extensive_game.efg") - node = game.root.children[0] + node = game.root.children["U1"] game.delete_tree(node) assert len(node.children) == 0 @@ -602,9 +606,8 @@ def _subtrees_equal( def test_copy_tree_onto_nondescendent_terminal_node(): """Test copying a subtree to a non-descendent node.""" g = gbt.catalog.load("journals/ijgt/selten1975/fig1") - list_nodes = list(g.nodes) - src_node = list_nodes[3] # path=[1, 0] - dest_node = list_nodes[2] # path=[0, 0] + src_node = g.root.children["R"].children["L"] + dest_node = g.root.children["R"].children["R"] g.copy_tree(src_node, dest_node) @@ -614,9 +617,8 @@ def test_copy_tree_onto_nondescendent_terminal_node(): def test_copy_tree_onto_descendent_terminal_node(): """Test copying a subtree to a node that's a descendent of the original.""" g = gbt.catalog.load("journals/ijgt/selten1975/fig1") - list_nodes = list(g.nodes) - src_node = list_nodes[1] # path=[0] - dest_node = list_nodes[4] # path=[0, 1, 0] + src_node = g.root.children["R"] + dest_node = g.root.children["R"].children["L"].children["R"] g.copy_tree(src_node, dest_node) @@ -634,7 +636,7 @@ def test_node_move_successor(): """Test on moving a node to one of its successors.""" game = games.read_from_file("basic_extensive_game.efg") with pytest.raises(gbt.UndefinedOperationError): - game.move_tree(game.root, game.root.children[0].children[0].children[0]) + game.move_tree(game.root, game.root.children["U1"].children["U2"].children["U3"]) def test_node_move_across_games(): @@ -653,9 +655,9 @@ def test_append_move_creates_single_infoset_list_of_nodes(): """Test that appending a list of nodes creates a single infoset.""" game = games.read_from_file("sample_extensive_game.efg") game.add_player("Player 3") - nodes = [game.root.children[1].children[0], - game.root.children[0].children[0], - game.root.children[0].children[1]] + nodes = [game.root.children["2"].children["1"], + game.root.children["1"].children["1"], + game.root.children["1"].children["2"]] game.append_move(nodes, "Player 3", ["B", "F"]) assert len(game.players["Player 3"].infosets) == 1 @@ -664,9 +666,10 @@ def test_append_move_same_infoset_list_of_nodes(): """Test that nodes from a list of nodes are resolved in the same infoset.""" game = games.read_from_file("sample_extensive_game.efg") game.add_player("Player 3") - nodes = [game.root.children[1].children[0], game.root.children[0].children[0]] - game.append_move(nodes, "Player 3", ["B", "F"]) - assert nodes[0].infoset == nodes[1].infoset + node1 = game.root.children["2"].children["1"] + node2 = game.root.children["1"].children["1"] + game.append_move([node1, node2], "Player 3", ["B", "F"]) + assert node1.infoset == node2.infoset def test_append_move_actions_list_of_nodes(): @@ -675,25 +678,26 @@ def test_append_move_actions_list_of_nodes(): """ game = games.read_from_file("sample_extensive_game.efg") game.add_player("Player 3") - nodes = [game.root.children[1].children[0], game.root.children[0].children[0]] - game.append_move(nodes, "Player 3", ["B", "F", "S"]) - action_list = list(game.players["Player 3"].infosets[0].actions) - for node in nodes: - assert list(node.infoset.actions) == action_list + node1 = game.root.children["2"].children["1"] + node2 = game.root.children["1"].children["1"] + game.append_move([node1, node2], "Player 3", ["B", "F", "S"]) + assert list(node1.infoset.actions) == list(node2.infoset.actions) def test_append_move_actions_list_of_node_labels(): """Test that nodes from a list of node labels are resolved correctly.""" game = games.read_from_file("sample_extensive_game.efg") game.add_player("Player 3") - game.root.children[1].children[0].label = "0" - game.root.children[0].children[0].label = "00" + node1 = game.root.children["2"].children["1"] + node2 = game.root.children["1"].children["1"] + node1.label = "0" + node2.label = "00" game.append_move(["0", "00"], "Player 3", ["B", "F", "S"]) - assert game.root.children[1].children[0].children[0].parent.label == "0" - assert game.root.children[0].children[0].children[0].parent.label == "00" - assert len(game.root.children[1].children[0].children) == 3 - assert len(game.root.children[0].children[0].children) == 3 + assert node1.children["B"].parent.label == "0" + assert node2.children["B"].parent.label == "00" + assert len(node1.children) == 3 + assert len(node2.children) == 3 def test_append_move_actions_list_of_mixed_node_references(): @@ -703,13 +707,15 @@ def test_append_move_actions_list_of_mixed_node_references(): game = games.read_from_file("sample_extensive_game.efg") game.add_player("Player 3") - game.root.children[1].children[0].label = " 000" - node_references = [" 000", game.root.children[0].children[0]] + node1 = game.root.children["2"].children["1"] + node2 = game.root.children["1"].children["1"] + node1.label = " 000" + node_references = [" 000", node2] game.append_move(node_references, "Player 3", ["B", "F", "S"]) - assert game.root.children[1].children[0].children[0].parent.label == " 000" - assert len(game.root.children[1].children[0].children) == 3 - assert len(game.root.children[0].children[0].children) == 3 + assert node1.children["B"].parent.label == " 000" + assert len(node1.children) == 3 + assert len(node2.children) == 3 def test_append_move_labels_list_of_nodes(): @@ -718,16 +724,12 @@ def test_append_move_labels_list_of_nodes(): """ game = games.read_from_file("sample_extensive_game.efg") game.add_player("Player 3") - nodes = [game.root.children[1].children[0], game.root.children[0].children[0]] - game.append_move(nodes, "Player 3", ["B", "F", "S"]) + node1 = game.root.children["2"].children["1"] + node2 = game.root.children["1"].children["1"] + game.append_move([node1, node2], "Player 3", ["B", "F", "S"]) - action_list = game.players["Player 3"].infosets[0].actions - tmp1 = game.root.children[1].children[0].infoset.actions - tmp2 = game.root.children[0].children[0].infoset.actions - - for (action, action1, action2) in zip(action_list, tmp1, tmp2, strict=True): - assert action.label == action1.label - assert action.label == action2.label + for action1, action2 in zip(node1.infoset.actions, node2.infoset.actions, strict=True): + assert action1.label == action2.label def test_append_move_node_list_with_non_terminal_node(): @@ -738,7 +740,7 @@ def test_append_move_node_list_with_non_terminal_node(): game.add_player("Player 3") with pytest.raises(gbt.UndefinedOperationError): game.append_move( - [game.root.children[1], game.root.children[0].children[1]], + [game.root.children["2"], game.root.children["1"].children["2"]], "Player 3", ["B", "F"] ) @@ -750,10 +752,11 @@ def test_append_move_node_list_with_duplicate_node_references(): """ game = games.read_from_file("sample_extensive_game.efg") game.add_player("Player 3") - game.root.children[0].children[1].label = "00" + node = game.root.children["1"].children["2"] + node.label = "00" with pytest.raises(ValueError): game.append_move( - ["00", game.root.children[1].children[0], game.root.children[0].children[1]], + ["00", game.root.children["2"].children["1"], node], "Player 3", ["B", "F"] ) @@ -775,11 +778,12 @@ def test_append_infoset_node_list_with_non_terminal_node(): """ game = games.read_from_file("sample_extensive_game.efg") game.add_player("Player 3") - game.append_move(game.root.children[0].children[0], "Player 3", ["B", "F"]) + seed_node = game.root.children["1"].children["1"] + game.append_move(seed_node, "Player 3", ["B", "F"]) with pytest.raises(gbt.UndefinedOperationError): game.append_infoset( - [game.root.children[1], game.root.children[0].children[1]], - game.root.children[0].children[0].infoset + [game.root.children["2"], game.root.children["1"].children["2"]], + seed_node.infoset ) @@ -789,13 +793,14 @@ def test_append_infoset_node_list_with_duplicate_node(): """ game = games.read_from_file("sample_extensive_game.efg") game.add_player("Player 3") - game.append_move(game.root.children[0].children[0], "Player 3", ["B", "F"]) + seed_node = game.root.children["1"].children["1"] + game.append_move(seed_node, "Player 3", ["B", "F"]) with pytest.raises(ValueError): game.append_infoset( - [game.root.children[0].children[1], - game.root.children[1].children[0], - game.root.children[0].children[1]], - game.root.children[0].children[0].infoset + [game.root.children["1"].children["2"], + game.root.children["2"].children["1"], + game.root.children["1"].children["2"]], + seed_node.infoset ) @@ -805,19 +810,10 @@ def test_append_infoset_node_list_is_empty(): """ game = games.read_from_file("sample_extensive_game.efg") game.add_player("Player 3") - game.append_move(game.root.children[0].children[0], "Player 3", ["B", "F"]) + seed_node = game.root.children["1"].children["1"] + game.append_move(seed_node, "Player 3", ["B", "F"]) with pytest.raises(ValueError): - game.append_infoset([], game.root.children[0].children[0].infoset) - - -def _get_members(action: gbt.Action) -> set[gbt.Node]: - """Calculates the set of nodes resulting from taking a specific action - at all nodes within its information set. - """ - infoset = action.infoset - action_index = action.number - - return [member_node.children[action_index] for member_node in infoset.members] + game.append_infoset([], seed_node.infoset) def _count_subtree_nodes(start_node: gbt.Node, count_terminal: bool) -> int: @@ -854,9 +850,8 @@ def test_len_after_delete_tree(): """ game = gbt.catalog.load("journals/ijgt/selten1975/fig1") initial_number_of_nodes = len(game.nodes) - list_nodes = list(game.nodes) - root_of_the_deleted_subtree = list_nodes[3] + root_of_the_deleted_subtree = game.root.children["R"].children["L"] number_of_deleted_nodes = _count_subtree_nodes(root_of_the_deleted_subtree, True) - 1 game.delete_tree(root_of_the_deleted_subtree) @@ -869,9 +864,8 @@ def test_len_after_delete_parent(): """ game = gbt.catalog.load("journals/ijgt/selten1975/fig2") initial_number_of_nodes = len(game.nodes) - list_nodes = list(game.nodes) - node_parent_to_delete = list_nodes[4] + node_parent_to_delete = game.root.children["L"].children["L"] number_of_node_ancestors = _count_subtree_nodes(node_parent_to_delete, True) number_of_parent_ancestors = _count_subtree_nodes(node_parent_to_delete.parent, True) @@ -883,14 +877,12 @@ def test_len_after_delete_parent(): def test_len_after_append_move(): - """Verify `len(game.nodes)` is correct after `append_move`. - """ + """Verify `len(game.nodes)` is correct after `append_move`.""" game = gbt.catalog.load("journals/ijgt/selten1975/fig1") initial_number_of_nodes = len(game.nodes) - list_nodes = list(game.nodes) - terminal_node = list_nodes[5] # path=[1, 1, 0] - player = game.players[0] + terminal_node = game.root.children["R"].children["L"].children["L"] # the [1,1,0] terminal + player = game.players["Player 1"] actions_to_add = ["T", "M", "B"] game.append_move(terminal_node, player, actions_to_add) @@ -903,12 +895,11 @@ def test_len_after_append_infoset(): """ game = gbt.catalog.load("journals/ijgt/selten1975/fig2") initial_number_of_nodes = len(game.nodes) - list_nodes = list(game.nodes) - member_node = list_nodes[2] # path=[1] + member_node = game.root.children["L"] infoset_to_modify = member_node.infoset number_of_infoset_actions = len(infoset_to_modify.actions) - terminal_node_to_add = list_nodes[6] # path=[1, 1, 1] + terminal_node_to_add = game.root.children["L"].children["L"].children["l"] game.append_infoset(terminal_node_to_add, infoset_to_modify) @@ -916,13 +907,11 @@ def test_len_after_append_infoset(): def test_len_after_add_action(): - """Verify `len(game.nodes)` is correct after `add_action`. - """ + """Verify `len(game.nodes)` is correct after `add_action`.""" game = gbt.catalog.load("journals/ijgt/selten1975/fig1") initial_number_of_nodes = len(game.nodes) - infoset_to_modify = game.infosets[1] - + infoset_to_modify = game.root.children["L"].infoset # Player 2's infoset num_nodes_in_infoset = len(infoset_to_modify.members) game.add_action(infoset_to_modify) @@ -931,20 +920,18 @@ def test_len_after_add_action(): def test_len_after_delete_action(): - """Verify `len(game.nodes)` is correct after `delete_action`. - """ + """Verify `len(game.nodes)` is correct after `delete_action`.""" game = gbt.catalog.load("journals/ijgt/selten1975/fig2") initial_number_of_nodes = len(game.nodes) - action_to_delete = game.infosets[0].actions[1] - - # Calculate the total number of nodes within all subtrees - # that begin immediately after taking the specified action. - nodes_to_delete = 0 - action_nodes = _get_members(action_to_delete) + action_to_delete = game.root.infoset.actions["L"] - for subtree_root in action_nodes: - nodes_to_delete += _count_subtree_nodes(subtree_root, True) + # Deleting an action removes the subtree reached by that action at each + # member node of its information set. + nodes_to_delete = sum( + _count_subtree_nodes(member.children[action_to_delete.label], True) + for member in action_to_delete.infoset.members + ) game.delete_action(action_to_delete) @@ -952,15 +939,12 @@ def test_len_after_delete_action(): def test_len_after_insert_move(): - """Verify `len(game.nodes)` is correct after `insert_move`. - """ + """Verify `len(game.nodes)` is correct after `insert_move`.""" game = gbt.catalog.load("journals/ijgt/selten1975/fig1") initial_number_of_nodes = len(game.nodes) - list_nodes = list(game.nodes) - node_to_insert_above = list_nodes[3] - - player = game.players[1] + node_to_insert_above = game.root.children["L"].children["R"] # the [1, 0] node + player = game.players["Player 2"] num_actions_to_add = 3 game.insert_move(node_to_insert_above, player, num_actions_to_add) @@ -973,11 +957,9 @@ def test_len_after_insert_infoset(): """ game = gbt.catalog.load("journals/ijgt/selten1975/fig1") initial_number_of_nodes = len(game.nodes) - list_nodes = list(game.nodes) - member_node = list_nodes[6] # path=[1] - infoset_to_modify = member_node.infoset - node_to_insert_above = list_nodes[7] # path=[0, 1] + infoset_to_modify = game.root.children["L"].infoset + node_to_insert_above = game.root.children["L"].children["R"] number_of_infoset_actions = len(infoset_to_modify.actions) game.insert_infoset(node_to_insert_above, infoset_to_modify) @@ -990,9 +972,8 @@ def test_len_after_copy_tree(): """ game = gbt.catalog.load("journals/ijgt/selten1975/fig1") initial_number_of_nodes = len(game.nodes) - list_nodes = list(game.nodes) - src_node = list_nodes[3] # path=[1, 0] - dest_node = list_nodes[2] # path=[0, 0] + src_node = game.root.children["R"].children["L"] + dest_node = game.root.children["R"].children["R"] number_of_src_ancestors = _count_subtree_nodes(src_node, True) game.copy_tree(src_node, dest_node) @@ -1004,38 +985,68 @@ def test_node_plays(): """Verify `node.plays` returns plays reachable from a given node. """ game = gbt.catalog.load("journals/ijgt/selten1975/fig2") - list_nodes = list(game.nodes) - test_node = list_nodes[2] # path=[1] + test_node = game.root.children["L"] expected_set_of_plays = { - list_nodes[3], list_nodes[5], list_nodes[6] - } # paths=[0, 1], [0, 1, 1], [1, 1, 1] + game.root.children["L"].children["R"], + game.root.children["L"].children["L"].children["r"], + game.root.children["L"].children["L"].children["l"], + } assert set(test_node.plays) == expected_set_of_plays def test_node_children_action_label(): + """Label lookup returns the correct child. + + The RHS reaches the child positionally (independent of ``__getitem__``); a label + on both sides would make the assertion circular. + """ game = games.read_from_file("stripped_down_poker.efg") - assert game.root.children["King"] == game.root.children[0] - assert game.root.children["Queen"].children["Fold"] == game.root.children[1].children[1] + root_children = list(game.root.children) + assert game.root.children["King"] == root_children[0] + assert game.root.children["Queen"].children["Fold"] == list(root_children[1].children)[1] def test_node_children_action(): + """Action lookup returns the correct child. + + The RHS reaches the child positionally -- cf. `test_node_children_action_label()`. + """ + game = games.read_from_file("stripped_down_poker.efg") + assert game.root.children[game.root.infoset.actions["King"]] == list(game.root.children)[0] + + +def test_node_children_empty_label(): game = games.read_from_file("stripped_down_poker.efg") - assert game.root.children[game.root.infoset.actions["King"]] == game.root.children[0] + with pytest.raises(ValueError, match="empty or all whitespace"): + _ = game.root.children[" "] + + +def test_node_children_terminal_node(): + game = games.read_from_file("stripped_down_poker.efg") + terminal = next(n for n in game.nodes if n.is_terminal) + with pytest.raises(KeyError, match="No action with label"): + _ = terminal.children["Bet"] def test_node_children_nonexistent_action(): game = games.read_from_file("stripped_down_poker.efg") - with pytest.raises(ValueError): + with pytest.raises(KeyError, match="No action with label 'Jack'"): _ = game.root.children["Jack"] +def test_node_children_rejects_int(): + game = games.read_from_file("stripped_down_poker.efg") + with pytest.raises(TypeError, match="16.7.0"): + _ = game.root.children[0] + + def test_node_children_other_infoset_action(): game = games.read_from_file("stripped_down_poker.efg") with pytest.raises(ValueError): - _ = game.root.children[game.root.children[0].infoset.actions["Bet"]] + _ = game.root.children[game.root.children["King"].infoset.actions["Bet"]] @pytest.mark.parametrize( diff --git a/tests/test_outcomes.py b/tests/test_outcomes.py index aa7d50670..068fe83f6 100644 --- a/tests/test_outcomes.py +++ b/tests/test_outcomes.py @@ -17,7 +17,7 @@ def test_outcome_add(game: gbt.Game): ) def test_outcome_delete(game: gbt.Game): outcome_count = len(game.outcomes) - game.delete_outcome(game.outcomes[0]) + game.delete_outcome(next(iter(game.outcomes))) assert len(game.outcomes) == outcome_count - 1 @@ -26,8 +26,9 @@ def test_outcome_delete(game: gbt.Game): [(gbt.Game.new_table([2, 2]), "outcome label")] ) def test_outcome_label(game: gbt.Game, label: str): - game.outcomes[0].label = label - assert game.outcomes[0].label == label + outcome = next(iter(game.outcomes)) + outcome.label = label + assert outcome.label == label @pytest.mark.parametrize( @@ -35,23 +36,16 @@ def test_outcome_label(game: gbt.Game, label: str): [(gbt.Game.new_table([2, 2]), "outcome label")] ) def test_outcome_index_label(game: gbt.Game, label: str): - game.outcomes[0].label = label - assert game.outcomes[0] == game.outcomes[label] + outcome = next(iter(game.outcomes)) + outcome.label = label + assert outcome == game.outcomes[label] assert game.outcomes[label].label == label @pytest.mark.parametrize( "game", [gbt.Game.new_table([2, 2])] ) -def test_outcome_index_int_range(game: gbt.Game): - with pytest.raises(IndexError): - _ = game.outcomes[2 * len(game.outcomes)] - - -@pytest.mark.parametrize( - "game", [gbt.Game.new_table([2, 2])] -) -def test_outcome_index_label_range(game: gbt.Game): +def test_outcome_index_unmatched_label(game: gbt.Game): with pytest.raises(KeyError): _ = game.outcomes["not an outcome"] @@ -66,29 +60,15 @@ def test_outcome_index_invalid_type(game: gbt.Game): def test_outcome_payoff_by_player_label(): game = gbt.Game.new_table([2, 2]) - game.players[0].label = "joe" - game.players[1].label = "dan" - game.outcomes[0]["joe"] = 1 - game.outcomes[0]["dan"] = 2 - game.outcomes[1]["joe"] = 3 - game.outcomes[1]["dan"] = 4 - assert game.outcomes[0]["joe"] == 1 - assert game.outcomes[0]["dan"] == 2 - assert game.outcomes[1]["joe"] == 3 - assert game.outcomes[1]["dan"] == 4 - - -def test_outcome_payoff_by_player(): - game = gbt.Game.new_table([2, 2]) - game.players[0].label = "joe" - game.players[1].label = "dan" - game.outcomes[0]["joe"] = 1 - game.outcomes[0]["dan"] = 2 - game.outcomes[1]["joe"] = 3 - game.outcomes[1]["dan"] = 4 - player1 = game.players[0] - player2 = game.players[1] - assert game.outcomes[0][player1] == 1 - assert game.outcomes[0][player2] == 2 - assert game.outcomes[1][player1] == 3 - assert game.outcomes[1][player2] == 4 + pl1, pl2 = list(game.players) + pl1.label = "joe" + pl2.label = "dan" + out1, out2, *_ = list(game.outcomes) + out1["joe"] = 1 + out1["dan"] = 2 + out2["joe"] = 3 + out2["dan"] = 4 + assert out1["joe"] == 1 + assert out1["dan"] == 2 + assert out2["joe"] == 3 + assert out2["dan"] == 4 diff --git a/tests/test_players.py b/tests/test_players.py index 339ae1de2..476ccd509 100644 --- a/tests/test_players.py +++ b/tests/test_players.py @@ -5,17 +5,6 @@ from . import games -def _generate_strategic_game() -> gbt.Game: - game = gbt.Game.new_table([2, 2]) - game.players[0].label = "Alphonse" - game.players[1].label = "Gaston" - return game - - -def _generate_extensive_game() -> gbt.Game: - return gbt.Game.new_tree() - - def test_player_count(): game = gbt.Game.new_table([2, 2]) assert len(game.players) == 2 @@ -23,32 +12,22 @@ def test_player_count(): def test_player_label(): game = gbt.Game.new_table([2, 2]) - game.players[0].label = "Alphonse" - game.players[1].label = "Gaston" - assert game.players[0].label == "Alphonse" - assert game.players[1].label == "Gaston" + pl1, pl2 = game.players + pl1.label = "Alphonse" + pl2.label = "Gaston" + assert pl1.label == "Alphonse" + assert pl2.label == "Gaston" def test_player_index_by_string(): game = gbt.Game.new_table([2, 2]) - game.players[0].label = "Alphonse" - game.players[1].label = "Gaston" + pl1, pl2 = game.players + pl1.label = "Alphonse" + pl2.label = "Gaston" assert game.players["Alphonse"].label == "Alphonse" assert game.players["Gaston"].label == "Gaston" -def test_player_index_out_of_range(): - game = gbt.Game.new_table([2, 2]) - assert len(game.players) == 2 - exp_error_msg = "Index out of range" - with pytest.raises(IndexError, match=exp_error_msg): - _ = game.players[2] - with pytest.raises(IndexError, match=exp_error_msg): - _ = game.players[3] - with pytest.raises(IndexError, match=exp_error_msg): - _ = game.players[-1] - - def test_player_index_invalid(): game = gbt.Game.new_table([2, 2]) with pytest.raises(TypeError): @@ -63,39 +42,43 @@ def test_player_label_invalid(): def test_set_empty_player_futurewarning(): game = games.create_stripped_down_poker_efg() + player = next(iter(game.players)) with pytest.warns(FutureWarning): - game.players[0].label = "" + player.label = "" def test_set_duplicate_player_futurewarning(): game = games.create_stripped_down_poker_efg() + pl1, pl2, *_ = game.players with pytest.warns(FutureWarning): - game.players[0].label = game.players[1].label + pl1.label = pl2.label def test_strategic_game_add_player(): game = gbt.Game.new_table([2, 2]) - game.add_player() + new_player = game.add_player() assert len(game.players) == 3 - assert len(game.players[2].strategies) == 1 + assert len(new_player.strategies) == 1 def test_extensive_game_add_player(): game = gbt.Game.new_tree() game.add_player() + pl1 = next(iter(game.players)) assert len(game.players) == 1 - assert len(game.players[0].infosets) == 0 - assert len(game.players[0].strategies) == 1 + assert len(pl1.infosets) == 0 + assert len(pl1.strategies) == 1 def test_strategic_game_add_strategy(): game = gbt.Game.new_table([2, 2]) - game.add_strategy(game.players[0], "new strategy") - assert len(game.players[0].strategies) == 3 + pl1, pl2 = game.players + game.add_strategy(pl1, "new strategy") + assert len(pl1.strategies) == 3 # This second add also ensures that we are testing the case where there # are null outcomes in the table - game.add_strategy(game.players[1], "new strategy") - assert len(game.players[1].strategies) == 3 + game.add_strategy(pl2, "new strategy") + assert len(pl2.strategies) == 3 def test_extensive_game_add_strategy(): @@ -107,44 +90,43 @@ def test_extensive_game_add_strategy(): def test_strategic_game_delete_strategy(): game = gbt.Game.new_table([2, 2]) - game.delete_strategy(game.players[0].strategies[0]) - assert len(game.players[0].strategies) == 1 + pl1 = next(iter(game.players)) + game.delete_strategy(next(iter(pl1.strategies))) + assert len(pl1.strategies) == 1 def test_strategic_game_delete_last_strategy(): game = gbt.Game.new_table([1, 2]) + pl1 = next(iter(game.players)) with pytest.raises(gbt.UndefinedOperationError): - game.delete_strategy(game.players[0].strategies[0]) + game.delete_strategy(next(iter(pl1.strategies))) def test_extensive_game_delete_strategy(): game = gbt.Game.new_tree(["Alice"]) with pytest.raises(gbt.UndefinedOperationError): - game.delete_strategy(game.players["Alice"].strategies[0]) + game.delete_strategy(next(iter(game.players["Alice"].strategies))) def test_player_strategy_by_label(): game = gbt.Game.new_table([2, 2]) - game.players[0].strategies[0].label = "Cooperate" - assert game.players[0].strategies["Cooperate"].label == "Cooperate" - - -def test_player_strategy_bad_index(): - game = gbt.Game.new_table([2, 2]) - with pytest.raises(IndexError): - _ = game.players[0].strategies[42] + pl1 = next(iter(game.players)) + next(iter(pl1.strategies)).label = "Cooperate" + assert pl1.strategies["Cooperate"].label == "Cooperate" def test_player_strategy_bad_label(): game = gbt.Game.new_table([2, 2]) + pl1 = next(iter(game.players)) with pytest.raises(KeyError): - _ = game.players[0].strategies["Cooperate"] + _ = pl1.strategies["Cooperate"] def test_player_strategy_bad_type(): game = gbt.Game.new_table([2, 2]) + pl1 = next(iter(game.players)) with pytest.raises(TypeError): - _ = game.players[0].strategies[1.3] + _ = pl1.strategies[1.3] @pytest.mark.parametrize( @@ -166,9 +148,11 @@ def test_player_strategy_bad_type(): ], ) def test_player_get_min_max_payoff(game: gbt.Game, exp_min_payoffs: list, exp_max_payoffs: list): - for i in range(len(game.players)): - assert game.players[i].min_payoff == exp_min_payoffs[i] - assert game.players[i].max_payoff == exp_max_payoffs[i] + for player, exp_min, exp_max in zip( + game.players, exp_min_payoffs, exp_max_payoffs, strict=True + ): + assert player.min_payoff == exp_min + assert player.max_payoff == exp_max def test_player_get_min_payoff_nonterminal_outcomes(): @@ -187,9 +171,10 @@ def test_player_get_min_payoff_null_outcome(): """Test whether `min_payoff` correctly reports minimum payoffs in a strategic game with a null outcome.""" game = gbt.Game.from_arrays([[1, 1], [1, 1]], [[2, 2], [2, 2]]) - assert game.players[0].min_payoff == 1 - assert game.players[1].min_payoff == 2 - game.add_strategy(game.players[0]) + pl1, pl2 = game.players + assert pl1.min_payoff == 1 + assert pl2.min_payoff == 2 + game.add_strategy(pl1) # Currently the outcomes associated with the new entries in the table # are null outcomes. So now minimum payoff should be zero from those. for player in game.players: @@ -212,9 +197,10 @@ def test_player_get_max_payoff_null_outcome(): """Test whether `max_payoff` correctly reports maximum payoffs in a strategic game with a null outcome.""" game = gbt.Game.from_arrays([[-1, -1], [-1, -1]], [[-2, -2], [-2, -2]]) - assert game.players[0].max_payoff == -1 - assert game.players[1].max_payoff == -2 - game.add_strategy(game.players[0]) + pl1, pl2 = game.players + assert pl1.max_payoff == -1 + assert pl2.max_payoff == -2 + game.add_strategy(pl1) # Currently the outcomes associated with the new entries in the table # are null outcomes. So now minimum payoff should be zero from those. for player in game.players: diff --git a/tests/test_strategic.py b/tests/test_strategic.py index b59c4af6d..fedef6097 100644 --- a/tests/test_strategic.py +++ b/tests/test_strategic.py @@ -13,8 +13,9 @@ def test_strategic_game_actions(): def test_strategic_game_player_actions(): game = gbt.Game.new_table([2, 2]) + player, _ = game.players with pytest.raises(gbt.UndefinedOperationError): - _ = game.players[0].actions + _ = player.actions def test_strategic_game_infosets(): @@ -25,8 +26,9 @@ def test_strategic_game_infosets(): def test_strategic_game_player_infosets(): game = gbt.Game.new_table([2, 2]) + player, _ = game.players with pytest.raises(gbt.UndefinedOperationError): - _ = game.players[0].infosets + _ = player.infosets def test_strategic_game_root():