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..1bbd415f8 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 src/pygambit/update_catalog.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..ca6e0dfd8 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/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..4164041f5 --- /dev/null +++ b/catalog/__init__.py @@ -0,0 +1,51 @@ +from importlib.resources import as_file, files + +import pandas as pd + +import pygambit as gbt + +# 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. + """ + for suffix, reader in READERS.items(): + resource_path = _CATALOG_RESOURCE / f"{slug}{suffix}" + + if resource_path.is_file(): + # as_file ensures we have a real filesystem path for the reader + with as_file(resource_path) as path: + return reader(str(path)) + + raise FileNotFoundError(f"No catalog entry called {slug}.nfg or {slug}.efg") + + +def games() -> pd.DataFrame: + """ + List games available in the package catalog. + """ + records: list[dict[str, str]] = [] + + # iterdir() works directly on the Traversable object + for resource_path in sorted(_CATALOG_RESOURCE.iterdir()): + reader = READERS.get(resource_path.suffix) + + if reader is not None and resource_path.is_file(): + with as_file(resource_path) as path: + game = reader(str(path)) + records.append( + { + "Game": resource_path.stem, + "Title": game.title, + } + ) + + return pd.DataFrame.from_records(records, columns=["Game", "Title"]) 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/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..40680c8e9 --- /dev/null +++ b/doc/developer.catalog.rst @@ -0,0 +1,45 @@ +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 games +------------- + +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_catalog.py`` script to update Gambit's documentation & build files. + + .. code-block:: bash + + python src/pygambit/update_catalog.py --build + + .. note:: + + Run this script in a Python environment where ``pygambit`` itself is also :ref:`installed `. + + .. warning:: + + This script updates `Makefile.am` with the game file added to the catalog, but if you moved games that were previously in `contrib/games` you'll want to 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. 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..7e4de7599 100644 --- a/doc/tutorials/01_quickstart.ipynb +++ b/doc/tutorials/01_quickstart.ipynb @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "c58d382d", "metadata": {}, "outputs": [], @@ -50,7 +50,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "2060c1ed", "metadata": {}, "outputs": [ @@ -60,7 +60,7 @@ "pygambit.gambit.Game" ] }, - "execution_count": 1, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -83,7 +83,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "9d8203e8", "metadata": {}, "outputs": [], @@ -111,7 +111,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "61030607", "metadata": {}, "outputs": [], @@ -135,7 +135,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "caecc334", "metadata": {}, "outputs": [ @@ -149,7 +149,7 @@ "Game(title='Prisoner's Dilemma')" ] }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -189,7 +189,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "843ba7f3", "metadata": {}, "outputs": [ @@ -203,7 +203,7 @@ "Game(title='Another Prisoner's Dilemma')" ] }, - "execution_count": 5, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -233,7 +233,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "5ee752c4", "metadata": {}, "outputs": [ @@ -270,7 +270,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "a81c06c7", "metadata": {}, "outputs": [ @@ -280,7 +280,7 @@ "pygambit.nash.NashComputationResult" ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -300,7 +300,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "bd395180", "metadata": {}, "outputs": [ @@ -310,7 +310,7 @@ "1" ] }, - "execution_count": 8, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -329,7 +329,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "id": "76570ebc", "metadata": {}, "outputs": [ @@ -342,7 +342,7 @@ "[[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]]" ] }, - "execution_count": 9, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -354,7 +354,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "id": "6e8cfcde", "metadata": {}, "outputs": [ @@ -364,7 +364,7 @@ "pygambit.gambit.MixedStrategyProfileRational" ] }, - "execution_count": 10, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -385,7 +385,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "id": "980bf6b1", "metadata": {}, "outputs": [ @@ -417,11 +417,129 @@ }, { "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": 13, + "id": "e1b060fb-94cc-432d-a9cc-4ccae5908f79", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
GameTitle
02smpTwo-stage matching pennies game
12x2x22x2x2 Example from McKelvey-McLennan, with 9 N...
2myerson_fig_4_2Myerson (1991) Fig 4.2
3pdTwo person Prisoner's Dilemma game
\n", + "
" + ], + "text/plain": [ + " Game Title\n", + "0 2smp Two-stage matching pennies game\n", + "1 2x2x2 2x2x2 Example from McKelvey-McLennan, with 9 N...\n", + "2 myerson_fig_4_2 Myerson (1991) Fig 4.2\n", + "3 pd Two person Prisoner's Dilemma game" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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": 14, + "id": "9ee2c3bd-22d1-4c8e-996c-cd9e0dfd64cc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "

Two person Prisoner's Dilemma game

\n", + "
12
19,90,10
210,01,1
\n" + ], + "text/plain": [ + "Game(title='Two person Prisoner's Dilemma game')" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "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", @@ -433,7 +551,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "f58eaa77", "metadata": {}, "outputs": [], @@ -451,23 +569,12 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "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..c7e16ba80 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)" ] }, @@ -436,7 +426,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "[[4.2517925671604327e-07, 0.49999911111761514, 0.5000004637031282], [0.3333333517938241, 0.6666666482061759]]\n" + "[[6.949101896011271e-07, 0.49999858461819596, 0.5000007204716144], [0.33333333942537524, 0.6666666605746248]]\n" ] } ], @@ -455,8 +445,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Liap value: 4.43446520109796e-14\n", - "Max regret: 1.694170896904268e-07\n" + "Liap value: 1.0863970174089946e-13\n", + "Max regret: 2.407747583532682e-07\n" ] } ], @@ -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/games/writer.cc b/src/games/writer.cc index 744f3efad..47baedc1e 100644 --- a/src/games/writer.cc +++ b/src/games/writer.cc @@ -91,6 +91,7 @@ std::string WriteHTMLFile(const Game &p_game, const GamePlayer &p_rowPlayer, } theHtml += ""; + break; } theHtml += "\n"; return theHtml; 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/src/pygambit/update_catalog.py b/src/pygambit/update_catalog.py new file mode 100644 index 000000000..c6787f170 --- /dev/null +++ b/src/pygambit/update_catalog.py @@ -0,0 +1,66 @@ +import argparse +from pathlib import Path + +import pygambit as gbt + +CATALOG_CSV = Path(__file__).parent.parent.parent / "doc" / "catalog.csv" +MAKEFILE_AM = Path(__file__).parent.parent.parent / "Makefile.am" + + +def update_makefile(): + """Update the Makefile.am with all games from the catalog.""" + catalog_dir = Path(__file__).parent.parent.parent / "catalog" + efg_files = list(catalog_dir.rglob("*.efg")) + nfg_files = list(catalog_dir.rglob("*.nfg")) + + game_files = [] + for entry in efg_files + nfg_files: + filename = str(entry).split("/")[-1] + game_files.append(f"catalog/{filename}") + 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/tests/test_catalog.py b/tests/test_catalog.py new file mode 100644 index 000000000..f281cc353 --- /dev/null +++ b/tests/test_catalog.py @@ -0,0 +1,33 @@ +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_games(): + """Test games() function returns df of game slugs and titles""" + all_games = gbt.catalog.games() + assert isinstance(all_games, pd.DataFrame) + assert len(all_games) > 0 + assert "2smp" in list(all_games.Game) + assert "Two-stage matching pennies game" in list(all_games.Title)