diff --git a/.gitignore b/.gitignore index 9d37e0d3b..f8be35fcf 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,5 @@ Gambit.app/* *.ef build_support/msw/gambit.wxs build_support/osx/Info.plist +src/pygambit/catalog +doc/catalog.csv diff --git a/.readthedocs.yml b/.readthedocs.yml index 73522dfdf..9cc997431 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -13,9 +13,13 @@ build: - libgmp-dev - pandoc - texlive-full + jobs: + # Create CSV for catalog table in docs + post_install: + - $READTHEDOCS_VIRTUALENV_PATH/bin/python build_support/catalog/update.py python: install: - requirements: doc/requirements.txt - - method: setuptools + - method: pip path: "." diff --git a/MANIFEST.in b/MANIFEST.in index d1d71b9a6..69fed7e1d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ recursive-include src/core *.cc *.h *.imp recursive-include src/games *.cc *.h *.imp recursive-include src/solvers *.c *.cc *.h *.imp +recursive-include catalog * include src/gambit.h include src/pygambit/*.pxd include src/pygambit/*.pyx diff --git a/Makefile.am b/Makefile.am index eccc92c12..4b31fe0a6 100644 --- a/Makefile.am +++ b/Makefile.am @@ -101,7 +101,6 @@ EXTRA_DIST = \ src/gui/bitmaps/zoom1.xpm \ src/gui/bitmaps/gambitrc.rc \ contrib/games/2s2x2x2.efg \ - contrib/games/2smp.efg \ contrib/games/2x2x2.efg \ contrib/games/4cards.efg \ contrib/games/artist1.efg \ @@ -169,7 +168,6 @@ EXTRA_DIST = \ contrib/games/my_3-3d.efg \ contrib/games/my_3-3e.efg \ contrib/games/my_3-4.efg \ - contrib/games/myerson.efg \ contrib/games/nim7.efg \ contrib/games/nim.efg \ contrib/games/palf2.efg \ @@ -195,7 +193,6 @@ EXTRA_DIST = \ contrib/games/2x2a.nfg \ contrib/games/2x2const.nfg \ contrib/games/2x2.nfg \ - contrib/games/2x2x2.nfg \ contrib/games/2x2x2x2.nfg \ contrib/games/2x2x2x2x2.nfg \ contrib/games/3x3x3.nfg \ @@ -224,7 +221,6 @@ EXTRA_DIST = \ contrib/games/mixdom2.nfg \ contrib/games/mixdom.nfg \ contrib/games/oneill.nfg \ - contrib/games/pd.nfg \ contrib/games/perfect1.nfg \ contrib/games/perfect2.nfg \ contrib/games/perfect3.nfg \ @@ -240,7 +236,11 @@ EXTRA_DIST = \ contrib/games/winkels.nfg \ contrib/games/yamamoto.nfg \ contrib/games/zero.nfg \ - src/README.rst + src/README.rst \ + catalog/2smp.efg \ + catalog/2x2x2.nfg \ + catalog/myerson/fig_4_2.efg \ + catalog/pd.nfg core_SOURCES = \ src/core/core.h \ diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py new file mode 100644 index 000000000..e42a18ad9 --- /dev/null +++ b/build_support/catalog/update.py @@ -0,0 +1,74 @@ +import argparse +from pathlib import Path + +import pygambit as gbt + +CATALOG_CSV = Path(__file__).parent.parent.parent / "doc" / "catalog.csv" +CATALOG_DIR = Path(__file__).parent.parent.parent / "catalog" +MAKEFILE_AM = Path(__file__).parent.parent.parent / "Makefile.am" + + +def update_makefile(): + """Update the Makefile.am with all games from the catalog.""" + + # Using rglob("*") to find files in all subdirectories + slugs = [] + for resource_path in sorted(CATALOG_DIR.rglob("*.efg")): + if resource_path.is_file(): + rel_path = resource_path.relative_to(CATALOG_DIR) + slugs.append(str(rel_path)) + for resource_path in sorted(CATALOG_DIR.rglob("*.nfg")): + if resource_path.is_file(): + rel_path = resource_path.relative_to(CATALOG_DIR) + slugs.append(str(rel_path)) + + game_files = [] + for slug in slugs: + game_files.append(f"catalog/{slug}") + game_files.sort() + + with open(MAKEFILE_AM, encoding="utf-8") as f: + content = f.readlines() + + with open(MAKEFILE_AM, "w", encoding="utf-8") as f: + in_gamefiles_section = False + for line in content: + # Add to the EXTRA_DIST after the README.rst line + if line.startswith(" src/README.rst \\"): + in_gamefiles_section = True + f.write(" src/README.rst \\\n") + for gf in game_files: + if gf == game_files[-1]: + f.write(f"\t{gf}\n") + else: + f.write(f"\t{gf} \\\n") + f.write("\n") + elif in_gamefiles_section: + if line.strip() == "": + in_gamefiles_section = False + continue # Skip old gamefiles lines + else: + f.write(line) + + with open(MAKEFILE_AM, encoding="utf-8") as f: + updated_content = f.readlines() + + if content != updated_content: + print(f"Updated {str(MAKEFILE_AM)}") + else: + print(f"No changes to add to {str(MAKEFILE_AM)}") + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser() + parser.add_argument("--build", action="store_true") + args = parser.parse_args() + + # Create CSV used by RST docs page + gbt.catalog.games().to_csv(CATALOG_CSV, index=False) + print(f"Generated {CATALOG_CSV} for use in local docs build. DO NOT COMMIT.") + + # Update the Makefile.am with the current list of catalog files + if args.build: + update_makefile() diff --git a/contrib/games/2smp.efg b/catalog/2smp.efg similarity index 100% rename from contrib/games/2smp.efg rename to catalog/2smp.efg diff --git a/contrib/games/2x2x2.nfg b/catalog/2x2x2.nfg similarity index 100% rename from contrib/games/2x2x2.nfg rename to catalog/2x2x2.nfg diff --git a/catalog/__init__.py b/catalog/__init__.py new file mode 100644 index 000000000..0e6b12338 --- /dev/null +++ b/catalog/__init__.py @@ -0,0 +1,6 @@ +from .utils import games, load + +__all__ = [ + "load", + "games", +] diff --git a/catalog/families.py b/catalog/families.py new file mode 100644 index 000000000..bcfe1dee5 --- /dev/null +++ b/catalog/families.py @@ -0,0 +1,54 @@ + +import pygambit as gbt + + +def family_games() -> dict[str, gbt.Game]: + """ + Generate a dict of games for inclusion in the catalog, + using the game families in this module. + """ + return { + "one_shot_trust": one_shot_trust(), + "oneshot_trust_unique_NE": one_shot_trust(unique_NE_variant=True), + } + + +################################################################################################ +# Families + +def one_shot_trust(unique_NE_variant: bool = False) -> gbt.Game: + """ + The unique_NE_variant makes Trust a dominant strategy, replacing the + non-singleton equilibrium component from the standard version of the game + where the Buyer plays "Not Trust" and the seller can play any mixture with + < 0.5 probability on Honor with a unique NE where the Buyer plays Trust and + the Seller plays Abuse. + + Parameters + ---------- + unique_NE_variant : bool, optional + Whether to modify the game so that it has a unique Nash equilibrium. + Defaults to False. + + Returns + ------- + gbt.Game + The constructed extensive-form game. + """ + g = gbt.Game.new_tree(players=["Buyer", "Seller"]) + g.description = "One-shot trust game with binary actions, originally from 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")) + if unique_NE_variant: + g.title = "One-shot trust game with unique NE" + g.set_outcome( + g.root.children[0].children[1], g.add_outcome(["1/2", 2], label="Untrustworthy") + ) + else: + g.title = "One-shot trust game" + g.set_outcome( + g.root.children[0].children[1], g.add_outcome([-1, 2], label="Untrustworthy") + ) + g.set_outcome(g.root.children[1], g.add_outcome([0, 0], label="Opt-out")) + return g diff --git a/contrib/games/myerson_fig_4_2.efg b/catalog/myerson/fig_4_2.efg similarity index 100% rename from contrib/games/myerson_fig_4_2.efg rename to catalog/myerson/fig_4_2.efg diff --git a/contrib/games/pd.nfg b/catalog/pd.nfg similarity index 100% rename from contrib/games/pd.nfg rename to catalog/pd.nfg diff --git a/catalog/utils.py b/catalog/utils.py new file mode 100644 index 000000000..801039dc7 --- /dev/null +++ b/catalog/utils.py @@ -0,0 +1,75 @@ +from importlib.resources import as_file, files +from pathlib import Path + +import pandas as pd + +import pygambit as gbt + +from .families import family_games + +# Use the full string path to the virtual package we created +_CATALOG_RESOURCE = files(__name__) + +READERS = { + ".nfg": gbt.read_nfg, + ".efg": gbt.read_efg, +} + + +def load(slug: str) -> gbt.Game: + """ + Load a game from the package catalog. + """ + slug = str(Path(slug)).replace("\\", "/") + + # Try to load from file + for suffix, reader in READERS.items(): + resource_path = _CATALOG_RESOURCE / f"{slug}{suffix}" + if resource_path.is_file(): + with as_file(resource_path) as path: + return reader(str(path)) + + # Try loading from family games + fg = family_games() + if slug in fg: + return fg[slug] + + # Raise error if game does not exist + raise FileNotFoundError(f"No catalog entry called {slug}") + + +def games() -> pd.DataFrame: + """ + List games available in the package catalog, including subdirectories. + """ + records: list[dict[str, str]] = [] + + # Add all the games stored as EFG/NFG files + for resource_path in sorted(_CATALOG_RESOURCE.rglob("*")): + reader = READERS.get(resource_path.suffix) + + if reader is not None and resource_path.is_file(): + # Calculate the path relative to the root resource + # and remove the suffix to get the "slug" + rel_path = resource_path.relative_to(_CATALOG_RESOURCE) + slug = rel_path.with_suffix("").as_posix() + + with as_file(resource_path) as path: + game = reader(str(path)) + records.append( + { + "Game": slug, + "Title": game.title, + } + ) + + # Add all the games from families + for slug, game in family_games().items(): + records.append( + { + "Game": slug, + "Title": game.title, + } + ) + + return pd.DataFrame.from_records(records, columns=["Game", "Title"]) diff --git a/doc/catalog.rst b/doc/catalog.rst new file mode 100644 index 000000000..cf8d45e2c --- /dev/null +++ b/doc/catalog.rst @@ -0,0 +1,8 @@ +Catalog of games +================ + +.. csv-table:: + :file: catalog.csv + :header-rows: 1 + :widths: 20, 80 + :class: tight-table diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst new file mode 100644 index 000000000..018e29ac0 --- /dev/null +++ b/doc/developer.catalog.rst @@ -0,0 +1,60 @@ +Updating the Games Catalog +========================== + +This page covers the process for contributing to and updating Gambit's :ref:`Games Catalog `. +To do so, you will need to have the `gambit` GitHub repo cloned and be able to submit pull request via GitHub; +you may wish to first review the :ref:`contributor guidelines `. + +You can add games to the catalog saved in a valid representation :ref:`format `. +Currently supported representations are: + +- `.efg` for extensive form games +- `.nfg` for normal form games + +Add new game files +------------------ + +1. **Create the game file:** + + Use either :ref:`pygambit `, the Gambit :ref:`CLI ` or :ref:`GUI ` to create and save game in a valid representation :ref:`format `. + +2. **Add the game file:** + + Create a new branch in the ``gambit`` repo. Add your new game file(s) inside the ``catalog`` dir and commit them. + +3. **Update the catalog:** + + Use the ``update.py`` script to update Gambit's documentation & build files. + + .. code-block:: bash + + python build_support/catalog/update.py --build + + .. note:: + + Run this script in a Python environment where ``pygambit`` itself is also :ref:`installed `. + + .. warning:: + + Running the script with the ``--build`` flag updates `Makefile.am`. If you moved games that were previously in `contrib/games` you'll need to also manually remove those files from `EXTRA_DIST`. + +4. **Submit a pull request to GitHub with all changes.** + + .. warning:: + + Make sure you commit all changed files e.g. run ``git add --all`` before committing and pushing. + +Code new games & add game families +---------------------------------- + +1. **Add the game code:** + + Open `catalog/families.py` and create a new function, or modify an existing one. Ensure your function returns a ``Game`` object. + You may wish to vary the game title and/or description based on the chosen parameters. + +2. **Update the catalog:** + + Update the dictionary returned by ``family_games()`` in `catalog/families.py` with all variants of your game(s) you want in the catalog. + Ensure each entry has unique game slug as key (this will be used by ``pygambit.catalog.load('slug')``), and returns a call of the function with specific parameters. + +3. **Submit a pull request to GitHub with all changes.** diff --git a/doc/developer.contributing.rst b/doc/developer.contributing.rst index f86939850..02ca37747 100644 --- a/doc/developer.contributing.rst +++ b/doc/developer.contributing.rst @@ -1,3 +1,5 @@ +.. _contributing: + Contributing to Gambit ====================== diff --git a/doc/developer.rst b/doc/developer.rst index 0a1512659..b954f0850 100644 --- a/doc/developer.rst +++ b/doc/developer.rst @@ -11,3 +11,4 @@ This section contains information for developers who want to contribute to the G developer.build developer.contributing + developer.catalog diff --git a/doc/index.rst b/doc/index.rst index 79076153a..72843860d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -64,6 +64,7 @@ We recommended most new users install the PyGambit Python package and read the a pygambit tools gui + catalog samples developer formats diff --git a/doc/tutorials/01_quickstart.ipynb b/doc/tutorials/01_quickstart.ipynb index 06ec6f91e..62949a4b4 100644 --- a/doc/tutorials/01_quickstart.ipynb +++ b/doc/tutorials/01_quickstart.ipynb @@ -53,18 +53,7 @@ "execution_count": null, "id": "2060c1ed", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pygambit.gambit.Game" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "n_strategies = [2, 2]\n", "g = gbt.Game.new_table(n_strategies, title=\"Prisoner's Dilemma\")\n", @@ -83,7 +72,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "9d8203e8", "metadata": {}, "outputs": [], @@ -111,7 +100,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "61030607", "metadata": {}, "outputs": [], @@ -135,25 +124,10 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "caecc334", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "

Prisoner's Dilemma

\n", - "
CooperateDefect
Cooperate-1,-1-3,0
Defect0,-3-2,-2
\n" - ], - "text/plain": [ - "Game(title='Prisoner's Dilemma')" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# View the payout matrix\n", "g" @@ -192,22 +166,7 @@ "execution_count": null, "id": "843ba7f3", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "

Another Prisoner's Dilemma

\n", - "
12
1-1,-1-3,0
20,-3-2,-2
\n" - ], - "text/plain": [ - "Game(title='Another Prisoner's Dilemma')" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "player1_payoffs = np.array([[-1, -3], [0, -2]])\n", "player2_payoffs = np.transpose(player1_payoffs)\n", @@ -233,19 +192,10 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "5ee752c4", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-1\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "tom_payoffs, jerry_payoffs = g.to_arrays(\n", " # dtype=float\n", @@ -270,21 +220,10 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "a81c06c7", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pygambit.nash.NashComputationResult" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "result = gbt.nash.enumpure_solve(g)\n", "type(result)" @@ -300,21 +239,10 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "bd395180", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "1" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "len(result.equilibria)" ] @@ -329,24 +257,10 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "76570ebc", "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\left[\\left[0,1\\right],\\left[0,1\\right]\\right]$" - ], - "text/plain": [ - "[[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "msp = result.equilibria[0]\n", "msp" @@ -354,21 +268,10 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "6e8cfcde", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pygambit.gambit.MixedStrategyProfileRational" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "type(msp)" ] @@ -385,27 +288,10 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "980bf6b1", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Tom plays the equilibrium strategy:\n", - "Probability of cooperating: 0\n", - "Probability of defecting: 1\n", - "Payoff: -2\n", - "\n", - "Jerry plays the equilibrium strategy:\n", - "Probability of cooperating: 0\n", - "Probability of defecting: 1\n", - "Payoff: -2\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "for player in g.players:\n", " print(f\"{player.label} plays the equilibrium strategy:\")\n", @@ -417,11 +303,51 @@ }, { "cell_type": "markdown", - "id": "24f36b0d", + "id": "c27c50f0-e8cc-4160-9975-aa02b33c6879", "metadata": {}, "source": [ "The equilibrium shows that both players are playing their dominant strategy, which is to defect. This is because defecting is the best response to the other player's strategy, regardless of what that strategy is.\n", "\n", + "Loading games from the catalog \n", + "------------------------------\n", + "\n", + "Gambit includes a catalog of standard games that can be loaded directly by name. You can list all the available games like so:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e1b060fb-94cc-432d-a9cc-4ccae5908f79", + "metadata": {}, + "outputs": [], + "source": [ + "gbt.catalog.games()" + ] + }, + { + "cell_type": "markdown", + "id": "3030ee7e-2d5e-4560-ab1b-7c865d0fe19d", + "metadata": {}, + "source": [ + "You can then load a specific game by its name. For example, to load the \"Prisoner's Dilemma\" game from the catalog, you would do the following:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ee2c3bd-22d1-4c8e-996c-cd9e0dfd64cc", + "metadata": {}, + "outputs": [], + "source": [ + "g = gbt.catalog.load(\"pd\")\n", + "g" + ] + }, + { + "cell_type": "markdown", + "id": "24f36b0d", + "metadata": {}, + "source": [ "Saving and reading strategic form games to and from file\n", "--------------------\n", "\n", @@ -454,20 +380,9 @@ "execution_count": null, "id": "4119a2ac", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pygambit.gambit.Game" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "# gbt.read_nfg(\"test_games/prisoners_dilemma.nfg\")" + "# gbt.read_nfg(\"prisoners_dilemma.nfg\")" ] } ], diff --git a/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb index 68b78ee11..3c3ac80b1 100644 --- a/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb +++ b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb @@ -35,235 +35,225 @@ { "data": { "image/svg+xml": [ - "\n", + "\n", "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", + "\n", "\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", - "\n", - "\n", - "\n", + "\n", "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", - "\n", - "\n", - "\n", + "\n", "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", - "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "\n", "" ], "text/plain": [ @@ -280,7 +270,7 @@ "\n", "import pygambit as gbt\n", "\n", - "g = gbt.read_efg(\"../../../contrib/games/myerson_fig_4_2.efg\")\n", + "g = gbt.catalog.load(\"myerson/fig_4_2\")\n", "draw_tree(g)" ] }, @@ -699,7 +689,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.9" + "version": "3.13.5" } }, "nbformat": 4, diff --git a/doc/tutorials/advanced_tutorials/starting_points.ipynb b/doc/tutorials/advanced_tutorials/starting_points.ipynb index fb976245b..9e8f72d5e 100644 --- a/doc/tutorials/advanced_tutorials/starting_points.ipynb +++ b/doc/tutorials/advanced_tutorials/starting_points.ipynb @@ -25,7 +25,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "493cafb8", "metadata": {}, "outputs": [ @@ -33,7 +33,7 @@ "data": { "text/html": [ "

2x2x2 Example from McKelvey-McLennan, with 9 Nash equilibria, 2 totally mixed

\n", - "
Subtable with strategies:
Player 3 Strategy 1
12
19,8,120,0,0
20,0,09,8,2
Subtable with strategies:
Player 3 Strategy 2
12
10,0,03,4,6
23,4,60,0,0
\n" + "
Subtable with strategies:
Player 3 Strategy 1
12
19,8,120,0,0
20,0,09,8,2
\n" ], "text/plain": [ "Game(title='2x2x2 Example from McKelvey-McLennan, with 9 Nash equilibria, 2 totally mixed')" @@ -45,8 +45,11 @@ } ], "source": [ + "import numpy as np\n", + "\n", "import pygambit as gbt\n", - "g = gbt.read_nfg(\"../../2x2x2.nfg\")\n", + "\n", + "g = gbt.catalog.load(\"2x2x2\")\n", "g" ] }, @@ -93,10 +96,10 @@ { "data": { "text/latex": [ - "$\\left[[0.3999999026224355, 0.6000000973775644],[0.49999981670851457, 0.5000001832914854],[0.3333329684317666, 0.6666670315682334]\\right]$" + "$\\left[[0.3999998880351315, 0.6000001119648686],[0.5000000683119051, 0.4999999316880949],[0.3333335574724357, 0.6666664425275644]\\right]$" ], "text/plain": [ - "[[0.3999999026224355, 0.6000000973775644], [0.49999981670851457, 0.5000001832914854], [0.3333329684317666, 0.6666670315682334]]" + "[[0.3999998880351315, 0.6000001119648686], [0.5000000683119051, 0.4999999316880949], [0.3333335574724357, 0.6666664425275644]]" ] }, "execution_count": 3, @@ -153,10 +156,10 @@ { "data": { "text/latex": [ - "$\\left[[1.0, 0.0],[0.9999999944750116, 5.524988446860122e-09],[0.9999999991845827, 8.154173380971617e-10]\\right]$" + "$\\left[[1.0, 0.0],[0.9999999916299683, 8.370031632789431e-09],[1.0, 0.0]\\right]$" ], "text/plain": [ - "[[1.0, 0.0], [0.9999999944750116, 5.524988446860122e-09], [0.9999999991845827, 8.154173380971617e-10]]" + "[[1.0, 0.0], [0.9999999916299683, 8.370031632789431e-09], [1.0, 0.0]]" ] }, "execution_count": 5, @@ -185,10 +188,10 @@ { "data": { "text/latex": [ - "$\\left[[0.7187961367413075, 0.2812038632586925],[0.1291105793795489, 0.8708894206204512],[0.12367227612277114, 0.876327723877229]\\right]$" + "$\\left[[0.5172260574334439, 0.48277394256655615],[0.5372523987305369, 0.462747601269463],[0.8261013405886477, 0.17389865941135238]\\right]$" ], "text/plain": [ - "[[0.7187961367413075, 0.2812038632586925], [0.1291105793795489, 0.8708894206204512], [0.12367227612277114, 0.876327723877229]]" + "[[0.5172260574334439, 0.48277394256655615], [0.5372523987305369, 0.462747601269463], [0.8261013405886477, 0.17389865941135238]]" ] }, "execution_count": 6, @@ -210,10 +213,10 @@ { "data": { "text/latex": [ - "$\\left[[0.5000003932357804, 0.4999996067642197],[0.3999998501612186, 0.6000001498387814],[0.2500001518113522, 0.7499998481886477]\\right]$" + "$\\left[[0.49999999983005444, 0.5000000001699455],[0.4999999947343101, 0.5000000052656899],[0.9999989483984311, 1.0516015689453635e-06]\\right]$" ], "text/plain": [ - "[[0.5000003932357804, 0.4999996067642197], [0.3999998501612186, 0.6000001498387814], [0.2500001518113522, 0.7499998481886477]]" + "[[0.49999999983005444, 0.5000000001699455], [0.4999999947343101, 0.5000000052656899], [0.9999989483984311, 1.0516015689453635e-06]]" ] }, "execution_count": 7, @@ -254,7 +257,6 @@ } ], "source": [ - "import numpy as np\n", "gen = np.random.default_rng(seed=1234567890)\n", "p1 = g.random_strategy_profile(gen=gen)\n", "gen = np.random.default_rng(seed=1234567890)\n", @@ -427,7 +429,7 @@ ], "metadata": { "kernelspec": { - "display_name": "gambitvenv313", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, diff --git a/pyproject.toml b/pyproject.toml index 71ea25bfc..270f77d06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers=[ dependencies = [ "numpy", "scipy", + "pandas", ] [project.urls] @@ -87,5 +88,12 @@ markers = [ "slow: all time-consuming tests", ] +[tool.setuptools] +packages = ["pygambit", "pygambit.catalog"] +package-dir = { "pygambit" = "src/pygambit", "pygambit.catalog" = "catalog" } + +[tool.setuptools.package-data] +"pygambit.catalog" = ["*"] + [tool.setuptools.dynamic] version = {file = "build_support/GAMBIT_VERSION"} diff --git a/setup.py b/setup.py index c8e7125f5..4a1e82b5a 100644 --- a/setup.py +++ b/setup.py @@ -103,8 +103,6 @@ def solver_library_config(library_name: str, paths: list) -> tuple: libraries=[cppgambit_bimatrix, cppgambit_liap, cppgambit_logit, cppgambit_simpdiv, cppgambit_gtracer, cppgambit_enumpoly, cppgambit_games, cppgambit_core], - package_dir={"": "src"}, - packages=["pygambit"], ext_modules=Cython.Build.cythonize(libgambit, language_level="3str", compiler_directives={"binding": True}) diff --git a/src/pygambit/__init__.py b/src/pygambit/__init__.py index 1d6f730bf..8b72c0682 100644 --- a/src/pygambit/__init__.py +++ b/src/pygambit/__init__.py @@ -26,6 +26,7 @@ nash, # noqa: F401 qre, # noqa: F401 supports, # noqa: F401 + catalog, ) import importlib.metadata diff --git a/tests/test_catalog.py b/tests/test_catalog.py new file mode 100644 index 000000000..0f4e3539b --- /dev/null +++ b/tests/test_catalog.py @@ -0,0 +1,51 @@ +import pandas as pd +import pytest + +import pygambit as gbt + + +def test_catalog_load_efg(): + """Test loading an extensive form game""" + g = gbt.catalog.load("2smp") + assert isinstance(g, gbt.Game) + assert g.title == "Two-stage matching pennies game" + + +def test_catalog_load_nfg(): + """Test loading a normal form game""" + g = gbt.catalog.load("pd") + assert isinstance(g, gbt.Game) + assert g.title == "Two person Prisoner's Dilemma game" + + +def test_catalog_load_invalid_slug(): + """Test loading an invalid game slug""" + with pytest.raises(FileNotFoundError): + gbt.catalog.load("invalid_slug") + + +def test_catalog_load_subdir_slug(): + """Test loading a game from catalog/somedir""" + g = gbt.catalog.load("myerson/fig_4_2") + assert isinstance(g, gbt.Game) + + +def test_catalog_load_family_game(): + """Test loading a game generated from code with a game family func.""" + g = gbt.catalog.load("one_shot_trust") + assert isinstance(g, gbt.Game) + + +def test_catalog_games(): + """Test games() function returns df of game slugs and titles""" + all_games = gbt.catalog.games() + slugs = list(all_games.Game) + assert isinstance(all_games, pd.DataFrame) + assert len(all_games) > 0 + assert "2smp" in slugs + assert "Two-stage matching pennies game" in list(all_games.Title) + # Check slug of game in subdir + assert "myerson/fig_4_2" in slugs + assert "myerson_fig_4_2" not in slugs + # Check family game present + assert "one_shot_trust" in slugs