diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5b88b5f528..48f092c06e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -63,3 +63,26 @@ jobs: run: python -m pip install cython-lint - name: cython-lint run: cython-lint . + + catalog-am-sync: + name: catalog.am is up to date + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + steps: + - uses: actions/checkout@v7 + - name: Setup Python + uses: actions/setup-python@v7 + with: + python-version: "3.14" + - name: Install pygambit (editable, picks up catalog/games directly) + run: | + python -m pip install -v -e ".[doc]" + pip install "gtdraw @ git+https://github.com/gambitproject/gtdraw.git@main" + - name: Regenerate catalog/catalog.am + run: python catalog/build.py --build --skip-table + - name: Fail if catalog.am is out of sync with catalog/games + run: | + if ! git diff --exit-code -- catalog/catalog.am; then + echo "::error::catalog/catalog.am is out of sync with catalog/games. Run 'python catalog/build.py --build' and commit the result." + exit 1 + fi diff --git a/.gitignore b/.gitignore index e708935781..68c08bd1cc 100644 --- a/.gitignore +++ b/.gitignore @@ -45,12 +45,14 @@ Gambit.app/* Gambit.app/* *.ipynb_checkpoints doc/**/*.ef +catalog/doc/**/*.ef build_support/msw/gambit.wxs build_support/osx/Info.plist src/pygambit/catalog -doc/catalog_table.rst -catalog/img/**/*.pdf -catalog/img/**/*.png -catalog/img/**/*.tex -catalog/img/**/*.ef -catalog/img/**/*.svg +catalog/doc/_table.rst +catalog/doc/_build +catalog/games/img/**/*.pdf +catalog/games/img/**/*.png +catalog/games/img/**/*.tex +catalog/games/img/**/*.ef +catalog/games/img/**/*.svg diff --git a/.readthedocs.yml b/.readthedocs.yml index 13359f0490..28c50773ec 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -17,9 +17,9 @@ build: - pdf2svg jobs: post_install: + # Required by the tutorials (jupyter_tikz game-tree rendering), not by the catalog — + # the catalog is now built as its own separate RTD project (see catalog/doc/). - pip install "gtdraw @ git+https://github.com/gambitproject/gtdraw.git@main" - # Create RST for catalog table in docs - - $READTHEDOCS_VIRTUALENV_PATH/bin/python build_support/catalog/update.py python: install: diff --git a/MANIFEST.in b/MANIFEST.in index ff09d59924..c262fa2165 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,7 @@ recursive-include src/core *.cc *.h recursive-include src/games *.cc *.h *.imp recursive-include src/solvers *.cc *.h *.imp -recursive-include catalog * +recursive-include catalog/games * include src/gambit.h include src/games.h include src/solvers.h diff --git a/Makefile.am b/Makefile.am index 232ed6e38e..f64b1b628e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -20,7 +20,7 @@ ## Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. ## -include build_support/catalog/catalog.am +include catalog/catalog.am EXTRA_DIST = \ build_support/GAMBIT_VERSION \ diff --git a/build_support/catalog/catalog.am b/build_support/catalog/catalog.am deleted file mode 100644 index cde07fd95a..0000000000 --- a/build_support/catalog/catalog.am +++ /dev/null @@ -1,53 +0,0 @@ -CATALOG_FILES = \ - catalog/books/myerson1991/fig2_1.efg \ - catalog/books/myerson1991/fig4_2.efg \ - catalog/books/shohamleytonbrown2008/fig5_1.efg \ - catalog/books/shohamleytonbrown2008/fig5_10.efg \ - catalog/books/shohamleytonbrown2008/fig5_10__original_layout.ef \ - catalog/books/shohamleytonbrown2008/fig5_11.efg \ - catalog/books/shohamleytonbrown2008/fig5_11__original_layout.ef \ - catalog/books/shohamleytonbrown2008/fig5_12.efg \ - catalog/books/shohamleytonbrown2008/fig5_12__original_layout.ef \ - catalog/books/shohamleytonbrown2008/fig5_15.efg \ - catalog/books/shohamleytonbrown2008/fig5_15__original_layout.ef \ - catalog/books/shohamleytonbrown2008/fig5_1__original_layout.ef \ - catalog/books/shohamleytonbrown2008/fig5_2.efg \ - catalog/books/shohamleytonbrown2008/fig5_2__original_layout.ef \ - catalog/books/shohamleytonbrown2008/fig5_9.efg \ - catalog/books/shohamleytonbrown2008/fig5_9__original_layout.ef \ - catalog/books/shohamleytonbrown2008/fig6_2.efg \ - catalog/books/shohamleytonbrown2008/fig6_2__original_layout.ef \ - catalog/books/shohamleytonbrown2008/fig6_8.efg \ - catalog/books/shohamleytonbrown2008/fig6_8__original_layout.ef \ - catalog/books/vonstengel2022/fig10.1.efg \ - catalog/books/vonstengel2022/fig10.12.efg \ - catalog/books/vonstengel2022/fig10.5.efg \ - catalog/books/vonstengel2022/fig10.7.efg \ - catalog/books/watson2013/exercise29_6.efg \ - catalog/books/watson2013/fig29_1.efg \ - catalog/conf/itcs/jakobsen2016/fig1a.efg \ - catalog/conf/itcs/jakobsen2016/fig1b.efg \ - catalog/conf/itcs/jakobsen2016/fig1c.efg \ - catalog/conf/itcs/jakobsen2016/fig3.efg \ - catalog/journals/dcg/vonstengel1999/6x6_game_with_75_eq.nfg \ - catalog/journals/dcg/vonstengel1999/6x6_game_with_75_eq_small_payoffs.nfg \ - catalog/journals/geb/bagwell1995.efg \ - catalog/journals/geb/gilboa1997/fig1.efg \ - catalog/journals/geb/gilboa1997/fig2.efg \ - catalog/journals/geb/wichardt2008.efg \ - catalog/journals/ijgt/nau2004/sec3.nfg \ - catalog/journals/ijgt/nau2004/sec4.nfg \ - catalog/journals/ijgt/nau2004/sec5.nfg \ - catalog/journals/ijgt/nau2004/sec6.nfg \ - catalog/journals/ijgt/selten1975/fig1.efg \ - catalog/journals/ijgt/selten1975/fig2.efg \ - catalog/journals/ijgt/selten1975/fig3.efg \ - catalog/journals/mor/vonstengelforges2008/fig1.efg \ - catalog/journals/mor/vonstengelforges2008/fig1__Original_Layout.ef \ - catalog/journals/mor/vonstengelforges2008/fig6.efg \ - catalog/journals/mor/vonstengelforges2008/fig6__Original_Layout.ef \ - catalog/journals/mor/vonstengelforges2008/fig9.efg \ - catalog/journals/mor/vonstengelforges2008/fig9__Original_Layout.ef \ - catalog/journals/other/reiley2008/fig1.efg \ - catalog/journals/other/shapley1974/fig2.nfg \ - catalog/journals/other/shapley1974/fig3.nfg diff --git a/catalog/.readthedocs.yml b/catalog/.readthedocs.yml new file mode 100644 index 0000000000..7dee191a91 --- /dev/null +++ b/catalog/.readthedocs.yml @@ -0,0 +1,33 @@ +version: 2 + +# This config file is for a SEPARATE ReadTheDocs project from the main Gambit docs +# (which use the root-level .readthedocs.yml). Setting it up requires creating a second +# project in the ReadTheDocs dashboard for this same GitHub repo, configured to use this +# file instead of the root one — that dashboard step is not something a git commit can do. + +sphinx: + configuration: catalog/doc/conf.py + +formats: all + +build: + os: ubuntu-22.04 + tools: + python: "3.14" + apt_packages: + - libgmp-dev + - texlive-full + - imagemagick + - pdf2svg + jobs: + post_install: + - pip install "gtdraw @ git+https://github.com/gambitproject/gtdraw.git@main" + # Regenerate catalog/doc/_table.rst (and the game images) from the current catalog. + - $READTHEDOCS_VIRTUALENV_PATH/bin/python catalog/build.py + +python: + install: + - method: pip + path: "." + extra_requirements: + - doc diff --git a/catalog/VERSION b/catalog/VERSION new file mode 100644 index 0000000000..6e8bf73aa5 --- /dev/null +++ b/catalog/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/build_support/catalog/update.py b/catalog/build.py similarity index 89% rename from build_support/catalog/update.py rename to catalog/build.py index 5cc15443b7..1aa76eba3b 100644 --- a/build_support/catalog/update.py +++ b/catalog/build.py @@ -10,11 +10,11 @@ import pygambit as gbt -CATALOG_RST_TABLE = Path(__file__).parent.parent.parent / "doc" / "catalog_table.rst" -CATALOG_DIR = Path(__file__).parent.parent.parent / "catalog" -MAKEFILE_AM = Path(__file__).parent.parent.parent / "Makefile.am" +CATALOG_RST_TABLE = Path(__file__).parent / "doc" / "_table.rst" +CATALOG_DIR = Path(__file__).parent / "games" +MAKEFILE_AM = Path(__file__).parent.parent / "Makefile.am" GTDRAW_SETTINGS_CONFIG = Path(__file__).parent / "gtdraw_settings.yaml" -CATALOG_HIERARCHY_CONFIG = Path(__file__).parent / "catalog_hierarchy.yaml" +CATALOG_HIERARCHY_CONFIG = Path(__file__).parent / "hierarchy.yaml" SUPPORTED_GAME_FORMATS = {"efg", "nfg"} @@ -60,8 +60,8 @@ def catalog_ef_file_variants(slug: str, catalog_dir: Path) -> list[dict] | None: File-naming convention:: - catalog/{slug}.ef primary variant → label "Default" - catalog/{slug}__{suffix}.ef additional variant → label derived from suffix + catalog/games/{slug}.ef primary variant → label "Default" + catalog/games/{slug}__{suffix}.ef additional variant → label derived from suffix The suffix part (after ``__``) is title-cased with underscores replaced by spaces, e.g. ``fig1__very_wide.ef`` → label "Very Wide". @@ -230,10 +230,10 @@ def _write_game_entry( for variant in ef_variants: vkey = variant["variant_key"] for ext in ["ef", "tex", "png", "pdf", "svg"]: - download_links.append(f":download:`{vkey}.{ext} <../catalog/img/{vkey}.{ext}>`") + download_links.append(f":download:`{vkey}.{ext} <../games/img/{vkey}.{ext}>`") else: for ext in all_exts: - download_links.append(f":download:`{slug}.{ext} <../catalog/img/{slug}.{ext}>`") + download_links.append(f":download:`{slug}.{ext} <../games/img/{slug}.{ext}>`") f.write(f"{i1}.. dropdown:: Download game and image files\n") f.write(f"{i1} \n") f.write(f"{i2}{' '.join(download_links)}\n") @@ -257,7 +257,7 @@ def _write_game_entry( f.write(f"{i4}import pygambit\n") f.write(f"{i4}from gtdraw import draw\n") if variant["ef_path"].exists(): - f.write(f'{i4}draw("../catalog/{vkey}.ef", {settings_str})\n') + f.write(f'{i4}draw("../games/{vkey}.ef", {settings_str})\n') else: f.write(f'{i4}draw(pygambit.catalog.load("{slug}"), {settings_str})\n') f.write(f"{i2}\n") @@ -274,13 +274,13 @@ def _write_game_entry( ) curated_ef = catalog_dir / f"{slug}.ef" if curated_ef.exists(): - f.write(f'{i2}draw("../catalog/{slug}.ef", {settings_str})\n') + f.write(f'{i2}draw("../games/{slug}.ef", {settings_str})\n') else: f.write(f'{i2}draw(pygambit.catalog.load("{slug}"), {settings_str})\n') elif row["Format"] == "nfg": f.write( f'{i2}draw(pygambit.catalog.load("{slug}"), ' - f'save_to="../catalog/img/{slug}.png")\n' + f'save_to="../games/img/{slug}.png")\n' ) f.write(f"{i1}\n") @@ -376,7 +376,7 @@ def update_makefile( game_files = [] for slug in slugs: - game_files.append(f"catalog/{slug}") + game_files.append(f"catalog/games/{slug}") game_files.sort() if am_path.exists(): @@ -404,15 +404,16 @@ def update_makefile( parser = argparse.ArgumentParser( description=( "Update Gambit catalog documentation and build files. " - "Always regenerates doc/catalog_table.rst from the current catalog. " - "Run from the repo root or build_support/catalog/." + "Regenerates catalog/doc/_table.rst from the current catalog by default; " + "pass --skip-table to skip this (it requires a LaTeX toolchain via gtdraw). " + "Run from the repo root or catalog/." ) ) parser.add_argument( "--build", action="store_true", help=( - "Also update build_support/catalog/catalog.am with the current list of " + "Also update catalog/catalog.am with the current list of " "catalog game files. Required after adding or removing games." ), ) @@ -425,13 +426,24 @@ def update_makefile( "gtdraw_settings.yaml." ), ) + parser.add_argument( + "--skip-table", + action="store_true", + help=( + "Skip regenerating catalog/doc/_table.rst and any missing game images. " + "Table/image generation requires a LaTeX toolchain (gtdraw's tex/pdf/png/svg " + "functions); use this with --build in contexts (e.g. CI) that only need " + "catalog.am checked or updated." + ), + ) args = parser.parse_args() - # Create RST list-table used by doc/catalog.rst - df = _catalog_games() - _warn_missing_descriptions(df) - generate_rst_table(df, CATALOG_RST_TABLE, regenerate_images=args.regenerate_images) - print(f"Generated {CATALOG_RST_TABLE} for use in local docs build. DO NOT COMMIT.") + if not args.skip_table: + # Create RST list-table used by catalog/doc/index.rst + df = _catalog_games() + _warn_missing_descriptions(df) + generate_rst_table(df, CATALOG_RST_TABLE, regenerate_images=args.regenerate_images) + print(f"Generated {CATALOG_RST_TABLE} for use in local docs build. DO NOT COMMIT.") if args.build: # Update the Makefile.am with the current list of catalog files update_makefile() diff --git a/catalog/catalog.am b/catalog/catalog.am new file mode 100644 index 0000000000..d91d45e75e --- /dev/null +++ b/catalog/catalog.am @@ -0,0 +1,53 @@ +CATALOG_FILES = \ + catalog/games/books/myerson1991/fig2_1.efg \ + catalog/games/books/myerson1991/fig4_2.efg \ + catalog/games/books/shohamleytonbrown2008/fig5_1.efg \ + catalog/games/books/shohamleytonbrown2008/fig5_10.efg \ + catalog/games/books/shohamleytonbrown2008/fig5_10__original_layout.ef \ + catalog/games/books/shohamleytonbrown2008/fig5_11.efg \ + catalog/games/books/shohamleytonbrown2008/fig5_11__original_layout.ef \ + catalog/games/books/shohamleytonbrown2008/fig5_12.efg \ + catalog/games/books/shohamleytonbrown2008/fig5_12__original_layout.ef \ + catalog/games/books/shohamleytonbrown2008/fig5_15.efg \ + catalog/games/books/shohamleytonbrown2008/fig5_15__original_layout.ef \ + catalog/games/books/shohamleytonbrown2008/fig5_1__original_layout.ef \ + catalog/games/books/shohamleytonbrown2008/fig5_2.efg \ + catalog/games/books/shohamleytonbrown2008/fig5_2__original_layout.ef \ + catalog/games/books/shohamleytonbrown2008/fig5_9.efg \ + catalog/games/books/shohamleytonbrown2008/fig5_9__original_layout.ef \ + catalog/games/books/shohamleytonbrown2008/fig6_2.efg \ + catalog/games/books/shohamleytonbrown2008/fig6_2__original_layout.ef \ + catalog/games/books/shohamleytonbrown2008/fig6_8.efg \ + catalog/games/books/shohamleytonbrown2008/fig6_8__original_layout.ef \ + catalog/games/books/vonstengel2022/fig10.1.efg \ + catalog/games/books/vonstengel2022/fig10.12.efg \ + catalog/games/books/vonstengel2022/fig10.5.efg \ + catalog/games/books/vonstengel2022/fig10.7.efg \ + catalog/games/books/watson2013/exercise29_6.efg \ + catalog/games/books/watson2013/fig29_1.efg \ + catalog/games/conf/itcs/jakobsen2016/fig1a.efg \ + catalog/games/conf/itcs/jakobsen2016/fig1b.efg \ + catalog/games/conf/itcs/jakobsen2016/fig1c.efg \ + catalog/games/conf/itcs/jakobsen2016/fig3.efg \ + catalog/games/journals/dcg/vonstengel1999/6x6_game_with_75_eq.nfg \ + catalog/games/journals/dcg/vonstengel1999/6x6_game_with_75_eq_small_payoffs.nfg \ + catalog/games/journals/geb/bagwell1995.efg \ + catalog/games/journals/geb/gilboa1997/fig1.efg \ + catalog/games/journals/geb/gilboa1997/fig2.efg \ + catalog/games/journals/geb/wichardt2008.efg \ + catalog/games/journals/ijgt/nau2004/sec3.nfg \ + catalog/games/journals/ijgt/nau2004/sec4.nfg \ + catalog/games/journals/ijgt/nau2004/sec5.nfg \ + catalog/games/journals/ijgt/nau2004/sec6.nfg \ + catalog/games/journals/ijgt/selten1975/fig1.efg \ + catalog/games/journals/ijgt/selten1975/fig2.efg \ + catalog/games/journals/ijgt/selten1975/fig3.efg \ + catalog/games/journals/mor/vonstengelforges2008/fig1.efg \ + catalog/games/journals/mor/vonstengelforges2008/fig1__Original_Layout.ef \ + catalog/games/journals/mor/vonstengelforges2008/fig6.efg \ + catalog/games/journals/mor/vonstengelforges2008/fig6__Original_Layout.ef \ + catalog/games/journals/mor/vonstengelforges2008/fig9.efg \ + catalog/games/journals/mor/vonstengelforges2008/fig9__Original_Layout.ef \ + catalog/games/journals/other/reiley2008/fig1.efg \ + catalog/games/journals/other/shapley1974/fig2.nfg \ + catalog/games/journals/other/shapley1974/fig3.nfg diff --git a/catalog/doc/conf.py b/catalog/doc/conf.py new file mode 100644 index 0000000000..f3a9b317df --- /dev/null +++ b/catalog/doc/conf.py @@ -0,0 +1,45 @@ +import pathlib +import sys + +# The shared bibtex citation style lives in the main docs project; import it from +# there rather than duplicating it (see doc/_bibstyle.py). +sys.path.insert(0, str(pathlib.Path(__file__).parent.parent.parent / "doc")) +import _bibstyle # noqa: E402,F401 registers the shared "keystyle" pybtex plugin + +# -- General configuration ----------------------------------------------------- + +extensions = [ + "sphinx_design", + "jupyter_sphinx", + "sphinxcontrib.bibtex", +] + +bibtex_bibfiles = [str(pathlib.Path(__file__).parent.parent.parent / "doc" / "references.bib")] + +source_suffix = {".rst": "restructuredtext"} +master_doc = "index" + +project = "Gambit Catalog" +copyright = "1994-2026, The Gambit Project" # noqa + +_version_file = pathlib.Path(__file__).parent.parent.parent / "build_support" / "GAMBIT_VERSION" +if _version_file.exists(): + _full_version = _version_file.read_text().strip() + version = ".".join(_full_version.split(".")[:2]) + release = _full_version +else: + raise FileNotFoundError("GAMBIT_VERSION file not found") + +exclude_patterns = ["_build", "_table.rst"] + +pygments_style = "sphinx" + +# -- Options for HTML output --------------------------------------------------- + +html_theme = "pydata_sphinx_theme" +html_theme_options = { + "external_links": [ + {"name": "Gambit documentation", "url": "https://gambitproject.readthedocs.io/"}, + {"name": "GitHub", "url": "https://github.com/gambitproject/gambit"}, + ], +} diff --git a/doc/developer.catalog.rst b/catalog/doc/developer.rst similarity index 59% rename from doc/developer.catalog.rst rename to catalog/doc/developer.rst index b69c2a519f..dc33bc6b96 100644 --- a/doc/developer.catalog.rst +++ b/catalog/doc/developer.rst @@ -1,13 +1,13 @@ .. _updating-catalog: -Updating the Games Catalog -========================== +Updating the catalog of games +============================= -This page includes developer notes regarding the catalog module, and the process for contributing to and updating Gambit's :ref:`Games Catalog `. +This page includes developer notes regarding the catalog module, and the process for contributing to and updating Gambit's :ref:`catalog of games `. 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 may wish to first review the `contributor guidelines `_. -You can add new games to the catalog, or edit existing ones that are saved in a valid representation :ref:`format `. +You can add new games to the catalog, or edit existing ones that are saved in a valid representation `format `_. Currently supported representations are: - `.efg` for extensive form games @@ -17,7 +17,7 @@ Currently supported representations are: When updating the catalog, changes can be viewed by inspecting the documentation build generated by a pull request. You can also test changes locally. It can be helpful when doing so to perform an editable install of `pygambit` available in your Python environment. - Note that for general edits to the codebase non-editable :ref:`developer install ` is better to pick up changes in the C++ code as well as Python. + Note that for general edits to the codebase non-editable `developer install `_ is better to pick up changes in the C++ code as well as Python. .. code-block:: bash @@ -26,23 +26,23 @@ Currently supported representations are: 1. **Create or edit a game file:** - Use either :ref:`pygambit `, the Gambit :ref:`CLI ` or :ref:`GUI ` to create (or edit) and save game in a valid representation :ref:`format `. - Make sure the game includes a description, with any citations referencing the :ref:`bibliography ` using the ``:cite:p:`key``` format, e.g. ``:cite:p:`Rei2008```. + Use either `pygambit `_, the Gambit `CLI `_ or `GUI `_ to create (or edit) and save game in a valid representation `format `_. + Make sure the game includes a description, with any citations referencing the `bibliography `_ using the ``:cite:p:`key``` format, e.g. ``:cite:p:`Rei2008```. .. important:: If no bibliography entry exists, you should add one. For instructions, - see :ref:`updating-bibliography`. + see `Updating the bibliography `_. 2. **Add the game(s) to the repo:** Create a new branch in the ``gambit`` repo. - Add your new game file(s) inside the ``catalog`` dir and commit them, or edit an existing game. + Add your new game file(s) inside the ``catalog/games`` dir and commit them, or edit an existing game. The catalog uses a hierarchical folder structure that groups games by publication type and venue: .. code-block:: text - catalog/ + catalog/games/ books/{author-year}/{game}.efg # games from textbooks journals/{venue}/{author-year}/{game}.efg # games from journals (venue = geb, ijgt, mor, …) conf/{venue}/{author-year}/{game}.efg # games from conferences @@ -58,7 +58,7 @@ Currently supported representations are: When adding a game from a **new** journal, conference, or other top-level category, add a human-readable label for the new hierarchy node(s) to - ``build_support/catalog/catalog_hierarchy.yaml``. + ``catalog/hierarchy.yaml``. The catalog documentation page groups and labels games based on this file. Nodes without an entry fall back to a title-cased version of the folder name. @@ -73,8 +73,8 @@ Currently supported representations are: 3. **[Optional] Add custom game layout(s):** For extensive form games, you may optionally commit a curated ``.ef`` file alongside the ``.efg`` - (e.g. ``catalog/source/game.ef``). - When present, ``update.py`` will use this file directly as input to gtdraw instead of + (e.g. ``catalog/games/source/game.ef``). + When present, ``build.py`` will use this file directly as input to gtdraw instead of auto-generating the layout from the ``.efg``, preserving any hand-tuned layout. Consult the `gtdraw docs `_ for the ``.ef`` format. @@ -92,8 +92,8 @@ Currently supported representations are: .. code-block:: text - catalog/example/game.ef # primary (tab label "Default") - catalog/example/game__original_layout.ef # additional (tab label "Original Layout") + catalog/games/example/game.ef # primary (tab label "Default") + catalog/games/example/game__original_layout.ef # additional (tab label "Original Layout") The label shown on each tab is derived automatically from the filename suffix: underscores are replaced by spaces and the result is title-cased (e.g. ``__alt_version`` → "Alt Version"). @@ -101,15 +101,15 @@ Currently supported representations are: 4. **Update the build files:** Ensure you have installed the package in editable mode to automatically pick up the new game file(s) in the ``pygambit.catalog`` module without reinstalling each time. - Then use the ``update.py`` script to update Gambit's documentation & build files, as well as generating images for the new game(s). - If you want to customise the visualisation parameters for your game(s), edit ``build_support/catalog/gtdraw_settings.yaml``. + Then use the ``build.py`` script to update Gambit's documentation & build files, as well as generating images for the new game(s). + If you want to customise the visualisation parameters for your game(s), edit ``catalog/gtdraw_settings.yaml``. Add an entry under ``overrides`` keyed by your game's exact slug, or by a shared prefix (e.g. the author-year folder name) to apply settings to all games from that source. More specific entries (longer keys) take precedence over shorter ones. Consult the `gtdraw docs `_ for available settings. .. code-block:: bash - python build_support/catalog/update.py --build + python catalog/build.py --build .. note:: @@ -117,34 +117,43 @@ Currently supported representations are: .. warning:: - - If haven't done an editable install of ``pygambit`` in your python environment, you'll need to re-install it before running the update script to include new games in the catalog module. - - Running the script with the ``--build`` flag updates ``build_support/catalog/catalog.am``, which is included in ``Makefile.am``. If you moved games that were previously in ``contrib/games`` you'll need to also manually remove those files from ``EXTRA_DIST`` in ``Makefile.am``. + - If you haven't done an editable install of ``pygambit`` in your python environment, you'll need to re-install it before running the build script to include new games in the catalog module. + - Running the script with the ``--build`` flag updates ``catalog/catalog.am``, which is included in ``Makefile.am``. If you moved games that were previously in ``contrib/games`` you'll need to also manually remove those files from ``EXTRA_DIST`` in ``Makefile.am``. .. tip:: - If you encounter any issues with game files in your python environment not matching the current branch preventing the update script or docs build working (step below) try running the following first: + If you encounter any issues with game files in your python environment not matching the current branch preventing the build script or docs build working (step below) try running the following first: .. code-block:: bash - git clean -fdX catalog/img doc/catalog_table.rst + git clean -fdX catalog/games/img catalog/doc/_table.rst .. tip:: - If the game visuals for extensive form games need some work and you aren't sure which settings to change in ``build_support/catalog/gtdraw_settings.yaml``, try loading the EFG in the gtdraw GUI and adjusting the layout there. + If the game visuals for extensive form games need some work and you aren't sure which settings to change in ``catalog/gtdraw_settings.yaml``, try loading the EFG in the gtdraw GUI and adjusting the layout there. There is an option to `download settings `_ which can be used in the Gambit catalog. 5. **[Optional] Test your updates to the documentation locally:** The previous step will (re)build your local copy of the Gambit Catalog RST page used by the documentation. - You should then build the docs in the :ref:`usual way `. - Open the catalog page at ``doc/_build/html/catalog.html`` to view your changes. + The catalog is its own Sphinx project, built independently of the main Gambit docs. + Run the build from inside ``catalog/doc`` itself, not the repo root: the generated table + embeds ``draw(...)`` calls whose paths are resolved relative to the current working + directory, so building from anywhere else raises a file-not-found error. + + .. code-block:: bash + + cd catalog/doc + sphinx-build -b html . _build/html + + Open ``catalog/doc/_build/html/index.html`` to view your changes. Iterate steps 2-4 as required. 6. **Submit a pull request to GitHub with all changes.** - Submit a PR according to the :ref:`usual workflow `. - Ensure that any additions and changes to game files, ``build_support/catalog/gtdraw_settings.yaml``, ``build_support/catalog/update.py`` and ``build_support/catalog/catalog.am`` are included. + Submit a PR according to the `usual workflow `_. + Ensure that any additions and changes to game files, ``catalog/gtdraw_settings.yaml``, ``catalog/build.py`` and ``catalog/catalog.am`` are included. .. important:: - Even if you already checked the local docs build, ensure the Catalog page on the ReadTheDocs preview build on the pull request looks right. + Even if you already checked the local docs build, ensure the catalog's own ReadTheDocs preview build on the pull request looks right. diff --git a/catalog/doc/index.rst b/catalog/doc/index.rst new file mode 100644 index 0000000000..c895f9bc13 --- /dev/null +++ b/catalog/doc/index.rst @@ -0,0 +1,26 @@ +.. _catalog: + +Catalog of games +================ + +Below is a complete list of games included in Gambit's catalog. +See the main `pygambit documentation `_ for the +the `pygambit.catalog API reference `_ +(including how to load these games in Python), +for loading games from the OpenSpiel library, and for generating games on the fly +from the GAMUT suite. +See :ref:`Updating the games catalog ` below for instructions on how +to contribute new games to the catalog. + +.. include:: _table.rst + +References +---------- + +.. bibliography:: ../../doc/references.bib + :style: keystyle + +.. toctree:: + :hidden: + + developer diff --git a/catalog/books/myerson1991/fig2_1.efg b/catalog/games/books/myerson1991/fig2_1.efg similarity index 100% rename from catalog/books/myerson1991/fig2_1.efg rename to catalog/games/books/myerson1991/fig2_1.efg diff --git a/catalog/books/myerson1991/fig4_2.efg b/catalog/games/books/myerson1991/fig4_2.efg similarity index 100% rename from catalog/books/myerson1991/fig4_2.efg rename to catalog/games/books/myerson1991/fig4_2.efg diff --git a/catalog/books/shohamleytonbrown2008/fig5_1.efg b/catalog/games/books/shohamleytonbrown2008/fig5_1.efg similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig5_1.efg rename to catalog/games/books/shohamleytonbrown2008/fig5_1.efg diff --git a/catalog/books/shohamleytonbrown2008/fig5_10.efg b/catalog/games/books/shohamleytonbrown2008/fig5_10.efg similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig5_10.efg rename to catalog/games/books/shohamleytonbrown2008/fig5_10.efg diff --git a/catalog/books/shohamleytonbrown2008/fig5_10__original_layout.ef b/catalog/games/books/shohamleytonbrown2008/fig5_10__original_layout.ef similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig5_10__original_layout.ef rename to catalog/games/books/shohamleytonbrown2008/fig5_10__original_layout.ef diff --git a/catalog/books/shohamleytonbrown2008/fig5_11.efg b/catalog/games/books/shohamleytonbrown2008/fig5_11.efg similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig5_11.efg rename to catalog/games/books/shohamleytonbrown2008/fig5_11.efg diff --git a/catalog/books/shohamleytonbrown2008/fig5_11__original_layout.ef b/catalog/games/books/shohamleytonbrown2008/fig5_11__original_layout.ef similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig5_11__original_layout.ef rename to catalog/games/books/shohamleytonbrown2008/fig5_11__original_layout.ef diff --git a/catalog/books/shohamleytonbrown2008/fig5_12.efg b/catalog/games/books/shohamleytonbrown2008/fig5_12.efg similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig5_12.efg rename to catalog/games/books/shohamleytonbrown2008/fig5_12.efg diff --git a/catalog/books/shohamleytonbrown2008/fig5_12__original_layout.ef b/catalog/games/books/shohamleytonbrown2008/fig5_12__original_layout.ef similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig5_12__original_layout.ef rename to catalog/games/books/shohamleytonbrown2008/fig5_12__original_layout.ef diff --git a/catalog/books/shohamleytonbrown2008/fig5_15.efg b/catalog/games/books/shohamleytonbrown2008/fig5_15.efg similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig5_15.efg rename to catalog/games/books/shohamleytonbrown2008/fig5_15.efg diff --git a/catalog/books/shohamleytonbrown2008/fig5_15__original_layout.ef b/catalog/games/books/shohamleytonbrown2008/fig5_15__original_layout.ef similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig5_15__original_layout.ef rename to catalog/games/books/shohamleytonbrown2008/fig5_15__original_layout.ef diff --git a/catalog/books/shohamleytonbrown2008/fig5_1__original_layout.ef b/catalog/games/books/shohamleytonbrown2008/fig5_1__original_layout.ef similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig5_1__original_layout.ef rename to catalog/games/books/shohamleytonbrown2008/fig5_1__original_layout.ef diff --git a/catalog/books/shohamleytonbrown2008/fig5_2.efg b/catalog/games/books/shohamleytonbrown2008/fig5_2.efg similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig5_2.efg rename to catalog/games/books/shohamleytonbrown2008/fig5_2.efg diff --git a/catalog/books/shohamleytonbrown2008/fig5_2__original_layout.ef b/catalog/games/books/shohamleytonbrown2008/fig5_2__original_layout.ef similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig5_2__original_layout.ef rename to catalog/games/books/shohamleytonbrown2008/fig5_2__original_layout.ef diff --git a/catalog/books/shohamleytonbrown2008/fig5_9.efg b/catalog/games/books/shohamleytonbrown2008/fig5_9.efg similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig5_9.efg rename to catalog/games/books/shohamleytonbrown2008/fig5_9.efg diff --git a/catalog/books/shohamleytonbrown2008/fig5_9__original_layout.ef b/catalog/games/books/shohamleytonbrown2008/fig5_9__original_layout.ef similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig5_9__original_layout.ef rename to catalog/games/books/shohamleytonbrown2008/fig5_9__original_layout.ef diff --git a/catalog/books/shohamleytonbrown2008/fig6_2.efg b/catalog/games/books/shohamleytonbrown2008/fig6_2.efg similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig6_2.efg rename to catalog/games/books/shohamleytonbrown2008/fig6_2.efg diff --git a/catalog/books/shohamleytonbrown2008/fig6_2__original_layout.ef b/catalog/games/books/shohamleytonbrown2008/fig6_2__original_layout.ef similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig6_2__original_layout.ef rename to catalog/games/books/shohamleytonbrown2008/fig6_2__original_layout.ef diff --git a/catalog/books/shohamleytonbrown2008/fig6_8.efg b/catalog/games/books/shohamleytonbrown2008/fig6_8.efg similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig6_8.efg rename to catalog/games/books/shohamleytonbrown2008/fig6_8.efg diff --git a/catalog/books/shohamleytonbrown2008/fig6_8__original_layout.ef b/catalog/games/books/shohamleytonbrown2008/fig6_8__original_layout.ef similarity index 100% rename from catalog/books/shohamleytonbrown2008/fig6_8__original_layout.ef rename to catalog/games/books/shohamleytonbrown2008/fig6_8__original_layout.ef diff --git a/catalog/books/vonstengel2022/fig10.1.efg b/catalog/games/books/vonstengel2022/fig10.1.efg similarity index 100% rename from catalog/books/vonstengel2022/fig10.1.efg rename to catalog/games/books/vonstengel2022/fig10.1.efg diff --git a/catalog/books/vonstengel2022/fig10.12.efg b/catalog/games/books/vonstengel2022/fig10.12.efg similarity index 100% rename from catalog/books/vonstengel2022/fig10.12.efg rename to catalog/games/books/vonstengel2022/fig10.12.efg diff --git a/catalog/books/vonstengel2022/fig10.5.efg b/catalog/games/books/vonstengel2022/fig10.5.efg similarity index 100% rename from catalog/books/vonstengel2022/fig10.5.efg rename to catalog/games/books/vonstengel2022/fig10.5.efg diff --git a/catalog/books/vonstengel2022/fig10.7.efg b/catalog/games/books/vonstengel2022/fig10.7.efg similarity index 100% rename from catalog/books/vonstengel2022/fig10.7.efg rename to catalog/games/books/vonstengel2022/fig10.7.efg diff --git a/catalog/books/watson2013/exercise29_6.efg b/catalog/games/books/watson2013/exercise29_6.efg similarity index 100% rename from catalog/books/watson2013/exercise29_6.efg rename to catalog/games/books/watson2013/exercise29_6.efg diff --git a/catalog/books/watson2013/fig29_1.efg b/catalog/games/books/watson2013/fig29_1.efg similarity index 100% rename from catalog/books/watson2013/fig29_1.efg rename to catalog/games/books/watson2013/fig29_1.efg diff --git a/catalog/conf/itcs/jakobsen2016/fig1a.efg b/catalog/games/conf/itcs/jakobsen2016/fig1a.efg similarity index 100% rename from catalog/conf/itcs/jakobsen2016/fig1a.efg rename to catalog/games/conf/itcs/jakobsen2016/fig1a.efg diff --git a/catalog/conf/itcs/jakobsen2016/fig1b.efg b/catalog/games/conf/itcs/jakobsen2016/fig1b.efg similarity index 100% rename from catalog/conf/itcs/jakobsen2016/fig1b.efg rename to catalog/games/conf/itcs/jakobsen2016/fig1b.efg diff --git a/catalog/conf/itcs/jakobsen2016/fig1c.efg b/catalog/games/conf/itcs/jakobsen2016/fig1c.efg similarity index 100% rename from catalog/conf/itcs/jakobsen2016/fig1c.efg rename to catalog/games/conf/itcs/jakobsen2016/fig1c.efg diff --git a/catalog/conf/itcs/jakobsen2016/fig3.efg b/catalog/games/conf/itcs/jakobsen2016/fig3.efg similarity index 100% rename from catalog/conf/itcs/jakobsen2016/fig3.efg rename to catalog/games/conf/itcs/jakobsen2016/fig3.efg diff --git a/catalog/img/.gitkeep b/catalog/games/img/.gitkeep similarity index 100% rename from catalog/img/.gitkeep rename to catalog/games/img/.gitkeep diff --git a/catalog/journals/dcg/vonstengel1999/6x6_game_with_75_eq.nfg b/catalog/games/journals/dcg/vonstengel1999/6x6_game_with_75_eq.nfg similarity index 99% rename from catalog/journals/dcg/vonstengel1999/6x6_game_with_75_eq.nfg rename to catalog/games/journals/dcg/vonstengel1999/6x6_game_with_75_eq.nfg index 79d55443f0..47ab59d1c0 100644 --- a/catalog/journals/dcg/vonstengel1999/6x6_game_with_75_eq.nfg +++ b/catalog/games/journals/dcg/vonstengel1999/6x6_game_with_75_eq.nfg @@ -3,7 +3,7 @@ NFG 1 R "von Stengel's 6x6 bimatrix game with 75 equilibria" { "1" "2" } { { "1" "2" "3" "4" "5" "6" } { "1" "2" "3" "4" "5" "6" } } -"The example from :cite:p:`vS97` as referenced in :cite:p:`vS99`. +"The example from :cite:p:`vS97` as referenced in :cite:p:`vS99`. It is a 6x6 bimatrix game with 75 equilibria, which is more than the previously conjectured bound of :math:`2^6 - 1 = 63`." diff --git a/catalog/journals/dcg/vonstengel1999/6x6_game_with_75_eq_small_payoffs.nfg b/catalog/games/journals/dcg/vonstengel1999/6x6_game_with_75_eq_small_payoffs.nfg similarity index 100% rename from catalog/journals/dcg/vonstengel1999/6x6_game_with_75_eq_small_payoffs.nfg rename to catalog/games/journals/dcg/vonstengel1999/6x6_game_with_75_eq_small_payoffs.nfg diff --git a/catalog/journals/geb/bagwell1995.efg b/catalog/games/journals/geb/bagwell1995.efg similarity index 100% rename from catalog/journals/geb/bagwell1995.efg rename to catalog/games/journals/geb/bagwell1995.efg diff --git a/catalog/journals/geb/gilboa1997/fig1.efg b/catalog/games/journals/geb/gilboa1997/fig1.efg similarity index 100% rename from catalog/journals/geb/gilboa1997/fig1.efg rename to catalog/games/journals/geb/gilboa1997/fig1.efg diff --git a/catalog/journals/geb/gilboa1997/fig2.efg b/catalog/games/journals/geb/gilboa1997/fig2.efg similarity index 100% rename from catalog/journals/geb/gilboa1997/fig2.efg rename to catalog/games/journals/geb/gilboa1997/fig2.efg diff --git a/catalog/journals/geb/wichardt2008.efg b/catalog/games/journals/geb/wichardt2008.efg similarity index 100% rename from catalog/journals/geb/wichardt2008.efg rename to catalog/games/journals/geb/wichardt2008.efg diff --git a/catalog/journals/ijgt/nau2004/sec3.nfg b/catalog/games/journals/ijgt/nau2004/sec3.nfg similarity index 100% rename from catalog/journals/ijgt/nau2004/sec3.nfg rename to catalog/games/journals/ijgt/nau2004/sec3.nfg diff --git a/catalog/journals/ijgt/nau2004/sec4.nfg b/catalog/games/journals/ijgt/nau2004/sec4.nfg similarity index 100% rename from catalog/journals/ijgt/nau2004/sec4.nfg rename to catalog/games/journals/ijgt/nau2004/sec4.nfg diff --git a/catalog/journals/ijgt/nau2004/sec5.nfg b/catalog/games/journals/ijgt/nau2004/sec5.nfg similarity index 100% rename from catalog/journals/ijgt/nau2004/sec5.nfg rename to catalog/games/journals/ijgt/nau2004/sec5.nfg diff --git a/catalog/journals/ijgt/nau2004/sec6.nfg b/catalog/games/journals/ijgt/nau2004/sec6.nfg similarity index 100% rename from catalog/journals/ijgt/nau2004/sec6.nfg rename to catalog/games/journals/ijgt/nau2004/sec6.nfg diff --git a/catalog/journals/ijgt/selten1975/fig1.efg b/catalog/games/journals/ijgt/selten1975/fig1.efg similarity index 100% rename from catalog/journals/ijgt/selten1975/fig1.efg rename to catalog/games/journals/ijgt/selten1975/fig1.efg diff --git a/catalog/journals/ijgt/selten1975/fig2.efg b/catalog/games/journals/ijgt/selten1975/fig2.efg similarity index 100% rename from catalog/journals/ijgt/selten1975/fig2.efg rename to catalog/games/journals/ijgt/selten1975/fig2.efg diff --git a/catalog/journals/ijgt/selten1975/fig3.efg b/catalog/games/journals/ijgt/selten1975/fig3.efg similarity index 100% rename from catalog/journals/ijgt/selten1975/fig3.efg rename to catalog/games/journals/ijgt/selten1975/fig3.efg diff --git a/catalog/journals/mor/vonstengelforges2008/fig1.efg b/catalog/games/journals/mor/vonstengelforges2008/fig1.efg similarity index 100% rename from catalog/journals/mor/vonstengelforges2008/fig1.efg rename to catalog/games/journals/mor/vonstengelforges2008/fig1.efg diff --git a/catalog/journals/mor/vonstengelforges2008/fig1__Original_Layout.ef b/catalog/games/journals/mor/vonstengelforges2008/fig1__Original_Layout.ef similarity index 100% rename from catalog/journals/mor/vonstengelforges2008/fig1__Original_Layout.ef rename to catalog/games/journals/mor/vonstengelforges2008/fig1__Original_Layout.ef diff --git a/catalog/journals/mor/vonstengelforges2008/fig6.efg b/catalog/games/journals/mor/vonstengelforges2008/fig6.efg similarity index 100% rename from catalog/journals/mor/vonstengelforges2008/fig6.efg rename to catalog/games/journals/mor/vonstengelforges2008/fig6.efg diff --git a/catalog/journals/mor/vonstengelforges2008/fig6__Original_Layout.ef b/catalog/games/journals/mor/vonstengelforges2008/fig6__Original_Layout.ef similarity index 100% rename from catalog/journals/mor/vonstengelforges2008/fig6__Original_Layout.ef rename to catalog/games/journals/mor/vonstengelforges2008/fig6__Original_Layout.ef diff --git a/catalog/journals/mor/vonstengelforges2008/fig9.efg b/catalog/games/journals/mor/vonstengelforges2008/fig9.efg similarity index 100% rename from catalog/journals/mor/vonstengelforges2008/fig9.efg rename to catalog/games/journals/mor/vonstengelforges2008/fig9.efg diff --git a/catalog/journals/mor/vonstengelforges2008/fig9__Original_Layout.ef b/catalog/games/journals/mor/vonstengelforges2008/fig9__Original_Layout.ef similarity index 100% rename from catalog/journals/mor/vonstengelforges2008/fig9__Original_Layout.ef rename to catalog/games/journals/mor/vonstengelforges2008/fig9__Original_Layout.ef diff --git a/catalog/journals/other/reiley2008/fig1.efg b/catalog/games/journals/other/reiley2008/fig1.efg similarity index 100% rename from catalog/journals/other/reiley2008/fig1.efg rename to catalog/games/journals/other/reiley2008/fig1.efg diff --git a/catalog/journals/other/shapley1974/fig2.nfg b/catalog/games/journals/other/shapley1974/fig2.nfg similarity index 100% rename from catalog/journals/other/shapley1974/fig2.nfg rename to catalog/games/journals/other/shapley1974/fig2.nfg diff --git a/catalog/journals/other/shapley1974/fig3.nfg b/catalog/games/journals/other/shapley1974/fig3.nfg similarity index 100% rename from catalog/journals/other/shapley1974/fig3.nfg rename to catalog/games/journals/other/shapley1974/fig3.nfg diff --git a/build_support/catalog/gtdraw_settings.yaml b/catalog/gtdraw_settings.yaml similarity index 100% rename from build_support/catalog/gtdraw_settings.yaml rename to catalog/gtdraw_settings.yaml diff --git a/build_support/catalog/catalog_hierarchy.yaml b/catalog/hierarchy.yaml similarity index 100% rename from build_support/catalog/catalog_hierarchy.yaml rename to catalog/hierarchy.yaml diff --git a/build_support/catalog/test_update.py b/catalog/test_build.py similarity index 80% rename from build_support/catalog/test_update.py rename to catalog/test_build.py index 58885b4ac9..8bb0a4a175 100644 --- a/build_support/catalog/test_update.py +++ b/catalog/test_build.py @@ -1,4 +1,4 @@ -"""Tests for build_support/catalog/update.py. +"""Tests for catalog/build.py. All catalog slugs used here are clearly fictional (e.g. ``"testgroup2000/fig1"``) and do not correspond to any game in the real catalog. This is intentional: the @@ -7,17 +7,17 @@ Monkeypatching strategy ----------------------- -``update.py`` depends on four external resources that are replaced in tests: +``build.py`` depends on four external resources that are replaced in tests: 1. ``GTDRAW_SETTINGS_CONFIG`` (a ``Path``) — swapped for a tmp YAML file so ``catalog_gtdraw_settings`` reads controlled config without touching the - real ``gtdraw_settings.yaml``. ``monkeypatch.setattr(update, + real ``gtdraw_settings.yaml``. ``monkeypatch.setattr(build, "GTDRAW_SETTINGS_CONFIG", yaml_file)`` replaces the module-level path for the duration of a single test and restores it automatically on teardown. 2. ``CATALOG_HIERARCHY_CONFIG`` (a ``Path``) — swapped for a tmp YAML file so ``load_hierarchy_labels`` reads controlled labels without touching the real - ``catalog_hierarchy.yaml``. Swap via ``monkeypatch.setattr(update, + ``hierarchy.yaml``. Swap via ``monkeypatch.setattr(build, "CATALOG_HIERARCHY_CONFIG", yaml_file)``. 3. ``tex`` / ``png`` / ``pdf`` / ``svg`` @@ -37,11 +37,12 @@ import pytest -pytest.importorskip("gtdraw") # update.py imports gtdraw at module level +pytest.importorskip("gtdraw") # build.py imports gtdraw at module level pytest.importorskip("yaml") import pandas as pd # noqa: E402 -import update # noqa: E402 + +import build # noqa: E402 # --------------------------------------------------------------------------- # Module-level test fixtures @@ -89,7 +90,7 @@ def _write_yaml(path, content=_YAML_CONFIG): """Write *content* to *path* and return *path*. Used to create a temporary gtdraw_settings YAML file that can be - pointed at via ``monkeypatch.setattr(update, "GTDRAW_SETTINGS_CONFIG", + pointed at via ``monkeypatch.setattr(build, "GTDRAW_SETTINGS_CONFIG", path)`` without touching the real config file. """ path.write_text(content, encoding="utf-8") @@ -104,7 +105,7 @@ def _efg_row(slug, title="Test EFG Game", description="A description."): "Game": slug, "Title": title, "Description": description, - "Download": f":download:`{slug}.efg <../catalog/{slug}.efg>`", + "Download": f":download:`{slug}.efg <../games/{slug}.efg>`", "Format": "efg", } @@ -115,7 +116,7 @@ def _nfg_row(slug, title="Test NFG Game", description="A description."): "Game": slug, "Title": title, "Description": description, - "Download": f":download:`{slug}.nfg <../catalog/{slug}.nfg>`", + "Download": f":download:`{slug}.nfg <../games/{slug}.nfg>`", "Format": "nfg", } @@ -164,16 +165,16 @@ def test_catalog_games_uses_requested_catalog_dir_and_restores_resource( calls = [] def fake_games(**kwargs): - calls.append((update.gbt.catalog._CATALOG_RESOURCE, kwargs)) + calls.append((build.gbt.catalog._CATALOG_RESOURCE, kwargs)) return _make_df(_efg_row("checkout/game1")) - monkeypatch.setattr(update.gbt.catalog, "_CATALOG_RESOURCE", stale_dir) - monkeypatch.setattr(update.gbt.catalog, "games", fake_games) + monkeypatch.setattr(build.gbt.catalog, "_CATALOG_RESOURCE", stale_dir) + monkeypatch.setattr(build.gbt.catalog, "games", fake_games) - df = update._catalog_games(checkout_dir) + df = build._catalog_games(checkout_dir) assert calls == [(checkout_dir, {"include_descriptions": True})] - assert stale_dir == update.gbt.catalog._CATALOG_RESOURCE + assert stale_dir == build.gbt.catalog._CATALOG_RESOURCE assert list(df["Game"]) == ["checkout/game1"] def test_generate_rst_table_uses_requested_catalog_dir_while_rendering( @@ -186,16 +187,16 @@ def test_generate_rst_table_uses_requested_catalog_dir_while_rendering( observed = [] def fake_write_tree_level(*args, **kwargs): - observed.append(update.gbt.catalog._CATALOG_RESOURCE) + observed.append(build.gbt.catalog._CATALOG_RESOURCE) - monkeypatch.setattr(update.gbt.catalog, "_CATALOG_RESOURCE", stale_dir) - monkeypatch.setattr(update, "load_hierarchy_labels", lambda: {}) - monkeypatch.setattr(update, "_write_tree_level", fake_write_tree_level) + monkeypatch.setattr(build.gbt.catalog, "_CATALOG_RESOURCE", stale_dir) + monkeypatch.setattr(build, "load_hierarchy_labels", lambda: {}) + monkeypatch.setattr(build, "_write_tree_level", fake_write_tree_level) - update.generate_rst_table(_make_df(), tmp_path / "out.rst", catalog_dir=checkout_dir) + build.generate_rst_table(_make_df(), tmp_path / "out.rst", catalog_dir=checkout_dir) assert observed == [checkout_dir] - assert stale_dir == update.gbt.catalog._CATALOG_RESOURCE + assert stale_dir == build.gbt.catalog._CATALOG_RESOURCE # --------------------------------------------------------------------------- @@ -215,24 +216,24 @@ class TestCatalogGtDrawSettings: def test_no_override_returns_defaults(self, tmp_path, monkeypatch): """A slug with no matching entry in ``overrides`` returns the defaults verbatim.""" yaml_file = _write_yaml(tmp_path / "settings.yaml") - monkeypatch.setattr(update, "GTDRAW_SETTINGS_CONFIG", yaml_file) - result = update.catalog_gtdraw_settings("unknowngame/v1") + monkeypatch.setattr(build, "GTDRAW_SETTINGS_CONFIG", yaml_file) + result = build.catalog_gtdraw_settings("unknowngame/v1") assert result == _YAML_DEFAULTS def test_exact_slug_override_applied(self, tmp_path, monkeypatch): """A key in ``overrides`` that exactly matches the slug is merged into defaults.""" yaml_file = _write_yaml(tmp_path / "settings.yaml") - monkeypatch.setattr(update, "GTDRAW_SETTINGS_CONFIG", yaml_file) - result = update.catalog_gtdraw_settings("testgroup2000/fig2") + monkeypatch.setattr(build, "GTDRAW_SETTINGS_CONFIG", yaml_file) + result = build.catalog_gtdraw_settings("testgroup2000/fig2") assert result["action_label_position"] == pytest.approx(0.4) assert result["color_scheme"] == "gambit" # defaults still present def test_prefix_slug_override_applied(self, tmp_path, monkeypatch): """A group-level key (e.g. ``"testgroup2000"``) matches any slug that starts with it.""" yaml_file = _write_yaml(tmp_path / "settings.yaml") - monkeypatch.setattr(update, "GTDRAW_SETTINGS_CONFIG", yaml_file) + monkeypatch.setattr(build, "GTDRAW_SETTINGS_CONFIG", yaml_file) # "testgroup2000/fig1" is not listed explicitly; it matches the group prefix - result = update.catalog_gtdraw_settings("testgroup2000/fig1") + result = build.catalog_gtdraw_settings("testgroup2000/fig1") assert result["sublevel_scaling"] == 1 def test_specific_key_wins_over_group(self, tmp_path, monkeypatch): @@ -254,19 +255,19 @@ def test_specific_key_wins_over_group(self, tmp_path, monkeypatch): sublevel_scaling: 2 """) yaml_file = _write_yaml(tmp_path / "settings.yaml", config) - monkeypatch.setattr(update, "GTDRAW_SETTINGS_CONFIG", yaml_file) - result = update.catalog_gtdraw_settings("testgroup2000/fig2") + monkeypatch.setattr(build, "GTDRAW_SETTINGS_CONFIG", yaml_file) + result = build.catalog_gtdraw_settings("testgroup2000/fig2") assert result["sublevel_scaling"] == 2 def test_group_override_does_not_bleed_to_other_game(self, tmp_path, monkeypatch): """A group-level override applies only to games whose slug starts with that prefix.""" yaml_file = _write_yaml(tmp_path / "settings.yaml") - monkeypatch.setattr(update, "GTDRAW_SETTINGS_CONFIG", yaml_file) + monkeypatch.setattr(build, "GTDRAW_SETTINGS_CONFIG", yaml_file) # "othergroup1999" override sets shared_terminal_depth = False - result_other = update.catalog_gtdraw_settings("othergroup1999/fig1") + result_other = build.catalog_gtdraw_settings("othergroup1999/fig1") assert result_other["shared_terminal_depth"] is False # "testgroup2000" has a different override; shared_terminal_depth should be True (default) - result_test = update.catalog_gtdraw_settings("testgroup2000/fig1") + result_test = build.catalog_gtdraw_settings("testgroup2000/fig1") assert result_test["shared_terminal_depth"] is True def test_no_overrides_section_returns_defaults(self, tmp_path, monkeypatch): @@ -277,8 +278,8 @@ def test_no_overrides_section_returns_defaults(self, tmp_path, monkeypatch): sublevel_scaling: 0 """) yaml_file = _write_yaml(tmp_path / "settings.yaml", config) - monkeypatch.setattr(update, "GTDRAW_SETTINGS_CONFIG", yaml_file) - result = update.catalog_gtdraw_settings("anygame/v1") + monkeypatch.setattr(build, "GTDRAW_SETTINGS_CONFIG", yaml_file) + result = build.catalog_gtdraw_settings("anygame/v1") assert result == {"color_scheme": "gambit", "sublevel_scaling": 0} @@ -312,7 +313,7 @@ def test_no_ef_files_returns_none(self, tmp_path): catalog_dir = tmp_path / "catalog" slug = "fakevariant2000/fig1" self._game_dir(catalog_dir, slug) - assert update.catalog_ef_file_variants(slug, catalog_dir) is None + assert build.catalog_ef_file_variants(slug, catalog_dir) is None def test_single_ef_file_returns_none(self, tmp_path): """A single curated .ef file → returns None (single image, no tabs needed).""" @@ -320,7 +321,7 @@ def test_single_ef_file_returns_none(self, tmp_path): slug = "fakevariant2000/fig1" game_dir = self._game_dir(catalog_dir, slug) (game_dir / "fig1.ef").touch() - assert update.catalog_ef_file_variants(slug, catalog_dir) is None + assert build.catalog_ef_file_variants(slug, catalog_dir) is None def test_two_ef_files_returns_variant_list(self, tmp_path): """Two .ef files → 2-item list with correct label, ef_path, and variant_key.""" @@ -329,7 +330,7 @@ def test_two_ef_files_returns_variant_list(self, tmp_path): game_dir = self._game_dir(catalog_dir, slug) (game_dir / "fig1.ef").touch() (game_dir / "fig1__wide.ef").touch() - result = update.catalog_ef_file_variants(slug, catalog_dir) + result = build.catalog_ef_file_variants(slug, catalog_dir) assert result is not None assert len(result) == 2 assert {v["label"] for v in result} == {"Default", "Wide"} @@ -343,7 +344,7 @@ def test_variant_without_primary_ef_returns_variants(self, tmp_path): slug = "fakevariant2000/fig1" game_dir = self._game_dir(catalog_dir, slug) (game_dir / "fig1__wide.ef").touch() - result = update.catalog_ef_file_variants(slug, catalog_dir) + result = build.catalog_ef_file_variants(slug, catalog_dir) assert result is not None assert len(result) == 2 assert {v["label"] for v in result} == {"Default", "Wide"} @@ -357,7 +358,7 @@ def test_label_derived_from_filename_suffix(self, tmp_path): game_dir = self._game_dir(catalog_dir, slug) (game_dir / "fig1.ef").touch() (game_dir / "fig1__compact.ef").touch() - result = update.catalog_ef_file_variants(slug, catalog_dir) + result = build.catalog_ef_file_variants(slug, catalog_dir) assert {v["label"] for v in result} == {"Default", "Compact"} def test_multi_word_suffix_title_cased(self, tmp_path): @@ -367,7 +368,7 @@ def test_multi_word_suffix_title_cased(self, tmp_path): game_dir = self._game_dir(catalog_dir, slug) (game_dir / "fig1.ef").touch() (game_dir / "fig1__very_wide.ef").touch() - result = update.catalog_ef_file_variants(slug, catalog_dir) + result = build.catalog_ef_file_variants(slug, catalog_dir) assert "Very Wide" in {v["label"] for v in result} def test_file_without_double_underscore_excluded(self, tmp_path): @@ -382,7 +383,7 @@ def test_file_without_double_underscore_excluded(self, tmp_path): game_dir = self._game_dir(catalog_dir, slug) (game_dir / "fig1.ef").touch() (game_dir / "fig1extra.ef").touch() # no __ separator — must be ignored - assert update.catalog_ef_file_variants(slug, catalog_dir) is None + assert build.catalog_ef_file_variants(slug, catalog_dir) is None # --------------------------------------------------------------------------- @@ -399,7 +400,7 @@ class TestGenerateRstTable: from the real catalog directory. ``_mock_generates`` uses ``monkeypatch.setattr`` to replace each of the - four gtdraw functions in the ``update`` module's namespace with + four gtdraw functions in the ``build`` module's namespace with a no-op. Because the replacement is scoped to the test, the originals are automatically restored afterward. @@ -416,7 +417,7 @@ def _no_op_generate(self, *args, **kwargs): def _mock_generates(self, monkeypatch): """Replace all four gtdraw image-generation functions with no-ops.""" for name in ["tex", "png", "pdf", "svg"]: - monkeypatch.setattr(update, name, self._no_op_generate) + monkeypatch.setattr(build, name, self._no_op_generate) def test_efg_row_produces_rst_with_slug_and_title(self, tmp_path, monkeypatch): """An EFG game row appears in the RST with its title, load call, and download links.""" @@ -426,7 +427,7 @@ def test_efg_row_produces_rst_with_slug_and_title(self, tmp_path, monkeypatch): _make_image_files(catalog_dir, slug, "efg") df = _make_df(_efg_row(slug, title="Fake Author (2000) Figure 1")) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) rst = rst_path.read_text() assert "Fake Author (2000) Figure 1" in rst assert f'pygambit.catalog.load("{slug}")' in rst @@ -441,9 +442,9 @@ def test_nfg_row_produces_rst_with_save_to(self, tmp_path, monkeypatch): _make_image_files(catalog_dir, slug, "nfg") df = _make_df(_nfg_row(slug)) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) rst = rst_path.read_text() - assert f'save_to="../catalog/img/{slug}.png"' in rst + assert f'save_to="../games/img/{slug}.png"' in rst def test_unknown_format_row_is_skipped(self, tmp_path, monkeypatch): """A row whose Format is not 'efg' or 'nfg' is silently omitted from the RST.""" @@ -459,7 +460,7 @@ def test_unknown_format_row_is_skipped(self, tmp_path, monkeypatch): } df = _make_df(row) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) rst = rst_path.read_text() assert "Fake Game" not in rst @@ -470,7 +471,7 @@ def test_row_without_description_is_skipped(self, tmp_path, monkeypatch): catalog_dir.mkdir() df = _make_df(_efg_row("fakeauthor2000/fig1", description="")) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) rst = rst_path.read_text() assert "fakeauthor2000/fig1" not in rst @@ -481,17 +482,17 @@ def test_curated_ef_used_in_draw_call(self, tmp_path, monkeypatch): catalog_dir = tmp_path / "catalog" slug = "fakeauthor1999/fig1" _make_image_files(catalog_dir, slug, "efg") - # Place a curated .ef file alongside the game — this is what update.py checks for + # Place a curated .ef file alongside the game — this is what build.py checks for curated = catalog_dir / f"{slug}.ef" curated.parent.mkdir(parents=True, exist_ok=True) curated.touch() df = _make_df(_efg_row(slug)) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) rst = rst_path.read_text() # Find the draw( call line in the jupyter-execute block draw_call = next(line for line in rst.splitlines() if "draw(" in line) - assert f'"../catalog/{slug}.ef"' in draw_call + assert f'"../games/{slug}.ef"' in draw_call assert "catalog.load" not in draw_call def test_images_not_regenerated_when_all_exist(self, tmp_path, monkeypatch): @@ -499,41 +500,41 @@ def test_images_not_regenerated_when_all_exist(self, tmp_path, monkeypatch): False, none of the gtdraw image-generation functions are called.""" calls = [] # Replace generate_* with lambdas that record invocations - monkeypatch.setattr(update, "tex", lambda *a, **k: calls.append("tex")) - monkeypatch.setattr(update, "png", lambda *a, **k: calls.append("png")) - monkeypatch.setattr(update, "pdf", lambda *a, **k: calls.append("pdf")) - monkeypatch.setattr(update, "svg", lambda *a, **k: calls.append("svg")) + monkeypatch.setattr(build, "tex", lambda *a, **k: calls.append("tex")) + monkeypatch.setattr(build, "png", lambda *a, **k: calls.append("png")) + monkeypatch.setattr(build, "pdf", lambda *a, **k: calls.append("pdf")) + monkeypatch.setattr(build, "svg", lambda *a, **k: calls.append("svg")) catalog_dir = tmp_path / "catalog" slug = "fakeauthor2000/fig1" _make_image_files(catalog_dir, slug, "efg") # all images already exist df = _make_df(_efg_row(slug)) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, regenerate_images=False, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, regenerate_images=False, catalog_dir=catalog_dir) assert calls == [] def test_images_regenerated_when_flag_set(self, tmp_path, monkeypatch): """When ``regenerate_images=True``, all four generate functions are called even if the image files already exist. - A curated .ef file is placed in the catalog dir so ``update.py`` uses it + A curated .ef file is placed in the catalog dir so ``build.py`` uses it as the gtdraw source rather than calling ``gbt.catalog.load``, which would require the real catalog to be present. """ calls = [] - monkeypatch.setattr(update, "tex", lambda *a, **k: calls.append("tex")) - monkeypatch.setattr(update, "png", lambda *a, **k: calls.append("png")) - monkeypatch.setattr(update, "pdf", lambda *a, **k: calls.append("pdf")) - monkeypatch.setattr(update, "svg", lambda *a, **k: calls.append("svg")) + monkeypatch.setattr(build, "tex", lambda *a, **k: calls.append("tex")) + monkeypatch.setattr(build, "png", lambda *a, **k: calls.append("png")) + monkeypatch.setattr(build, "pdf", lambda *a, **k: calls.append("pdf")) + monkeypatch.setattr(build, "svg", lambda *a, **k: calls.append("svg")) catalog_dir = tmp_path / "catalog" slug = "fakeauthor2000/fig1" _make_image_files(catalog_dir, slug, "efg") - # Place a curated .ef file alongside the game — this is what update.py checks for + # Place a curated .ef file alongside the game — this is what build.py checks for curated = catalog_dir / f"{slug}.ef" curated.parent.mkdir(parents=True, exist_ok=True) curated.touch() df = _make_df(_efg_row(slug)) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, regenerate_images=True, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, regenerate_images=True, catalog_dir=catalog_dir) assert set(calls) == {"tex", "png", "pdf", "svg"} def test_multi_variant_efg_produces_tab_set(self, tmp_path, monkeypatch): @@ -551,7 +552,7 @@ def test_multi_variant_efg_produces_tab_set(self, tmp_path, monkeypatch): (game_dir / "fig1__wide.ef").touch() df = _make_df(_efg_row(slug)) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, regenerate_images=True, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, regenerate_images=True, catalog_dir=catalog_dir) rst = rst_path.read_text() assert ".. tab-set::" in rst assert ".. tab-item:: Default" in rst @@ -562,7 +563,7 @@ def test_multi_variant_efg_without_primary_ef_produces_tab_set(self, tmp_path, m containing both a Default and the custom variant, calling catalog.load for the Default. """ self._mock_generates(monkeypatch) - monkeypatch.setattr(update.gbt.catalog, "load", lambda slug: "dummy_game") + monkeypatch.setattr(build.gbt.catalog, "load", lambda slug: "dummy_game") catalog_dir = tmp_path / "catalog" slug = "fakevariant2001/fig1" game_dir = catalog_dir / "fakevariant2001" @@ -570,13 +571,13 @@ def test_multi_variant_efg_without_primary_ef_produces_tab_set(self, tmp_path, m (game_dir / "fig1__wide.ef").touch() df = _make_df(_efg_row(slug)) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, regenerate_images=True, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, regenerate_images=True, catalog_dir=catalog_dir) rst = rst_path.read_text() assert ".. tab-set::" in rst assert ".. tab-item:: Default" in rst assert ".. tab-item:: Wide" in rst assert 'draw(pygambit.catalog.load("fakevariant2001/fig1")' in rst - assert 'draw("../catalog/fakevariant2001/fig1__wide.ef"' in rst + assert 'draw("../games/fakevariant2001/fig1__wide.ef"' in rst def test_single_variant_efg_produces_no_tab_set(self, tmp_path, monkeypatch): """A single curated .ef file (or no .ef file) does not produce a ``tab-set``.""" @@ -589,7 +590,7 @@ def test_single_variant_efg_produces_no_tab_set(self, tmp_path, monkeypatch): (game_dir / "fig1.ef").touch() # only one .ef — no tabs df = _make_df(_efg_row(slug)) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) rst = rst_path.read_text() assert ".. tab-set::" not in rst @@ -599,10 +600,10 @@ def test_per_variant_images_generated(self, tmp_path, monkeypatch): Two variants × four generate functions = eight total calls. """ calls = [] - monkeypatch.setattr(update, "tex", lambda *a, **k: calls.append("tex")) - monkeypatch.setattr(update, "png", lambda *a, **k: calls.append("png")) - monkeypatch.setattr(update, "pdf", lambda *a, **k: calls.append("pdf")) - monkeypatch.setattr(update, "svg", lambda *a, **k: calls.append("svg")) + monkeypatch.setattr(build, "tex", lambda *a, **k: calls.append("tex")) + monkeypatch.setattr(build, "png", lambda *a, **k: calls.append("png")) + monkeypatch.setattr(build, "pdf", lambda *a, **k: calls.append("pdf")) + monkeypatch.setattr(build, "svg", lambda *a, **k: calls.append("svg")) catalog_dir = tmp_path / "catalog" slug = "fakevariant2001/fig1" game_dir = catalog_dir / "fakevariant2001" @@ -611,17 +612,17 @@ def test_per_variant_images_generated(self, tmp_path, monkeypatch): (game_dir / "fig1__wide.ef").touch() df = _make_df(_efg_row(slug)) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, regenerate_images=True, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, regenerate_images=True, catalog_dir=catalog_dir) assert len(calls) == 8 # 4 functions × 2 variants def test_per_variant_images_not_regenerated_when_all_exist(self, tmp_path, monkeypatch): """If all variant image files already exist and ``regenerate_images`` is False, generate functions are not called.""" calls = [] - monkeypatch.setattr(update, "tex", lambda *a, **k: calls.append("tex")) - monkeypatch.setattr(update, "png", lambda *a, **k: calls.append("png")) - monkeypatch.setattr(update, "pdf", lambda *a, **k: calls.append("pdf")) - monkeypatch.setattr(update, "svg", lambda *a, **k: calls.append("svg")) + monkeypatch.setattr(build, "tex", lambda *a, **k: calls.append("tex")) + monkeypatch.setattr(build, "png", lambda *a, **k: calls.append("png")) + monkeypatch.setattr(build, "pdf", lambda *a, **k: calls.append("pdf")) + monkeypatch.setattr(build, "svg", lambda *a, **k: calls.append("svg")) catalog_dir = tmp_path / "catalog" slug = "fakevariant2001/fig1" game_dir = catalog_dir / "fakevariant2001" @@ -633,7 +634,7 @@ def test_per_variant_images_not_regenerated_when_all_exist(self, tmp_path, monke _make_image_files(catalog_dir, vkey, "efg") df = _make_df(_efg_row(slug)) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, regenerate_images=False, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, regenerate_images=False, catalog_dir=catalog_dir) assert calls == [] @@ -641,7 +642,7 @@ def test_per_variant_images_not_regenerated_when_all_exist(self, tmp_path, monke # Tests for hierarchy helpers and hierarchical RST output # --------------------------------------------------------------------------- -# A minimal catalog_hierarchy.yaml used by hierarchy tests. +# A minimal hierarchy.yaml used by hierarchy tests. _HIERARCHY_YAML = textwrap.dedent("""\ labels: cat: "My Category" @@ -657,8 +658,8 @@ def test_load_hierarchy_labels_returns_dict(self, tmp_path, monkeypatch): """``load_hierarchy_labels`` returns the labels dict from the YAML.""" yaml_file = tmp_path / "hier.yaml" yaml_file.write_text(_HIERARCHY_YAML, encoding="utf-8") - monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", yaml_file) - labels = update.load_hierarchy_labels() + monkeypatch.setattr(build, "CATALOG_HIERARCHY_CONFIG", yaml_file) + labels = build.load_hierarchy_labels() assert labels["cat"] == "My Category" assert labels["cat/src"] == "My Source" @@ -666,22 +667,22 @@ def test_node_label_uses_yaml(self, tmp_path, monkeypatch): """``_node_label`` returns the YAML label when the prefix is present.""" yaml_file = tmp_path / "hier.yaml" yaml_file.write_text(_HIERARCHY_YAML, encoding="utf-8") - monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", yaml_file) - labels = update.load_hierarchy_labels() - assert update._node_label("cat", labels) == "My Category" + monkeypatch.setattr(build, "CATALOG_HIERARCHY_CONFIG", yaml_file) + labels = build.load_hierarchy_labels() + assert build._node_label("cat", labels) == "My Category" def test_node_label_fallback_title_case(self, tmp_path, monkeypatch): """``_node_label`` falls back to title-casing the last component.""" yaml_file = tmp_path / "hier.yaml" yaml_file.write_text(_HIERARCHY_YAML, encoding="utf-8") - monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", yaml_file) - labels = update.load_hierarchy_labels() - assert update._node_label("cat/unknownsrc", labels) == "Unknownsrc" + monkeypatch.setattr(build, "CATALOG_HIERARCHY_CONFIG", yaml_file) + labels = build.load_hierarchy_labels() + assert build._node_label("cat/unknownsrc", labels) == "Unknownsrc" def test_build_slug_tree_single_game(self): """A single-slug DataFrame builds a 2-level tree.""" df = _make_df(_efg_row("cat/src/game1")) - tree = update._build_slug_tree(df) + tree = build._build_slug_tree(df) assert "cat" in tree assert "src" in tree["cat"] assert "game1" in tree["cat"]["src"] @@ -689,19 +690,19 @@ def test_build_slug_tree_single_game(self): def test_build_slug_tree_groups_siblings(self): """Two slugs sharing a prefix are grouped under the same intermediate node.""" df = _make_df(_efg_row("cat/src/game1"), _efg_row("cat/src/game2")) - tree = update._build_slug_tree(df) + tree = build._build_slug_tree(df) assert set(tree["cat"]["src"].keys()) == {"game1", "game2"} def test_build_slug_tree_skips_unknown_format(self): """Rows with unrecognised Format are excluded from the tree.""" row = {**_efg_row("cat/src/game1"), "Format": "xyz"} df = _make_df(row) - assert update._build_slug_tree(df) == {} + assert build._build_slug_tree(df) == {} def test_build_slug_tree_skips_empty_description(self): """Rows with an empty description are excluded from the tree.""" df = _make_df(_efg_row("cat/src/game1", description="")) - assert update._build_slug_tree(df) == {} + assert build._build_slug_tree(df) == {} @pytest.mark.catalog_update @@ -710,7 +711,7 @@ class TestHierarchicalRstOutput: def _mock_generates(self, monkeypatch): for name in ["tex", "png", "pdf", "svg"]: - monkeypatch.setattr(update, name, lambda *a, **k: None) + monkeypatch.setattr(build, name, lambda *a, **k: None) def _write_hierarchy_yaml(self, tmp_path, content=_HIERARCHY_YAML): yaml_file = tmp_path / "hier.yaml" @@ -721,13 +722,13 @@ def test_top_level_dropdown_is_open(self, tmp_path, monkeypatch): """Top-level category dropdowns carry ``:open:`` so the first level is visible.""" self._mock_generates(monkeypatch) hier_yaml = self._write_hierarchy_yaml(tmp_path) - monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", hier_yaml) + monkeypatch.setattr(build, "CATALOG_HIERARCHY_CONFIG", hier_yaml) catalog_dir = tmp_path / "catalog" slug = "cat/src/game1" _make_image_files(catalog_dir, slug, "efg") df = _make_df(_efg_row(slug)) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) rst = rst_path.read_text() assert ".. dropdown:: My Category\n :open:" in rst @@ -735,13 +736,13 @@ def test_second_level_dropdown_is_not_open(self, tmp_path, monkeypatch): """Sub-category dropdowns do NOT carry ``:open:`` so they are collapsed by default.""" self._mock_generates(monkeypatch) hier_yaml = self._write_hierarchy_yaml(tmp_path) - monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", hier_yaml) + monkeypatch.setattr(build, "CATALOG_HIERARCHY_CONFIG", hier_yaml) catalog_dir = tmp_path / "catalog" slug = "cat/src/game1" _make_image_files(catalog_dir, slug, "efg") df = _make_df(_efg_row(slug)) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) rst = rst_path.read_text() assert " .. dropdown:: My Source\n \n" in rst # Confirm :open: does not immediately follow the second-level dropdown @@ -752,13 +753,13 @@ def test_game_dropdown_is_open(self, tmp_path, monkeypatch): """Individual game dropdowns carry ``:open:`` so game content is visible on expand.""" self._mock_generates(monkeypatch) hier_yaml = self._write_hierarchy_yaml(tmp_path) - monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", hier_yaml) + monkeypatch.setattr(build, "CATALOG_HIERARCHY_CONFIG", hier_yaml) catalog_dir = tmp_path / "catalog" slug = "cat/src/game1" _make_image_files(catalog_dir, slug, "efg") df = _make_df(_efg_row(slug, title="My Game Title")) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) rst = rst_path.read_text() assert " .. dropdown:: My Game Title\n :open:" in rst @@ -766,7 +767,7 @@ def test_sibling_games_both_appear_under_source(self, tmp_path, monkeypatch): """Two games sharing a source prefix both appear nested under the source dropdown.""" self._mock_generates(monkeypatch) hier_yaml = self._write_hierarchy_yaml(tmp_path) - monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", hier_yaml) + monkeypatch.setattr(build, "CATALOG_HIERARCHY_CONFIG", hier_yaml) catalog_dir = tmp_path / "catalog" for slug in ["cat/src/game1", "cat/src/game2"]: _make_image_files(catalog_dir, slug, "efg") @@ -775,7 +776,7 @@ def test_sibling_games_both_appear_under_source(self, tmp_path, monkeypatch): _efg_row("cat/src/game2", title="Game Two"), ) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) rst = rst_path.read_text() assert ".. dropdown:: My Category" in rst assert " .. dropdown:: My Source" in rst @@ -786,13 +787,13 @@ def test_no_list_table_in_output(self, tmp_path, monkeypatch): """The new output does not use ``.. list-table::`` (replaced by nested dropdowns).""" self._mock_generates(monkeypatch) hier_yaml = self._write_hierarchy_yaml(tmp_path) - monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", hier_yaml) + monkeypatch.setattr(build, "CATALOG_HIERARCHY_CONFIG", hier_yaml) catalog_dir = tmp_path / "catalog" slug = "cat/src/game1" _make_image_files(catalog_dir, slug, "efg") df = _make_df(_efg_row(slug)) rst_path = tmp_path / "out.rst" - update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + build.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) rst = rst_path.read_text() assert ".. list-table::" not in rst assert ".. contents::" not in rst @@ -817,10 +818,10 @@ def test_efg_and_nfg_files_included(self, tmp_path): (tmp_path / "subfolder").mkdir() (tmp_path / "subfolder" / "matrix.nfg").touch() am = tmp_path / "catalog.am" - update.update_makefile(catalog_dir=tmp_path, am_path=am) + build.update_makefile(catalog_dir=tmp_path, am_path=am) content = am.read_text() - assert "catalog/standalone.efg" in content - assert "catalog/subfolder/matrix.nfg" in content + assert "catalog/games/standalone.efg" in content + assert "catalog/games/subfolder/matrix.nfg" in content def test_curated_ef_included(self, tmp_path): """A curated .ef file committed alongside a game file appears in catalog.am.""" @@ -828,9 +829,9 @@ def test_curated_ef_included(self, tmp_path): (tmp_path / "fakegame" / "fig1.efg").touch() (tmp_path / "fakegame" / "fig1.ef").touch() # curated layout file am = tmp_path / "catalog.am" - update.update_makefile(catalog_dir=tmp_path, am_path=am) + build.update_makefile(catalog_dir=tmp_path, am_path=am) content = am.read_text() - assert "catalog/fakegame/fig1.ef" in content + assert "catalog/games/fakegame/fig1.ef" in content def test_ef_in_img_dir_excluded(self, tmp_path): """Generated .ef files under the img/ subdirectory are excluded from catalog.am.""" @@ -838,7 +839,7 @@ def test_ef_in_img_dir_excluded(self, tmp_path): img.mkdir(parents=True) (img / "fig1.ef").touch() # generated artifact — should not be distributed am = tmp_path / "catalog.am" - update.update_makefile(catalog_dir=tmp_path, am_path=am) + build.update_makefile(catalog_dir=tmp_path, am_path=am) content = am.read_text() assert "img" not in content @@ -847,7 +848,7 @@ def test_non_game_file_excluded(self, tmp_path): (tmp_path / "fakegame.efg_2").touch() # hidden/renamed file (tmp_path / "README.txt").touch() am = tmp_path / "catalog.am" - update.update_makefile(catalog_dir=tmp_path, am_path=am) + build.update_makefile(catalog_dir=tmp_path, am_path=am) content = am.read_text() assert "efg_2" not in content assert "README" not in content @@ -861,15 +862,15 @@ def test_no_write_when_content_unchanged(self, tmp_path): """ (tmp_path / "standalone.efg").touch() am = tmp_path / "catalog.am" - update.update_makefile(catalog_dir=tmp_path, am_path=am) + build.update_makefile(catalog_dir=tmp_path, am_path=am) mtime_after_first_write = am.stat().st_mtime - update.update_makefile(catalog_dir=tmp_path, am_path=am) + build.update_makefile(catalog_dir=tmp_path, am_path=am) assert am.stat().st_mtime == mtime_after_first_write def test_empty_catalog_produces_valid_am(self, tmp_path): """An empty catalog directory produces a catalog.am with a valid (empty) CATALOG_FILES.""" am = tmp_path / "catalog.am" - update.update_makefile(catalog_dir=tmp_path, am_path=am) + build.update_makefile(catalog_dir=tmp_path, am_path=am) content = am.read_text() assert content.startswith("CATALOG_FILES =") @@ -885,7 +886,7 @@ class TestWarnMissingDescriptions: def test_game_without_description_warns(self, capsys): """A game with an empty description produces a WARNING on stderr.""" df = _make_df(_efg_row("journals/nobody2025/fig1", description="")) - update._warn_missing_descriptions(df) + build._warn_missing_descriptions(df) err = capsys.readouterr().err assert "WARNING" in err assert "journals/nobody2025/fig1" in err @@ -893,6 +894,6 @@ def test_game_without_description_warns(self, capsys): def test_game_with_description_does_not_warn(self, capsys): """A game with a non-empty description produces no output.""" df = _make_df(_efg_row("journals/nobody2025/fig1")) - update._warn_missing_descriptions(df) + build._warn_missing_descriptions(df) err = capsys.readouterr().err assert err == "" diff --git a/doc/_bibstyle.py b/doc/_bibstyle.py new file mode 100644 index 0000000000..f9b4673a48 --- /dev/null +++ b/doc/_bibstyle.py @@ -0,0 +1,173 @@ +"""Shared pybtex citation style, registered as the "keystyle" plugin. + +Imported by both this project's ``conf.py`` and ``catalog/doc/conf.py`` so the two +independent Sphinx projects render ``:cite:p:`` citations identically without +duplicating the style definition. +""" +import re + +import pybtex.plugin +from pybtex.richtext import Symbol, Text +from pybtex.style.formatting import toplevel +from pybtex.style.formatting.alpha import Style as AlphaStyle +from pybtex.style.labels import BaseLabelStyle +from pybtex.style.template import ( + field, + first_of, + join, + optional, + optional_field, + sentence, + tag, + words, +) + + +def dashify(text): + dash_re = re.compile(r"-+") + return Text(Symbol("ndash")).join(text.split(dash_re)) + + +pages = field("pages", apply_func=dashify) + + +class KeyLabelStyle(BaseLabelStyle): + """Custom label style that uses the BibTeX entry key as the citation label.""" + + def format_labels(self, sorted_entries): + for entry in sorted_entries: + yield entry.key + + +class CustomAlphaStyle(AlphaStyle): + """Custom formatting style that uses KeyLabelStyle for labels.""" + + default_label_style = "keystyle" + default_name_style = "lastfirst" + + def format_author_or_editor(self, e, as_sentence=False): + return first_of[ + optional[self.format_names("author", as_sentence=as_sentence)], + optional[self.format_editor(e, as_sentence=as_sentence)], + ] + + def format_editor(self, e, as_sentence=True): + editors = self.format_names("editor", as_sentence=False) + if "editor" not in e.persons: + return editors + word = "(eds)" if len(e.persons["editor"]) > 1 else "(ed.)" + result = join(sep=" ")[editors, word] + if as_sentence: + return sentence[result] + return result + + def get_article_template(self, e): + return toplevel[ + sentence[ + join(sep=", ")[ + join(sep=" ")[ + self.format_names("author", as_sentence=False), + field("year"), + ], + join["‘", field("title"), "’"], + tag("em")[field("journal")], + optional[words["vol.", field("volume")]], + optional[words["no.", field("number")]], + optional[words["pp.", pages]], + ] + ], + sentence[optional_field("note")], + self.format_web_refs(e), + ] + + def get_book_template(self, e): + return toplevel[ + sentence[ + join(sep=", ")[ + join(sep=" ")[ + self.format_author_or_editor(e, as_sentence=False), + field("year"), + ], + tag("em")[field("title")], + optional[words[field("edition"), "edn"]], + field("publisher"), + optional_field("address"), + ] + ], + sentence[optional_field("note")], + self.format_web_refs(e), + ] + + def get_incollection_template(self, e): + return toplevel[ + sentence[ + join(sep=", ")[ + join(sep=" ")[ + self.format_names("author", as_sentence=False), + field("year"), + ], + join["‘", field("title"), "’"], + words[ + "in", + join(sep=", ")[ + optional[self.format_editor(e, as_sentence=False)], + tag("em")[field("booktitle")], + optional[words["vol.", field("volume")]], + ], + ], + field("publisher"), + optional_field("address"), + optional[words["pp.", pages]], + ] + ], + sentence[optional_field("note")], + self.format_web_refs(e), + ] + + def get_inbook_template(self, e): + return self.get_incollection_template(e) + + def get_inproceedings_template(self, e): + return toplevel[ + sentence[ + join(sep=", ")[ + join(sep=" ")[ + self.format_names("author", as_sentence=False), + field("year"), + ], + join["‘", field("title"), "’"], + tag("em")[field("booktitle")], + optional[words["pp.", pages]], + ] + ], + sentence[optional_field("note")], + self.format_web_refs(e), + ] + + def get_techreport_template(self, e): + type_and_number = optional[ + words[ + first_of[optional_field("type"), "Technical Report"], + field("number"), + ] + ] + return toplevel[ + sentence[ + join(sep=", ")[ + join(sep=" ")[ + self.format_names("author", as_sentence=False), + field("year"), + ], + join["‘", field("title"), "’"], + type_and_number, + field("institution"), + optional_field("address"), + ] + ], + sentence[optional_field("note")], + self.format_web_refs(e), + ] + + +pybtex.plugin.register_plugin("pybtex.style.labels", "keystyle", KeyLabelStyle) +pybtex.plugin.register_plugin("pybtex.style.formatting", "keystyle", CustomAlphaStyle) diff --git a/doc/catalog.rst b/doc/catalog_generators.rst similarity index 80% rename from doc/catalog.rst rename to doc/catalog_generators.rst index 118a991f59..3e3ad7e468 100644 --- a/doc/catalog.rst +++ b/doc/catalog_generators.rst @@ -1,14 +1,12 @@ -.. _catalog: +.. _catalog-generators: -Catalog of games -================ +Generating games for the catalog +================================= -Below is a complete list of games included in Gambit's catalog. -Check out the :ref:`pygambit API reference ` for instructions on how to search and load these games in Python, and the :ref:`Updating the games catalog ` guide for instructions on how to contribute new games to the catalog. -Games from the OpenSpiel library are also available; see :ref:`Loading OpenSpiel games `. -Games can also be generated on the fly from the GAMUT suite; see :ref:`Generating GAMUT games `. - -.. include:: catalog_table.rst +Alongside the curated catalog of games (built and hosted separately; see the +`catalog's developer guide `_ +for how to contribute new games), pygambit can generate games on the fly from two +external sources: the OpenSpiel library and the GAMUT suite. .. _catalog-openspiel: diff --git a/doc/conf.py b/doc/conf.py index 45175ea3fa..13aee504ea 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,21 +1,8 @@ import pathlib -import re - -import pybtex.plugin -from pybtex.richtext import Symbol, Text -from pybtex.style.formatting import toplevel -from pybtex.style.formatting.alpha import Style as AlphaStyle -from pybtex.style.labels import BaseLabelStyle -from pybtex.style.template import ( - field, - first_of, - join, - optional, - optional_field, - sentence, - tag, - words, -) +import sys + +sys.path.insert(0, str(pathlib.Path(__file__).parent)) +import _bibstyle # noqa: E402,F401 registers the shared "keystyle" pybtex plugin # # Gambit documentation build configuration file, created by @@ -54,157 +41,8 @@ # BibTeX configuration -def dashify(text): - dash_re = re.compile(r"-+") - return Text(Symbol("ndash")).join(text.split(dash_re)) - - -pages = field("pages", apply_func=dashify) - bibtex_bibfiles = ["references.bib"] - -class KeyLabelStyle(BaseLabelStyle): - """Custom label style that uses the BibTeX entry key as the citation label.""" - - def format_labels(self, sorted_entries): - for entry in sorted_entries: - yield entry.key - - -class CustomAlphaStyle(AlphaStyle): - """Custom formatting style that uses KeyLabelStyle for labels.""" - - default_label_style = "keystyle" - default_name_style = "lastfirst" - - def format_author_or_editor(self, e, as_sentence=False): - return first_of[ - optional[self.format_names("author", as_sentence=as_sentence)], - optional[self.format_editor(e, as_sentence=as_sentence)], - ] - - def format_editor(self, e, as_sentence=True): - editors = self.format_names("editor", as_sentence=False) - if "editor" not in e.persons: - return editors - word = "(eds)" if len(e.persons["editor"]) > 1 else "(ed.)" - result = join(sep=" ")[editors, word] - if as_sentence: - return sentence[result] - return result - - def get_article_template(self, e): - return toplevel[ - sentence[ - join(sep=", ")[ - join(sep=" ")[ - self.format_names("author", as_sentence=False), - field("year"), - ], - join["\u2018", field("title"), "\u2019"], - tag("em")[field("journal")], - optional[words["vol.", field("volume")]], - optional[words["no.", field("number")]], - optional[words["pp.", pages]], - ] - ], - sentence[optional_field("note")], - self.format_web_refs(e), - ] - - def get_book_template(self, e): - return toplevel[ - sentence[ - join(sep=", ")[ - join(sep=" ")[ - self.format_author_or_editor(e, as_sentence=False), - field("year"), - ], - tag("em")[field("title")], - optional[words[field("edition"), "edn"]], - field("publisher"), - optional_field("address"), - ] - ], - sentence[optional_field("note")], - self.format_web_refs(e), - ] - - def get_incollection_template(self, e): - return toplevel[ - sentence[ - join(sep=", ")[ - join(sep=" ")[ - self.format_names("author", as_sentence=False), - field("year"), - ], - join["\u2018", field("title"), "\u2019"], - words[ - "in", - join(sep=", ")[ - optional[self.format_editor(e, as_sentence=False)], - tag("em")[field("booktitle")], - optional[words["vol.", field("volume")]], - ], - ], - field("publisher"), - optional_field("address"), - optional[words["pp.", pages]], - ] - ], - sentence[optional_field("note")], - self.format_web_refs(e), - ] - - def get_inbook_template(self, e): - return self.get_incollection_template(e) - - def get_inproceedings_template(self, e): - return toplevel[ - sentence[ - join(sep=", ")[ - join(sep=" ")[ - self.format_names("author", as_sentence=False), - field("year"), - ], - join["\u2018", field("title"), "\u2019"], - tag("em")[field("booktitle")], - optional[words["pp.", pages]], - ] - ], - sentence[optional_field("note")], - self.format_web_refs(e), - ] - - def get_techreport_template(self, e): - type_and_number = optional[ - words[ - first_of[optional_field("type"), "Technical Report"], - field("number"), - ] - ] - return toplevel[ - sentence[ - join(sep=", ")[ - join(sep=" ")[ - self.format_names("author", as_sentence=False), - field("year"), - ], - join["\u2018", field("title"), "\u2019"], - type_and_number, - field("institution"), - optional_field("address"), - ] - ], - sentence[optional_field("note")], - self.format_web_refs(e), - ] - - -pybtex.plugin.register_plugin("pybtex.style.labels", "keystyle", KeyLabelStyle) -pybtex.plugin.register_plugin("pybtex.style.formatting", "keystyle", CustomAlphaStyle) - # IPython directive configuration ipython_execlines = ["import pygambit as gbt", "import os", "import sys"] ipython_savefig_dir = "savefig" @@ -262,7 +100,7 @@ def get_techreport_template(self, e): # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = ["_build"] -exclude_patterns = ["_build", "catalog_table.rst"] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None diff --git a/doc/developer.overview.rst b/doc/developer.overview.rst index 607400b759..a4e6aab303 100644 --- a/doc/developer.overview.rst +++ b/doc/developer.overview.rst @@ -23,7 +23,7 @@ Running ``make check`` has no effect (there are no tests defined in that build s The test suite makes use of a variety of games, which are drawn from examples in the game theory literature and textbooks, as well as examples constructed specifically to test edge cases. From Gambit 17, these are all curated as part of Gambit's -:ref:`catalog of games `. +`catalog of games `_. From Gambit 17, ``pygambit`` is also responsible for providing command-line interfaces to equilibrium computation and other facilities; previously the command-line wrappers were diff --git a/doc/developer.rst b/doc/developer.rst index ee1bb5fd44..c319b384b3 100644 --- a/doc/developer.rst +++ b/doc/developer.rst @@ -5,6 +5,7 @@ Developer docs ============== This section contains information for developers who want to contribute to the Gambit project, including how to build Gambit from source, how to contribute code, and how to report bugs. +For instructions on contributing new games to the catalog, see the `catalog's own developer guide `_ (built and versioned separately from these docs). .. toctree:: :maxdepth: 2 @@ -12,4 +13,3 @@ This section contains information for developers who want to contribute to the G developer.overview developer.build developer.contributing - developer.catalog diff --git a/doc/index.rst b/doc/index.rst index ad354146a0..9329f1b8a2 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -47,14 +47,15 @@ construction and analysis of finite extensive and strategic games. .. grid-item-card:: 📚 Catalog of games :columns: 3 - Browse a curated collection of game theory models. + Browse a curated collection of games. - .. button-ref:: catalog - :ref-type: ref + .. button-link:: https://gambit-catalog.readthedocs.io/ :click-parent: :color: secondary :expand: + Open catalog + .. grid-item-card:: 🖱️ Graphical interface :columns: 3 @@ -109,7 +110,7 @@ construction and analysis of finite extensive and strategic games. pygambit tools gui - catalog + catalog_generators developer formats biblio diff --git a/doc/tools.enummixed.rst b/doc/tools.enummixed.rst index e367bc54c8..76d87cdfeb 100644 --- a/doc/tools.enummixed.rst +++ b/doc/tools.enummixed.rst @@ -41,7 +41,7 @@ See the :ref:`algorithm description ` for full details. Computing the equilibria, in mixed strategies, of the reduced strategic form of the example in Figure 2 of :cite:p:`Sel75`:: - $ gambit-enummixed catalog/journals/ijgt/selten1975/fig2.efg + $ gambit-enummixed catalog/games/journals/ijgt/selten1975/fig2.efg Compute Nash equilibria by enumerating extreme points Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL @@ -53,7 +53,7 @@ In fact, this game has a one-dimensional continuum of equilibria. This fact can be observed by examining the connectedness information using the ``-c`` switch:: - $ gambit-enummixed -c catalog/journals/ijgt/selten1975/fig2.efg + $ gambit-enummixed -c catalog/games/journals/ijgt/selten1975/fig2.efg Compute Nash equilibria by enumerating extreme points Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL diff --git a/doc/tools.enumpoly.rst b/doc/tools.enumpoly.rst index 91996b2a91..d7d27fbf6a 100644 --- a/doc/tools.enumpoly.rst +++ b/doc/tools.enumpoly.rst @@ -90,7 +90,7 @@ and (in verbose mode) the support is identified with the label Computing equilibria of the example in Figure 1 of :cite:p:`Sel75`, sometimes called "Selten's horse":: - $ gambit-enumpoly -S catalog/journals/ijgt/selten1975/fig1.efg + $ gambit-enumpoly -S catalog/games/journals/ijgt/selten1975/fig1.efg Compute Nash equilibria by solving polynomial systems Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL diff --git a/doc/tools.enumpure.rst b/doc/tools.enumpure.rst index 29f17c3efd..2683c167df 100644 --- a/doc/tools.enumpure.rst +++ b/doc/tools.enumpure.rst @@ -54,7 +54,7 @@ See the :ref:`algorithm description ` for full details. Computing the pure-strategy equilibria of extensive game in Figure 2 of :cite:p:`Sel75`:: - $ gambit-enumpure catalog/journals/ijgt/selten1975/fig2.efg + $ gambit-enumpure catalog/games/journals/ijgt/selten1975/fig2.efg Search for Nash equilibria in pure strategies Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project @@ -66,7 +66,7 @@ With the `-S` switch, the set of equilibria returned is the same, except expressed in strategic game strategies rather than behavior strategies:: - $ gambit-enumpure -S catalog/journals/ijgt/selten1975/fig2.efg + $ gambit-enumpure -S catalog/games/journals/ijgt/selten1975/fig2.efg Search for Nash equilibria in pure strategies Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project @@ -78,7 +78,7 @@ The `-A` switch considers only behavior strategy profiles where there is no way for a player to improve his payoff by changing action at only one information set; therefore the set of solutions is larger:: - $ gambit-enumpure -A catalog/journals/ijgt/selten1975/fig2.efg + $ gambit-enumpure -A catalog/games/journals/ijgt/selten1975/fig2.efg Search for Nash equilibria in pure strategies Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL diff --git a/doc/tools.gnm.rst b/doc/tools.gnm.rst index a823be6554..c92fd2534d 100644 --- a/doc/tools.gnm.rst +++ b/doc/tools.gnm.rst @@ -90,7 +90,7 @@ subsets of equilibria being found. Computing an equilibrium of the reduced strategic form of the example in Figure 2 of :cite:p:`Sel75`:: - $ gambit-gnm catalog/journals/ijgt/selten1975/fig2.efg + $ gambit-gnm catalog/games/journals/ijgt/selten1975/fig2.efg Compute Nash equilibria using a global Newton method Gametracer version 0.2, Copyright (C) 2002, Ben Blum and Christian Shelton Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project diff --git a/doc/tools.hp.rst b/doc/tools.hp.rst index 86770fa328..b3a00b7d7a 100644 --- a/doc/tools.hp.rst +++ b/doc/tools.hp.rst @@ -73,7 +73,7 @@ Figure 2 of :cite:p:`Sel75`, starting from the prior in which player 1 plays (0.5, 0.3, 0.2) and player 2 plays (0.6, 0.4):: $ echo "0.5,0.3,0.2,0.6,0.4" > prior.csv - $ gambit-hp -s prior.csv catalog/journals/ijgt/selten1975/fig2.efg + $ gambit-hp -s prior.csv catalog/games/journals/ijgt/selten1975/fig2.efg Compute a Nash equilibrium using the Herings-Peeters (2001) homotopy method Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL diff --git a/doc/tools.ipa.rst b/doc/tools.ipa.rst index e0e1e765cd..d2f83f391f 100644 --- a/doc/tools.ipa.rst +++ b/doc/tools.ipa.rst @@ -61,7 +61,7 @@ equilibria being found. Computing an equilibrium of the reduced strategic form of the example in Figure 2 of :cite:p:`Sel75`:: - $ gambit-ipa catalog/journals/ijgt/selten1975/fig2.efg + $ gambit-ipa catalog/games/journals/ijgt/selten1975/fig2.efg Compute Nash equilibria using iterated polymatrix approximation Gametracer version 0.2, Copyright (C) 2002, Ben Blum and Christian Shelton Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project diff --git a/doc/tools.lcp.rst b/doc/tools.lcp.rst index c122fd0c5d..d3dbaa82e7 100644 --- a/doc/tools.lcp.rst +++ b/doc/tools.lcp.rst @@ -64,7 +64,7 @@ See the :ref:`algorithm description ` for full details. Computing an equilibrium of the example in Figure 2 of :cite:p:`Sel75`:: - $ gambit-lcp catalog/journals/ijgt/selten1975/fig2.efg + $ gambit-lcp catalog/games/journals/ijgt/selten1975/fig2.efg Compute Nash equilibria by solving a linear complementarity program Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL diff --git a/doc/tools.liap.rst b/doc/tools.liap.rst index f18c636752..78ade88c4b 100644 --- a/doc/tools.liap.rst +++ b/doc/tools.liap.rst @@ -97,7 +97,7 @@ See the :ref:`algorithm description ` for full details. Computing an equilibrium in mixed strategies of the example in Figure 2 of :cite:p:`Sel75`:: - $ gambit-liap catalog/journals/ijgt/selten1975/fig2.efg + $ gambit-liap catalog/games/journals/ijgt/selten1975/fig2.efg Compute Nash equilibria by minimizing the Lyapunov function Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL diff --git a/doc/tools.logit.rst b/doc/tools.logit.rst index a905c6fb7e..2b709b7c39 100644 --- a/doc/tools.logit.rst +++ b/doc/tools.logit.rst @@ -88,7 +88,7 @@ Computing the principal branch, in mixed strategies, of the reduced strategic form of the example in Figure 2 of :cite:p:`Sel75`:: - $ gambit-logit -S catalog/journals/ijgt/selten1975/fig2.efg + $ gambit-logit -S catalog/games/journals/ijgt/selten1975/fig2.efg Compute a branch of the logit equilibrium correspondence Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL diff --git a/doc/tools.simpdiv.rst b/doc/tools.simpdiv.rst index 5b42f2b868..14a7a1a33b 100644 --- a/doc/tools.simpdiv.rst +++ b/doc/tools.simpdiv.rst @@ -96,7 +96,7 @@ options to specify additional starting points for the algorithm. Computing an equilibrium in mixed strategies of the example in Figure 2 of :cite:p:`Sel75`:: - $ gambit-simpdiv catalog/journals/ijgt/selten1975/fig2.efg + $ gambit-simpdiv catalog/games/journals/ijgt/selten1975/fig2.efg Compute Nash equilibria using simplicial subdivision Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL diff --git a/doc/tutorials/interoperability_tutorials/gamut.ipynb b/doc/tutorials/interoperability_tutorials/gamut.ipynb index be80880614..17d4154f06 100644 --- a/doc/tutorials/interoperability_tutorials/gamut.ipynb +++ b/doc/tutorials/interoperability_tutorials/gamut.ipynb @@ -4,22 +4,7 @@ "cell_type": "markdown", "id": "gamut-intro", "metadata": {}, - "source": [ - "# Generating games with GAMUT\n", - "\n", - "[GAMUT](http://gamut.stanford.edu/) is a suite of parameterised game generators covering a wide range of game families studied in the game theory literature. Written in Java, GAMUT can generate instances of 35 game classes, including random games, coordination games, covariant games, voting games, and many more.\n", - "\n", - "PyGambit's `generate_gamut` function calls GAMUT as an external subprocess and returns the resulting game as a `Game` object, ready for analysis with PyGambit's full suite of tools. Before running this tutorial, you will need Java and `gamut.jar` installed; see the [catalog documentation](../../catalog.html#catalog-gamut) for full installation instructions.\n", - "\n", - "> **Note:** The cell outputs in this notebook were generated locally. To reproduce them, update the `gamut_jar` argument in each cell to the path of your local `gamut.jar`.\n", - "\n", - "This tutorial covers:\n", - "- Generating classic two-player games with no additional parameters\n", - "- Generating parameterised random normal-form games\n", - "- Exploring how the covariance parameter in `CovariantGame` affects equilibrium structure\n", - "- Generating multi-player games\n", - "- Controlling payoff normalisation and obtaining integer payoffs\n" - ] + "source": "# Generating games with GAMUT\n\n[GAMUT](http://gamut.stanford.edu/) is a suite of parameterised game generators covering a wide range of game families studied in the game theory literature. Written in Java, GAMUT can generate instances of 35 game classes, including random games, coordination games, covariant games, voting games, and many more.\n\nPyGambit's `generate_gamut` function calls GAMUT as an external subprocess and returns the resulting game as a `Game` object, ready for analysis with PyGambit's full suite of tools. Before running this tutorial, you will need Java and `gamut.jar` installed; see the [catalog documentation](../../catalog_generators.html#catalog-gamut) for full installation instructions.\n\n> **Note:** The cell outputs in this notebook were generated locally. To reproduce them, update the `gamut_jar` argument in each cell to the path of your local `gamut.jar`.\n\nThis tutorial covers:\n- Generating classic two-player games with no additional parameters\n- Generating parameterised random normal-form games\n- Exploring how the covariance parameter in `CovariantGame` affects equilibrium structure\n- Generating multi-player games\n- Controlling payoff normalisation and obtaining integer payoffs" }, { "cell_type": "code", diff --git a/pyproject.toml b/pyproject.toml index b3c9e30c48..6826e78f2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,7 +110,7 @@ max-line-length = 99 [tool.pytest.ini_options] addopts = "--strict-markers" -pythonpath = ["build_support/catalog", "build_support/releases"] +pythonpath = ["catalog", "build_support/releases"] markers = [ "nash_enumpure_strategy: tests of enumpure_solve in pure strategies", "nash_enumpure_agent: tests of enumpure_solve in pure behaviors", @@ -134,7 +134,7 @@ markers = [ "qre_logit_branch: tests of logit_solve_branch", "nash: all tests of Nash equilibrium solvers", "slow: all time-consuming tests", - "catalog_update: tests of build_support/catalog/update.py", + "catalog_update: tests of catalog/build.py", ] [tool.setuptools] diff --git a/setup.py b/setup.py index 35a70b8410..f0cc7f3663 100644 --- a/setup.py +++ b/setup.py @@ -84,7 +84,7 @@ class GambitBuildPy(setuptools.command.build_py.build_py): def run(self) -> None: super().run() - catalog_source = pathlib.Path("catalog") + catalog_source = pathlib.Path("catalog") / "games" catalog_target = pathlib.Path(self.build_lib) / "pygambit/catalog_data" if catalog_target.exists(): shutil.rmtree(catalog_target) diff --git a/src/pygambit/catalog.py b/src/pygambit/catalog.py index afdb0e3576..253ecceb79 100644 --- a/src/pygambit/catalog.py +++ b/src/pygambit/catalog.py @@ -11,13 +11,14 @@ import pygambit as gbt -# Use the full string path to where the catalog data are placed in the package -_CATALOG_RESOURCE = files("pygambit") / "catalog_data" -# This ensures that catalog files are included in editable installs too -if not _CATALOG_RESOURCE.is_dir(): - _repo_catalog = Path(__file__).parent.parent.parent / "catalog" - if _repo_catalog.is_dir(): - _CATALOG_RESOURCE = _repo_catalog +# Prefer a co-located repo checkout's catalog (editable/dev installs) over any packaged +# catalog_data. A prior non-editable install in the same environment can leave a stale +# catalog_data directory in site-packages that nothing else would clean up; checking the repo +# path first means it's never even consulted when a checkout is present (gambit#920). +_repo_catalog = Path(__file__).parent.parent.parent / "catalog" / "games" +_CATALOG_RESOURCE = ( + _repo_catalog if _repo_catalog.is_dir() else files("pygambit") / "catalog_data" +) READERS = { ".nfg": gbt.read_nfg, @@ -467,7 +468,7 @@ def append_record( if include_descriptions: record["Description"] = game.description ext = "efg" if game.is_tree else "nfg" - record["Download"] = f":download:`{slug}.{ext} <../catalog/{slug}.{ext}>`" + record["Download"] = f":download:`{slug}.{ext} <../games/{slug}.{ext}>`" record["Format"] = ext records.append(record)