From 61a0e81b220320f118c17752cf661c0de6b32145 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 26 May 2026 15:39:34 +0100 Subject: [PATCH 01/38] refactor: modularize catalog file management by moving the game file list to an included catalog.am file --- Makefile.am | 21 ++------------ build_support/catalog/catalog.am | 19 +++++++++++++ build_support/catalog/update.py | 47 +++++++++++++------------------- doc/developer.catalog.rst | 6 ++-- 4 files changed, 44 insertions(+), 49 deletions(-) create mode 100644 build_support/catalog/catalog.am diff --git a/Makefile.am b/Makefile.am index f07dd5034..425dbb66a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -22,6 +22,8 @@ ACLOCAL_AMFLAGS = -I m4 +include build_support/catalog/catalog.am + EXTRA_DIST = \ build_support/msw/gambit.wxs.in \ build_support/osx/Info.plist.in \ @@ -205,24 +207,7 @@ EXTRA_DIST = \ contrib/games/yamamoto.nfg \ contrib/games/zero.nfg \ src/README.rst \ - catalog/bagwell1995.efg \ - catalog/gilboa1997/fig2.efg \ - catalog/jakobsen2016/fig1a.efg \ - catalog/jakobsen2016/fig1b.efg \ - catalog/jakobsen2016/fig1c.efg \ - catalog/jakobsen2016/fig3.efg \ - catalog/myerson1991/fig2_1.efg \ - catalog/myerson1991/fig4_2.efg \ - catalog/nau2004/sec3.nfg \ - catalog/nau2004/sec4.nfg \ - catalog/nau2004/sec5.nfg \ - catalog/nau2004/sec6.nfg \ - catalog/reiley2008/fig1.efg \ - catalog/selten1975/fig1.efg \ - catalog/selten1975/fig2.efg \ - catalog/selten1975/fig3.efg \ - catalog/watson2013/exercise29_6.efg \ - catalog/watson2013/fig29_1.efg + $(CATALOG_FILES) core_SOURCES = \ src/core/core.h \ diff --git a/build_support/catalog/catalog.am b/build_support/catalog/catalog.am new file mode 100644 index 000000000..980afb135 --- /dev/null +++ b/build_support/catalog/catalog.am @@ -0,0 +1,19 @@ +CATALOG_FILES = \ + catalog/bagwell1995.efg \ + catalog/gilboa1997/fig2.efg \ + catalog/jakobsen2016/fig1a.efg \ + catalog/jakobsen2016/fig1b.efg \ + catalog/jakobsen2016/fig1c.efg \ + catalog/jakobsen2016/fig3.efg \ + catalog/myerson1991/fig2_1.efg \ + catalog/myerson1991/fig4_2.efg \ + catalog/nau2004/sec3.nfg \ + catalog/nau2004/sec4.nfg \ + catalog/nau2004/sec5.nfg \ + catalog/nau2004/sec6.nfg \ + catalog/reiley2008/fig1.efg \ + catalog/selten1975/fig1.efg \ + catalog/selten1975/fig2.efg \ + catalog/selten1975/fig3.efg \ + catalog/watson2013/exercise29_6.efg \ + catalog/watson2013/fig29_1.efg diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index 0d44b3cc6..b7ad2d767 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -121,7 +121,7 @@ def generate_rst_table(df: pd.DataFrame, rst_path: Path, regenerate_images: bool def update_makefile(): - """Update the Makefile.am with all games from the catalog.""" + """Update the catalog.am with all games from the catalog.""" # Using rglob("*") to find files in all subdirectories slugs = [] @@ -139,36 +139,27 @@ def update_makefile(): game_files.append(f"catalog/{slug}") game_files.sort() - with open(MAKEFILE_AM, encoding="utf-8") as f: - content = f.readlines() - - with open(MAKEFILE_AM, "w", encoding="utf-8") as f: - in_gamefiles_section = False - for line in content: - # Add to the EXTRA_DIST after the README.rst line - if line.startswith(" src/README.rst \\"): - in_gamefiles_section = True - f.write(" src/README.rst \\\n") - for gf in game_files: - if gf == game_files[-1]: - f.write(f"\t{gf}\n") - else: - f.write(f"\t{gf} \\\n") - f.write("\n") - elif in_gamefiles_section: - if line.strip() == "": - in_gamefiles_section = False - continue # Skip old gamefiles lines - else: - f.write(line) - - with open(MAKEFILE_AM, encoding="utf-8") as f: - updated_content = f.readlines() + am_path = Path(__file__).parent / "catalog.am" + + if am_path.exists(): + with open(am_path, encoding="utf-8") as f: + content = f.read() + else: + content = "" + + updated_content = "CATALOG_FILES = \\\n" + for gf in game_files: + if gf == game_files[-1]: + updated_content += f"\t{gf}\n" + else: + updated_content += f"\t{gf} \\\n" if content != updated_content: - print(f"Updated {str(MAKEFILE_AM)}") + with open(am_path, "w", encoding="utf-8") as f: + f.write(updated_content) + print(f"Updated {str(am_path)}") else: - print(f"No changes to add to {str(MAKEFILE_AM)}") + print(f"No changes to add to {str(am_path)}") if __name__ == "__main__": diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index 9ed5b51d2..8c98a7716 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -60,13 +60,13 @@ Currently supported representations are: .. warning:: - Running the script with the ``--build`` flag updates ``Makefile.am``. If you moved games that were previously in ``contrib/games`` you'll need to also manually remove those files from ``EXTRA_DIST``. + 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``. 4. **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/update.py`` and ``Makefile.am`` are included. + Ensure that any additions and changes to game files, ``build_support/catalog/update.py`` and ``build_support/catalog/catalog.am`` are included. .. important:: - If you didn't run the update script in step 3, you should manually update ``EXTRA_DIST`` in ``Makefile.am`` with any new game files. + If you didn't run the update script in step 3, you should manually update the list of files in ``build_support/catalog/catalog.am`` with any new game files. From 659cadd10ee0c6a1286cc3cd75eaa22fc4bf8915 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 26 May 2026 15:46:29 +0100 Subject: [PATCH 02/38] Step 3 is not optional --- doc/developer.catalog.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index 8c98a7716..726a2bac8 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -42,7 +42,7 @@ Currently supported representations are: pygambit.catalog.load("watson2013/exercise29_6") -3. **[Optional] Test your updates locally (and customise visuals):** +3. **Test your updates locally (and customise visuals):** Reinstall the package to pick up the new game file(s) in the ``pygambit.catalog`` module. Then use the ``update.py`` script to update Gambit's documentation & build files, as well as generating images for the new game(s). @@ -66,7 +66,3 @@ Currently supported representations are: Submit a PR according to the :ref:`usual workflow `. Ensure that any additions and changes to game files, ``build_support/catalog/update.py`` and ``build_support/catalog/catalog.am`` are included. - - .. important:: - - If you didn't run the update script in step 3, you should manually update the list of files in ``build_support/catalog/catalog.am`` with any new game files. From 66086d1e2247bccc971a5b76dbf93aaddc23721a Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 26 May 2026 15:59:56 +0100 Subject: [PATCH 03/38] Clarify docs building --- doc/developer.catalog.rst | 17 ++++++++++++++--- doc/developer.contributing.rst | 1 + 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index 726a2bac8..50fd994ee 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -28,7 +28,7 @@ Currently supported representations are: If no bibliography entry exists, you should add one by editing `doc/biblio.rst`. -2. **Commit the changes:** +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. @@ -42,7 +42,7 @@ Currently supported representations are: pygambit.catalog.load("watson2013/exercise29_6") -3. **Test your updates locally (and customise visuals):** +3. **Update the build files:** Reinstall the package to pick up the new game file(s) in the ``pygambit.catalog`` module. Then use the ``update.py`` script to update Gambit's documentation & build files, as well as generating images for the new game(s). @@ -62,7 +62,18 @@ Currently supported representations are: 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``. -4. **Submit a pull request to GitHub with all changes.** +4. **[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. + Iterate steps 2-4 as required. + +5. **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/update.py`` and ``build_support/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. diff --git a/doc/developer.contributing.rst b/doc/developer.contributing.rst index e890faba2..ab8851e37 100644 --- a/doc/developer.contributing.rst +++ b/doc/developer.contributing.rst @@ -176,6 +176,7 @@ Tests can be added to the test suite by creating new test files in the ``tests`` Tests should be written using the `pytest` framework. Refer to existing test files for examples of how to write tests or see the `pytest documentation `_ for more information. +.. _editing-docs: Editing this documentation --------------------------- From fa168435985763637eeafda9304aa594bbbb81cf Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 26 May 2026 16:43:50 +0100 Subject: [PATCH 04/38] docs: update development guide and update catalog path resolution to support editable installs --- doc/developer.catalog.rst | 10 ++++++---- src/pygambit/catalog.py | 7 ++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index 50fd994ee..6551e90ff 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -13,10 +13,13 @@ Currently supported representations are: - `.efg` for extensive form games - `.nfg` for normal form games -.. note:: +.. important:: When updating the catalog, changes can be viewed by inspecting the documentation build generated by a pull request. - To test changes locally, you'll need to have a developer install of `pygambit` available in your Python environment, see :ref:`build-python`. + To test changes locally, you'll need to have an editable developer install of `pygambit` available in your Python environment. + + .. code-block:: python + pip install -e ".[doc]" 1. **Create or edit a game file:** @@ -44,14 +47,13 @@ Currently supported representations are: 3. **Update the build files:** - Reinstall the package to pick up the new game file(s) in the ``pygambit.catalog`` module. + 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, you can first edit the ``catalog_draw_tree_settings`` in ``build_support/catalog/update.py`` to change the default visualisation parameters for your game(s). Consult the `DrawTree docs `_ for the available options. .. code-block:: bash - pip install . python build_support/catalog/update.py --build .. note:: diff --git a/src/pygambit/catalog.py b/src/pygambit/catalog.py index 6b24f17eb..01f6d353b 100644 --- a/src/pygambit/catalog.py +++ b/src/pygambit/catalog.py @@ -7,7 +7,12 @@ 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" +_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 READERS = { ".nfg": gbt.read_nfg, From 583cc64fd33283f61a032a97afc87cd488bd3e80 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 26 May 2026 16:49:46 +0100 Subject: [PATCH 05/38] fix indentation --- doc/developer.catalog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index 6551e90ff..fe174ba67 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -19,7 +19,7 @@ Currently supported representations are: To test changes locally, you'll need to have an editable developer install of `pygambit` available in your Python environment. .. code-block:: python - pip install -e ".[doc]" + pip install -e ".[doc]" 1. **Create or edit a game file:** From 43886a125d827989b18b7d066157e635989c0a57 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 26 May 2026 16:55:39 +0100 Subject: [PATCH 06/38] docs: correct code block language hint for editable install instructions --- doc/developer.catalog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index fe174ba67..217ed42aa 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -18,7 +18,8 @@ Currently supported representations are: When updating the catalog, changes can be viewed by inspecting the documentation build generated by a pull request. To test changes locally, you'll need to have an editable developer install of `pygambit` available in your Python environment. - .. code-block:: python + .. code-block:: bash + pip install -e ".[doc]" 1. **Create or edit a game file:** From 9e0b74e68c7dd093739f9e4592daab49d8957901 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 27 May 2026 09:39:16 +0100 Subject: [PATCH 07/38] docs: clarify when to use developer install in catalog dev doc --- doc/developer.catalog.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index 217ed42aa..2173c943a 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -16,7 +16,8 @@ Currently supported representations are: .. important:: When updating the catalog, changes can be viewed by inspecting the documentation build generated by a pull request. - To test changes locally, you'll need to have an editable developer install of `pygambit` available in your Python environment. + 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. .. code-block:: bash @@ -63,7 +64,8 @@ Currently supported representations are: .. warning:: - 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 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``. 4. **[Optional] Test your updates to the documentation locally:** From 820e982aab69b5e8f98c3e781fc54dda3d79c8f6 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 27 May 2026 10:01:56 +0100 Subject: [PATCH 08/38] refactor: migrate game visualisation parameters to external YAML configuration file --- build_support/catalog/draw_tree_settings.yaml | 24 +++++++++++++++++++ build_support/catalog/update.py | 22 +++++++---------- doc/developer.catalog.rst | 8 ++++--- pyproject.toml | 1 + 4 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 build_support/catalog/draw_tree_settings.yaml diff --git a/build_support/catalog/draw_tree_settings.yaml b/build_support/catalog/draw_tree_settings.yaml new file mode 100644 index 000000000..d969f4d11 --- /dev/null +++ b/build_support/catalog/draw_tree_settings.yaml @@ -0,0 +1,24 @@ +# Default draw_tree settings applied to all catalog games. +defaults: + color_scheme: gambit + font_family: sffamily + font_italic: true + shared_terminal_depth: true + sublevel_scaling: 0 + +# Per-game overrides. Keys are slug prefixes or exact slugs. +# For a given game, all matching keys are applied shortest-first, +# so more specific entries (longer keys) override more general ones. +# A key matches if slug == key or slug starts with key + "/". +# Consult https://www.gambit-project.org/draw_tree/ for available settings. +overrides: + bagwell1995: + sublevel_scaling: 1 + watson2013: + sublevel_scaling: 1 + selten1975: + shared_terminal_depth: false + myerson1991/fig2_1: + action_label_position: 0.4 + reiley2008/fig1: + action_label_position: 0.4 diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index b7ad2d767..1d0745f22 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -2,6 +2,7 @@ from pathlib import Path import pandas as pd +import yaml from draw_tree import generate_pdf, generate_png, generate_svg, generate_tex import pygambit as gbt @@ -9,23 +10,18 @@ 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" +DRAW_TREE_SETTINGS_CONFIG = Path(__file__).parent / "draw_tree_settings.yaml" def catalog_draw_tree_settings(slug: str) -> dict: """Return the draw_tree settings for a given catalog slug.""" - settings = { - "color_scheme": "gambit", - "font_family": "sffamily", - "font_italic": True, - "shared_terminal_depth": True, - "sublevel_scaling": 0, - } - if slug == "bagwell1995" or "watson2013" in slug: - settings["sublevel_scaling"] = 1 - elif slug == "myerson1991/fig2_1" or slug == "reiley2008/fig1": - settings["action_label_position"] = 0.4 - elif "selten1975" in slug: - settings["shared_terminal_depth"] = False + with open(DRAW_TREE_SETTINGS_CONFIG, encoding="utf-8") as f: + config = yaml.safe_load(f) + settings = dict(config["defaults"]) + overrides = config.get("overrides", {}) + for key in sorted(overrides, key=len): + if slug == key or slug.startswith(key + "/"): + settings.update(overrides[key]) return settings diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index 2173c943a..6e3a5a2ec 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -51,8 +51,10 @@ Currently supported representations are: 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, you can first edit the ``catalog_draw_tree_settings`` in ``build_support/catalog/update.py`` to change the default visualisation parameters for your game(s). - Consult the `DrawTree docs `_ for the available options. + If you want to customise the visualisation parameters for your game(s), edit ``build_support/catalog/draw_tree_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 `DrawTree docs `_ for available settings. .. code-block:: bash @@ -77,7 +79,7 @@ Currently supported representations are: 5. **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/update.py`` and ``build_support/catalog/catalog.am`` are included. + Ensure that any additions and changes to game files, ``build_support/catalog/draw_tree_settings.yaml``, ``build_support/catalog/update.py`` and ``build_support/catalog/catalog.am`` are included. .. important:: diff --git a/pyproject.toml b/pyproject.toml index 601a91599..71e5f0065 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ doc = [ "open_spiel; sys_platform != 'win32'", "sphinxcontrib-tikz", "jupyter_sphinx", + "pyyaml", ] [project.urls] From 55f4ba9108410d7890e809a33f2709d3c5561565 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 27 May 2026 10:52:25 +0100 Subject: [PATCH 09/38] feat: allow curated .ef files to override auto-generated DrawTree layouts for catalog games --- build_support/catalog/update.py | 13 +++++++++++-- doc/developer.catalog.rst | 8 ++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index 1d0745f22..9ce1e040e 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -1,4 +1,5 @@ import argparse +import shutil from pathlib import Path import pandas as pd @@ -61,11 +62,15 @@ def generate_rst_table(df: pd.DataFrame, rst_path: Path, regenerate_images: bool missing_any = not all(p.exists() for p in all_paths) if regenerate_images or missing_any: - g = gbt.catalog.load(slug) viz_path = CATALOG_DIR / "img" / f"{slug}" viz_path.parent.mkdir(parents=True, exist_ok=True) + curated_ef = CATALOG_DIR / f"{slug}.ef" + source = str(curated_ef) if curated_ef.exists() else gbt.catalog.load(slug) for func in [generate_tex, generate_png, generate_pdf, generate_svg]: - func(g, save_to=str(viz_path), **catalog_draw_tree_settings(slug)) + func(source, save_to=str(viz_path), **catalog_draw_tree_settings(slug)) + img_ef = CATALOG_DIR / "img" / f"{slug}.ef" + if not img_ef.exists() and curated_ef.exists(): + shutil.copy2(curated_ef, img_ef) # Main dropdown f.write(f" * - .. dropdown:: {title}\n") @@ -129,6 +134,10 @@ def update_makefile(): if resource_path.is_file(): rel_path = resource_path.relative_to(CATALOG_DIR) slugs.append(str(rel_path)) + for resource_path in sorted(CATALOG_DIR.rglob("*.ef")): + if resource_path.is_file() and CATALOG_DIR / "img" not in resource_path.parents: + rel_path = resource_path.relative_to(CATALOG_DIR) + slugs.append(str(rel_path)) game_files = [] for slug in slugs: diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index 6e3a5a2ec..deacfb6c3 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -39,6 +39,14 @@ Currently supported representations are: Add your new game file(s) inside the ``catalog`` dir and commit them, or edit an existing game. If there are multiple games from a particular source, place them in an appropriately named folder. + .. note:: + + 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 DrawTree instead of + auto-generating the layout from the ``.efg``, preserving any hand-tuned layout. + Consult the `DrawTree docs `_ for the ``.ef`` format. + .. important:: The name of the game file will determine it's "slug", used by the load function of the catalog module: From f49fd5235480d8c4c14bdd038e67e0ed8b2dcec5 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 27 May 2026 11:08:32 +0100 Subject: [PATCH 10/38] feat: update catalog image display to support curated EF files when present --- build_support/catalog/update.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index 9ce1e040e..d20da5f18 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -107,11 +107,19 @@ def generate_rst_table(df: pd.DataFrame, rst_path: Path, regenerate_images: bool if row["Format"] == "efg": settings = catalog_draw_tree_settings(slug) settings_str = ", ".join(f"{k}={v!r}" for k, v in settings.items()) - f.write( - f" draw_tree(" - f'pygambit.catalog.load("{slug}"), ' - f"{settings_str})\n" - ) + curated_ef = CATALOG_DIR / f"{slug}.ef" + if curated_ef.exists(): + f.write( + f" draw_tree(" + f'"../catalog/{slug}.ef", ' + f"{settings_str})\n" + ) + else: + f.write( + f" draw_tree(" + f'pygambit.catalog.load("{slug}"), ' + f"{settings_str})\n" + ) elif row["Format"] == "nfg": f.write( f" draw_tree(" From f0d7463485e453e95b82377501ce589f0d849259 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 27 May 2026 11:42:10 +0100 Subject: [PATCH 11/38] Update comments and add help to args for update.py --- build_support/catalog/update.py | 46 ++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index d20da5f18..73841e289 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -12,6 +12,7 @@ CATALOG_DIR = Path(__file__).parent.parent.parent / "catalog" MAKEFILE_AM = Path(__file__).parent.parent.parent / "Makefile.am" DRAW_TREE_SETTINGS_CONFIG = Path(__file__).parent / "draw_tree_settings.yaml" +SUPPORTED_GAME_FORMATS = {"efg", "nfg"} def catalog_draw_tree_settings(slug: str) -> dict: @@ -20,6 +21,8 @@ def catalog_draw_tree_settings(slug: str) -> dict: config = yaml.safe_load(f) settings = dict(config["defaults"]) overrides = config.get("overrides", {}) + # Apply overrides shortest-key-first so that more specific (longer) entries + # such as "myerson1991/fig2_1" win over group-level entries like "myerson1991". for key in sorted(overrides, key=len): if slug == key or slug.startswith(key + "/"): settings.update(overrides[key]) @@ -45,8 +48,13 @@ def generate_rst_table(df: pd.DataFrame, rst_path: Path, regenerate_images: bool slug = row["Game"] title = str(row.get("Title", "")).strip() description = str(row.get("Description", "")).strip() + # Skip rows with unrecognised formats (defensive guard). + if row["Format"] not in SUPPORTED_GAME_FORMATS: + continue # Skip any games which lack a description if description: + # Build the list of expected image files so we can check whether + # any are missing and need to be generated. all_exts = [] all_paths = [] if row["Format"] == "efg": @@ -64,10 +72,14 @@ def generate_rst_table(df: pd.DataFrame, rst_path: Path, regenerate_images: bool if regenerate_images or missing_any: viz_path = CATALOG_DIR / "img" / f"{slug}" viz_path.parent.mkdir(parents=True, exist_ok=True) + # Use a committed curated .ef file if present; otherwise derive + # the layout automatically from the game object. curated_ef = CATALOG_DIR / f"{slug}.ef" source = str(curated_ef) if curated_ef.exists() else gbt.catalog.load(slug) for func in [generate_tex, generate_png, generate_pdf, generate_svg]: func(source, save_to=str(viz_path), **catalog_draw_tree_settings(slug)) + # DrawTree may not write catalog/img/{slug}.ef when its input is + # already an .ef file, so copy it if the img copy is still absent. img_ef = CATALOG_DIR / "img" / f"{slug}.ef" if not img_ef.exists() and curated_ef.exists(): shutil.copy2(curated_ef, img_ef) @@ -110,9 +122,7 @@ def generate_rst_table(df: pd.DataFrame, rst_path: Path, regenerate_images: bool curated_ef = CATALOG_DIR / f"{slug}.ef" if curated_ef.exists(): f.write( - f" draw_tree(" - f'"../catalog/{slug}.ef", ' - f"{settings_str})\n" + f' draw_tree("../catalog/{slug}.ef", {settings_str})\n' ) else: f.write( @@ -132,7 +142,6 @@ def generate_rst_table(df: pd.DataFrame, rst_path: Path, regenerate_images: bool def update_makefile(): """Update the catalog.am with all games from the catalog.""" - # Using rglob("*") to find files in all subdirectories slugs = [] for resource_path in sorted(CATALOG_DIR.rglob("*.efg")): if resource_path.is_file(): @@ -143,6 +152,8 @@ def update_makefile(): rel_path = resource_path.relative_to(CATALOG_DIR) slugs.append(str(rel_path)) for resource_path in sorted(CATALOG_DIR.rglob("*.ef")): + # Exclude the generated .ef files under catalog/img/; only curated + # .ef files committed alongside game files should be distributed. if resource_path.is_file() and CATALOG_DIR / "img" not in resource_path.parents: rel_path = resource_path.relative_to(CATALOG_DIR) slugs.append(str(rel_path)) @@ -176,9 +187,30 @@ def update_makefile(): if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--build", action="store_true") - parser.add_argument("--regenerate-images", action="store_true") + 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/." + ) + ) + parser.add_argument( + "--build", + action="store_true", + help=( + "Also update build_support/catalog/catalog.am with the current list of " + "catalog game files. Required after adding or removing games." + ), + ) + parser.add_argument( + "--regenerate-images", + action="store_true", + help=( + "Force regeneration of all game visualisation images (PNG, PDF, SVG, TeX), " + "even if they already exist. Use this to pick up changes to game files or " + "draw_tree_settings.yaml." + ), + ) args = parser.parse_args() # Create RST list-table used by doc/catalog.rst From 2f36d8bb322bb80a77a7d785f39887e66ddfe4de Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 27 May 2026 11:42:48 +0100 Subject: [PATCH 12/38] refactor: improve catalog game loading ensure only proper EFG and NFG files are included --- src/pygambit/catalog.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/pygambit/catalog.py b/src/pygambit/catalog.py index 01f6d353b..961c2a532 100644 --- a/src/pygambit/catalog.py +++ b/src/pygambit/catalog.py @@ -160,20 +160,23 @@ def append_record( record["Format"] = ext records.append(record) - # Add all the games stored as EFG/NFG files - for resource_path in sorted(_CATALOG_RESOURCE.rglob("*")): - reader = READERS.get(resource_path.suffix) - - if reader is not None and resource_path.is_file(): - # Calculate the path relative to the root resource - # and remove the suffix to get the "slug" - rel_path = resource_path.relative_to(_CATALOG_RESOURCE) - slug = rel_path.with_suffix("").as_posix() - - with as_file(resource_path) as path: - game = reader(str(path)) - if check_filters(game): - append_record(slug, game) + # Add all the games stored as EFG/NFG files. + # Collect paths matching each supported extension, sort together to preserve + # a consistent alphabetical order, then load using the known reader. + for resource_path in sorted( + path + for suffix in READERS + for path in _CATALOG_RESOURCE.rglob(f"*{suffix}") + if path.is_file() + ): + reader = READERS[resource_path.suffix] + rel_path = resource_path.relative_to(_CATALOG_RESOURCE) + slug = rel_path.with_suffix("").as_posix() + + with as_file(resource_path) as path: + game = reader(str(path)) + if check_filters(game): + append_record(slug, game) if include_descriptions: return pd.DataFrame.from_records( From 2dc6efcd45616e5ce3c7ee157d2c0b50053ff2ff Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 27 May 2026 13:34:37 +0100 Subject: [PATCH 13/38] test: add suite for catalog update logic and update test dependencies --- build_support/catalog/test_update.py | 310 +++++++++++++++++++++++++++ build_support/catalog/update.py | 49 +++-- pyproject.toml | 2 + 3 files changed, 341 insertions(+), 20 deletions(-) create mode 100644 build_support/catalog/test_update.py diff --git a/build_support/catalog/test_update.py b/build_support/catalog/test_update.py new file mode 100644 index 000000000..aebe7bd0b --- /dev/null +++ b/build_support/catalog/test_update.py @@ -0,0 +1,310 @@ +"""Tests for build_support/catalog/update.py.""" + +import pytest + +pytest.importorskip("draw_tree") # update.py imports draw_tree at module level +pytest.importorskip("yaml") + +import pandas as pd # noqa: E402 +import update # noqa: E402 + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +_YAML_DEFAULTS = { + "color_scheme": "gambit", + "font_family": "sffamily", + "font_italic": True, + "shared_terminal_depth": True, + "sublevel_scaling": 0, +} + +_YAML_CONFIG = """ +defaults: + color_scheme: gambit + font_family: sffamily + font_italic: true + shared_terminal_depth: true + sublevel_scaling: 0 + +overrides: + watson2013: + sublevel_scaling: 1 + selten1975: + shared_terminal_depth: false + myerson1991/fig2_1: + action_label_position: 0.4 +""" + + +def _write_yaml(path, content=_YAML_CONFIG): + path.write_text(content, encoding="utf-8") + return path + + +def _efg_row(slug, title="Game Title", description="A description."): + return { + "Game": slug, + "Title": title, + "Description": description, + "Download": f":download:`{slug}.efg <../catalog/{slug}.efg>`", + "Format": "efg", + } + + +def _nfg_row(slug, title="NFG Title", description="NFG description."): + return { + "Game": slug, + "Title": title, + "Description": description, + "Download": f":download:`{slug}.nfg <../catalog/{slug}.nfg>`", + "Format": "nfg", + } + + +def _make_df(*rows): + return pd.DataFrame(list(rows)) + + +def _make_image_files(catalog_dir, slug, fmt="efg"): + """Create the stub image files so the existence check passes.""" + img = catalog_dir / "img" / slug + img.parent.mkdir(parents=True, exist_ok=True) + for ext in ["tex", "png", "pdf", "svg"]: + (catalog_dir / "img" / f"{slug}.{ext}").touch() + if fmt == "efg": + (catalog_dir / "img" / f"{slug}.ef").touch() + + +# --------------------------------------------------------------------------- +# catalog_draw_tree_settings +# --------------------------------------------------------------------------- + + +@pytest.mark.catalog_update +class TestCatalogDrawTreeSettings: + def test_no_override_returns_defaults(self, tmp_path, monkeypatch): + yaml_file = _write_yaml(tmp_path / "settings.yaml") + monkeypatch.setattr(update, "DRAW_TREE_SETTINGS_CONFIG", yaml_file) + result = update.catalog_draw_tree_settings("unknown/game") + assert result == _YAML_DEFAULTS + + def test_exact_slug_override_applied(self, tmp_path, monkeypatch): + yaml_file = _write_yaml(tmp_path / "settings.yaml") + monkeypatch.setattr(update, "DRAW_TREE_SETTINGS_CONFIG", yaml_file) + result = update.catalog_draw_tree_settings("myerson1991/fig2_1") + 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): + yaml_file = _write_yaml(tmp_path / "settings.yaml") + monkeypatch.setattr(update, "DRAW_TREE_SETTINGS_CONFIG", yaml_file) + result = update.catalog_draw_tree_settings("watson2013/exercise29_6") + assert result["sublevel_scaling"] == 1 + + def test_specific_key_wins_over_group(self, tmp_path, monkeypatch): + config = """ + defaults: + color_scheme: gambit + sublevel_scaling: 0 + overrides: + myerson1991: + sublevel_scaling: 1 + myerson1991/fig2_1: + sublevel_scaling: 2 + """ + yaml_file = _write_yaml(tmp_path / "settings.yaml", config) + monkeypatch.setattr(update, "DRAW_TREE_SETTINGS_CONFIG", yaml_file) + result = update.catalog_draw_tree_settings("myerson1991/fig2_1") + assert result["sublevel_scaling"] == 2 + + def test_group_override_does_not_bleed_to_other_game(self, tmp_path, monkeypatch): + yaml_file = _write_yaml(tmp_path / "settings.yaml") + monkeypatch.setattr(update, "DRAW_TREE_SETTINGS_CONFIG", yaml_file) + result = update.catalog_draw_tree_settings("selten1975/fig1") + assert result["shared_terminal_depth"] is False + result2 = update.catalog_draw_tree_settings("watson2013/fig29_1") + assert result2["shared_terminal_depth"] is True # selten override not applied + + def test_no_overrides_section_returns_defaults(self, tmp_path, monkeypatch): + config = "defaults:\n color_scheme: gambit\n sublevel_scaling: 0\n" + yaml_file = _write_yaml(tmp_path / "settings.yaml", config) + monkeypatch.setattr(update, "DRAW_TREE_SETTINGS_CONFIG", yaml_file) + result = update.catalog_draw_tree_settings("any/game") + assert result == {"color_scheme": "gambit", "sublevel_scaling": 0} + + +# --------------------------------------------------------------------------- +# generate_rst_table +# --------------------------------------------------------------------------- + + +@pytest.mark.catalog_update +class TestGenerateRstTable: + def _no_op_generate(self, *args, **kwargs): + """Replacement for draw_tree generate_* functions that does nothing.""" + + def _mock_generates(self, monkeypatch): + for name in ["generate_tex", "generate_png", "generate_pdf", "generate_svg"]: + monkeypatch.setattr(update, name, self._no_op_generate) + + def test_efg_row_produces_rst_with_slug_and_title(self, tmp_path, monkeypatch): + self._mock_generates(monkeypatch) + catalog_dir = tmp_path / "catalog" + slug = "bagwell1995" + _make_image_files(catalog_dir, slug, "efg") + df = _make_df(_efg_row(slug, title="Bagwell 1995")) + rst_path = tmp_path / "out.rst" + update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + rst = rst_path.read_text() + assert "Bagwell 1995" in rst + assert f'pygambit.catalog.load("{slug}")' in rst + assert f":download:`{slug}.efg" in rst + assert f":download:`{slug}.ef" in rst + + def test_nfg_row_produces_rst_with_save_to(self, tmp_path, monkeypatch): + self._mock_generates(monkeypatch) + catalog_dir = tmp_path / "catalog" + slug = "nau2004/sec3" + _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) + rst = rst_path.read_text() + assert f'save_to="../catalog/img/{slug}.png"' in rst + + def test_unknown_format_row_is_skipped(self, tmp_path, monkeypatch): + self._mock_generates(monkeypatch) + catalog_dir = tmp_path / "catalog" + catalog_dir.mkdir() + row = { + "Game": "bogus/game", + "Title": "Bogus", + "Description": "Has a description.", + "Download": "", + "Format": "efg_2", + } + df = _make_df(row) + rst_path = tmp_path / "out.rst" + update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + rst = rst_path.read_text() + assert "Bogus" not in rst + + def test_row_without_description_is_skipped(self, tmp_path, monkeypatch): + self._mock_generates(monkeypatch) + catalog_dir = tmp_path / "catalog" + catalog_dir.mkdir() + df = _make_df(_efg_row("bagwell1995", description="")) + rst_path = tmp_path / "out.rst" + update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + rst = rst_path.read_text() + assert "bagwell1995" not in rst + + def test_curated_ef_used_in_draw_tree_call(self, tmp_path, monkeypatch): + self._mock_generates(monkeypatch) + catalog_dir = tmp_path / "catalog" + slug = "selten1975/fig1" + _make_image_files(catalog_dir, slug, "efg") + # Place a curated .ef alongside the .efg + 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) + rst = rst_path.read_text() + # The draw_tree() call line should reference the .ef file path directly + draw_tree_call = next(line for line in rst.splitlines() if "draw_tree(" in line) + assert f'"../catalog/{slug}.ef"' in draw_tree_call + assert "catalog.load" not in draw_tree_call + + def test_images_not_regenerated_when_all_exist(self, tmp_path, monkeypatch): + calls = [] + monkeypatch.setattr(update, "generate_tex", lambda *a, **k: calls.append("tex")) + monkeypatch.setattr(update, "generate_png", lambda *a, **k: calls.append("png")) + monkeypatch.setattr(update, "generate_pdf", lambda *a, **k: calls.append("pdf")) + monkeypatch.setattr(update, "generate_svg", lambda *a, **k: calls.append("svg")) + catalog_dir = tmp_path / "catalog" + slug = "bagwell1995" + _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, regenerate_images=False, catalog_dir=catalog_dir) + assert calls == [] + + def test_images_regenerated_when_flag_set(self, tmp_path, monkeypatch): + calls = [] + monkeypatch.setattr(update, "generate_tex", lambda *a, **k: calls.append("tex")) + monkeypatch.setattr(update, "generate_png", lambda *a, **k: calls.append("png")) + monkeypatch.setattr(update, "generate_pdf", lambda *a, **k: calls.append("pdf")) + monkeypatch.setattr(update, "generate_svg", lambda *a, **k: calls.append("svg")) + catalog_dir = tmp_path / "catalog" + slug = "bagwell1995" + _make_image_files(catalog_dir, slug, "efg") + # Use a curated .ef so gbt.catalog.load is not called (avoids needing real catalog) + curated = catalog_dir / f"{slug}.ef" + 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) + assert set(calls) == {"tex", "png", "pdf", "svg"} + + +# --------------------------------------------------------------------------- +# update_makefile +# --------------------------------------------------------------------------- + + +@pytest.mark.catalog_update +class TestUpdateMakefile: + def test_efg_and_nfg_files_included(self, tmp_path): + (tmp_path / "foo.efg").touch() + (tmp_path / "sub").mkdir() + (tmp_path / "sub" / "bar.nfg").touch() + am = tmp_path / "catalog.am" + update.update_makefile(catalog_dir=tmp_path, am_path=am) + content = am.read_text() + assert "catalog/foo.efg" in content + assert "catalog/sub/bar.nfg" in content + + def test_curated_ef_included(self, tmp_path): + (tmp_path / "myerson1991").mkdir() + (tmp_path / "myerson1991" / "fig1.efg").touch() + (tmp_path / "myerson1991" / "fig1.ef").touch() + am = tmp_path / "catalog.am" + update.update_makefile(catalog_dir=tmp_path, am_path=am) + content = am.read_text() + assert "catalog/myerson1991/fig1.ef" in content + + def test_ef_in_img_dir_excluded(self, tmp_path): + img = tmp_path / "img" / "selten1975" + img.mkdir(parents=True) + (img / "fig1.ef").touch() + am = tmp_path / "catalog.am" + update.update_makefile(catalog_dir=tmp_path, am_path=am) + content = am.read_text() + assert "img" not in content + + def test_non_game_file_excluded(self, tmp_path): + (tmp_path / "foo.efg_2").touch() + (tmp_path / "readme.txt").touch() + am = tmp_path / "catalog.am" + update.update_makefile(catalog_dir=tmp_path, am_path=am) + content = am.read_text() + assert "efg_2" not in content + assert "readme" not in content + + def test_no_write_when_content_unchanged(self, tmp_path): + (tmp_path / "foo.efg").touch() + am = tmp_path / "catalog.am" + update.update_makefile(catalog_dir=tmp_path, am_path=am) + mtime_after_first = am.stat().st_mtime + update.update_makefile(catalog_dir=tmp_path, am_path=am) + assert am.stat().st_mtime == mtime_after_first + + def test_empty_catalog_produces_valid_am(self, tmp_path): + am = tmp_path / "catalog.am" + update.update_makefile(catalog_dir=tmp_path, am_path=am) + content = am.read_text() + assert content.startswith("CATALOG_FILES =") diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index 73841e289..a6f9991bd 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -29,8 +29,14 @@ def catalog_draw_tree_settings(slug: str) -> dict: return settings -def generate_rst_table(df: pd.DataFrame, rst_path: Path, regenerate_images: bool = False): +def generate_rst_table( + df: pd.DataFrame, + rst_path: Path, + regenerate_images: bool = False, + catalog_dir: Path | None = None, +): """Generate RST output with a list-table for games.""" + catalog_dir = catalog_dir or CATALOG_DIR with open(rst_path, "w", encoding="utf-8") as f: # TOC linking to both sections f.write(".. contents::\n") @@ -58,29 +64,29 @@ def generate_rst_table(df: pd.DataFrame, rst_path: Path, regenerate_images: bool all_exts = [] all_paths = [] if row["Format"] == "efg": - ef_path = CATALOG_DIR / "img" / f"{slug}.ef" + ef_path = catalog_dir / "img" / f"{slug}.ef" all_exts.append("ef") all_paths.append(ef_path) all_exts = all_exts + ["tex", "png", "pdf", "svg"] - tex_path = CATALOG_DIR / "img" / f"{slug}.tex" + tex_path = catalog_dir / "img" / f"{slug}.tex" all_paths.append(tex_path) - all_paths.append(CATALOG_DIR / "img" / f"{slug}.png") - all_paths.append(CATALOG_DIR / "img" / f"{slug}.pdf") - all_paths.append(CATALOG_DIR / "img" / f"{slug}.svg") + all_paths.append(catalog_dir / "img" / f"{slug}.png") + all_paths.append(catalog_dir / "img" / f"{slug}.pdf") + all_paths.append(catalog_dir / "img" / f"{slug}.svg") missing_any = not all(p.exists() for p in all_paths) if regenerate_images or missing_any: - viz_path = CATALOG_DIR / "img" / f"{slug}" + viz_path = catalog_dir / "img" / f"{slug}" viz_path.parent.mkdir(parents=True, exist_ok=True) # Use a committed curated .ef file if present; otherwise derive # the layout automatically from the game object. - curated_ef = CATALOG_DIR / f"{slug}.ef" + curated_ef = catalog_dir / f"{slug}.ef" source = str(curated_ef) if curated_ef.exists() else gbt.catalog.load(slug) for func in [generate_tex, generate_png, generate_pdf, generate_svg]: func(source, save_to=str(viz_path), **catalog_draw_tree_settings(slug)) # DrawTree may not write catalog/img/{slug}.ef when its input is # already an .ef file, so copy it if the img copy is still absent. - img_ef = CATALOG_DIR / "img" / f"{slug}.ef" + img_ef = catalog_dir / "img" / f"{slug}.ef" if not img_ef.exists() and curated_ef.exists(): shutil.copy2(curated_ef, img_ef) @@ -119,7 +125,7 @@ def generate_rst_table(df: pd.DataFrame, rst_path: Path, regenerate_images: bool if row["Format"] == "efg": settings = catalog_draw_tree_settings(slug) settings_str = ", ".join(f"{k}={v!r}" for k, v in settings.items()) - curated_ef = CATALOG_DIR / f"{slug}.ef" + curated_ef = catalog_dir / f"{slug}.ef" if curated_ef.exists(): f.write( f' draw_tree("../catalog/{slug}.ef", {settings_str})\n' @@ -139,23 +145,28 @@ def generate_rst_table(df: pd.DataFrame, rst_path: Path, regenerate_images: bool f.write(" \n") -def update_makefile(): +def update_makefile( + catalog_dir: Path | None = None, + am_path: Path | None = None, +): """Update the catalog.am with all games from the catalog.""" + catalog_dir = catalog_dir or CATALOG_DIR + am_path = am_path or Path(__file__).parent / "catalog.am" slugs = [] - for resource_path in sorted(CATALOG_DIR.rglob("*.efg")): + for resource_path in sorted(catalog_dir.rglob("*.efg")): if resource_path.is_file(): - rel_path = resource_path.relative_to(CATALOG_DIR) + rel_path = resource_path.relative_to(catalog_dir) slugs.append(str(rel_path)) - for resource_path in sorted(CATALOG_DIR.rglob("*.nfg")): + for resource_path in sorted(catalog_dir.rglob("*.nfg")): if resource_path.is_file(): - rel_path = resource_path.relative_to(CATALOG_DIR) + rel_path = resource_path.relative_to(catalog_dir) slugs.append(str(rel_path)) - for resource_path in sorted(CATALOG_DIR.rglob("*.ef")): + for resource_path in sorted(catalog_dir.rglob("*.ef")): # Exclude the generated .ef files under catalog/img/; only curated # .ef files committed alongside game files should be distributed. - if resource_path.is_file() and CATALOG_DIR / "img" not in resource_path.parents: - rel_path = resource_path.relative_to(CATALOG_DIR) + if resource_path.is_file() and catalog_dir / "img" not in resource_path.parents: + rel_path = resource_path.relative_to(catalog_dir) slugs.append(str(rel_path)) game_files = [] @@ -163,8 +174,6 @@ def update_makefile(): game_files.append(f"catalog/{slug}") game_files.sort() - am_path = Path(__file__).parent / "catalog.am" - if am_path.exists(): with open(am_path, encoding="utf-8") as f: content = f.read() diff --git a/pyproject.toml b/pyproject.toml index 71e5f0065..22c8faaf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,6 +94,7 @@ max-line-length = 99 [tool.pytest.ini_options] addopts = "--strict-markers" +pythonpath = ["build_support/catalog"] markers = [ "nash_enumpure_strategy: tests of enumpure_solve in pure strategies", "nash_enumpure_agent: tests of enumpure_solve in pure behaviors", @@ -115,6 +116,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", ] [tool.setuptools] From 0e16262b7624aa9b1c6e54be6763473b64aa4969 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 27 May 2026 13:36:07 +0100 Subject: [PATCH 14/38] nicer formatting of _YAML_CONFIG indents --- build_support/catalog/test_update.py | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/build_support/catalog/test_update.py b/build_support/catalog/test_update.py index aebe7bd0b..f4a06f686 100644 --- a/build_support/catalog/test_update.py +++ b/build_support/catalog/test_update.py @@ -21,21 +21,21 @@ } _YAML_CONFIG = """ -defaults: - color_scheme: gambit - font_family: sffamily - font_italic: true - shared_terminal_depth: true - sublevel_scaling: 0 - -overrides: - watson2013: - sublevel_scaling: 1 - selten1975: - shared_terminal_depth: false - myerson1991/fig2_1: - action_label_position: 0.4 -""" + defaults: + color_scheme: gambit + font_family: sffamily + font_italic: true + shared_terminal_depth: true + sublevel_scaling: 0 + + overrides: + watson2013: + sublevel_scaling: 1 + selten1975: + shared_terminal_depth: false + myerson1991/fig2_1: + action_label_position: 0.4 + """ def _write_yaml(path, content=_YAML_CONFIG): From 2d193e9ba042690e3fe8a894b8e8891cf040d45e Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 27 May 2026 13:58:55 +0100 Subject: [PATCH 15/38] refactor: update docstrings and comments in catalog update test suite --- build_support/catalog/test_update.py | 312 +++++++++++++++++++-------- 1 file changed, 228 insertions(+), 84 deletions(-) diff --git a/build_support/catalog/test_update.py b/build_support/catalog/test_update.py index f4a06f686..05edea358 100644 --- a/build_support/catalog/test_update.py +++ b/build_support/catalog/test_update.py @@ -1,4 +1,34 @@ -"""Tests for build_support/catalog/update.py.""" +"""Tests for build_support/catalog/update.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 +tests construct their own temporary catalog directories and DataFrame rows so they +are completely isolated from the actual catalog on disk. + +Monkeypatching strategy +----------------------- +``update.py`` depends on three external resources that are replaced in tests: + +1. ``DRAW_TREE_SETTINGS_CONFIG`` (a ``Path``) — swapped for a tmp YAML file so + ``catalog_draw_tree_settings`` reads controlled config without touching the + real ``draw_tree_settings.yaml``. ``monkeypatch.setattr(update, + "DRAW_TREE_SETTINGS_CONFIG", yaml_file)`` replaces the module-level path for + the duration of a single test and restores it automatically on teardown. + +2. ``generate_tex`` / ``generate_png`` / ``generate_pdf`` / ``generate_svg`` + (functions imported from ``draw_tree``) — replaced with no-ops or + call-tracking lambdas. This lets us test RST-generation logic without + actually invoking LaTeX, and lets us assert whether image + generation was triggered at all. + +3. ``catalog_dir`` (an argument to ``generate_rst_table`` and + ``update_makefile``) — both functions accept an optional ``catalog_dir`` + kwarg that defaults to the real ``CATALOG_DIR``. Tests pass a + ``tmp_path``-based directory instead, keeping all file I/O inside pytest's + temporary directory and avoiding any reads from or writes to the repo. +""" + +import textwrap import pytest @@ -9,9 +39,11 @@ import update # noqa: E402 # --------------------------------------------------------------------------- -# Helpers +# Module-level test fixtures # --------------------------------------------------------------------------- +# The expected dict produced by _YAML_CONFIG with no slug-specific override. +# Tests that expect defaults-only results compare against this constant. _YAML_DEFAULTS = { "color_scheme": "gambit", "font_family": "sffamily", @@ -20,30 +52,49 @@ "sublevel_scaling": 0, } -_YAML_CONFIG = """ - defaults: - color_scheme: gambit - font_family: sffamily - font_italic: true - shared_terminal_depth: true - sublevel_scaling: 0 +# A self-contained draw_tree_settings YAML config used by settings tests. +# Slugs are entirely fictional: +# "testgroup2000" – group-level prefix covering testgroup2000/* +# "othergroup1999" – group-level prefix covering othergroup1999/* +# "testgroup2000/fig2" – game-specific entry that overrides the group above +_YAML_CONFIG = textwrap.dedent("""\ + defaults: + color_scheme: gambit + font_family: sffamily + font_italic: true + shared_terminal_depth: true + sublevel_scaling: 0 + + overrides: + testgroup2000: + sublevel_scaling: 1 + othergroup1999: + shared_terminal_depth: false + testgroup2000/fig2: + action_label_position: 0.4 +""") - overrides: - watson2013: - sublevel_scaling: 1 - selten1975: - shared_terminal_depth: false - myerson1991/fig2_1: - action_label_position: 0.4 - """ + +# --------------------------------------------------------------------------- +# Helper functions +# --------------------------------------------------------------------------- def _write_yaml(path, content=_YAML_CONFIG): + """Write *content* to *path* and return *path*. + + Used to create a temporary draw_tree_settings YAML file that can be + pointed at via ``monkeypatch.setattr(update, "DRAW_TREE_SETTINGS_CONFIG", + path)`` without touching the real config file. + """ path.write_text(content, encoding="utf-8") return path -def _efg_row(slug, title="Game Title", description="A description."): +def _efg_row(slug, title="Test EFG Game", description="A description."): + """Return a dict representing one row of the DataFrame produced by + ``gbt.catalog.games(include_descriptions=True)`` for an extensive-form game. + """ return { "Game": slug, "Title": title, @@ -53,7 +104,8 @@ def _efg_row(slug, title="Game Title", description="A description."): } -def _nfg_row(slug, title="NFG Title", description="NFG description."): +def _nfg_row(slug, title="Test NFG Game", description="A description."): + """Return a dict representing one row of the DataFrame for a normal-form game.""" return { "Game": slug, "Title": title, @@ -64,109 +116,169 @@ def _nfg_row(slug, title="NFG Title", description="NFG description."): def _make_df(*rows): + """Build a DataFrame from one or more row dicts as ``generate_rst_table`` expects.""" return pd.DataFrame(list(rows)) def _make_image_files(catalog_dir, slug, fmt="efg"): - """Create the stub image files so the existence check passes.""" - img = catalog_dir / "img" / slug - img.parent.mkdir(parents=True, exist_ok=True) + """Create stub image files under *catalog_dir*/img/ for *slug*. + + ``generate_rst_table`` checks that all expected image files exist before + deciding whether to regenerate them. Touching empty files satisfies that + check without requiring real draw_tree output, so tests that are not + specifically about image generation can use this helper to set up the + pre-existing-images state. + + For EFG games the ``.ef`` intermediate file is also created, since it + appears in the existence check and the download links. + """ + img_dir = catalog_dir / "img" + img_dir.mkdir(parents=True, exist_ok=True) + slug_path = img_dir / slug + slug_path.parent.mkdir(parents=True, exist_ok=True) for ext in ["tex", "png", "pdf", "svg"]: - (catalog_dir / "img" / f"{slug}.{ext}").touch() + (img_dir / f"{slug}.{ext}").touch() if fmt == "efg": - (catalog_dir / "img" / f"{slug}.ef").touch() + (img_dir / f"{slug}.ef").touch() # --------------------------------------------------------------------------- -# catalog_draw_tree_settings +# Tests for catalog_draw_tree_settings # --------------------------------------------------------------------------- @pytest.mark.catalog_update class TestCatalogDrawTreeSettings: + """Unit tests for ``catalog_draw_tree_settings(slug) -> dict``. + + Each test writes a temporary YAML config and redirects the module-level + ``DRAW_TREE_SETTINGS_CONFIG`` path to it via ``monkeypatch.setattr``. + This means the real ``draw_tree_settings.yaml`` is never read or modified. + """ + 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, "DRAW_TREE_SETTINGS_CONFIG", yaml_file) - result = update.catalog_draw_tree_settings("unknown/game") + result = update.catalog_draw_tree_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, "DRAW_TREE_SETTINGS_CONFIG", yaml_file) - result = update.catalog_draw_tree_settings("myerson1991/fig2_1") + result = update.catalog_draw_tree_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, "DRAW_TREE_SETTINGS_CONFIG", yaml_file) - result = update.catalog_draw_tree_settings("watson2013/exercise29_6") + # "testgroup2000/fig1" is not listed explicitly; it matches the group prefix + result = update.catalog_draw_tree_settings("testgroup2000/fig1") assert result["sublevel_scaling"] == 1 def test_specific_key_wins_over_group(self, tmp_path, monkeypatch): - config = """ - defaults: - color_scheme: gambit - sublevel_scaling: 0 - overrides: - myerson1991: - sublevel_scaling: 1 - myerson1991/fig2_1: - sublevel_scaling: 2 - """ + """When both a group key and a more specific key match, the specific key wins. + + The config has ``testgroup2000`` (sets sublevel_scaling=1) and + ``testgroup2000/fig2`` (sets sublevel_scaling=2). The game + ``testgroup2000/fig2`` matches both, but the longer/specific key is + applied last, so sublevel_scaling should be 2. + """ + config = textwrap.dedent("""\ + defaults: + color_scheme: gambit + sublevel_scaling: 0 + overrides: + testgroup2000: + sublevel_scaling: 1 + testgroup2000/fig2: + sublevel_scaling: 2 + """) yaml_file = _write_yaml(tmp_path / "settings.yaml", config) monkeypatch.setattr(update, "DRAW_TREE_SETTINGS_CONFIG", yaml_file) - result = update.catalog_draw_tree_settings("myerson1991/fig2_1") + result = update.catalog_draw_tree_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, "DRAW_TREE_SETTINGS_CONFIG", yaml_file) - result = update.catalog_draw_tree_settings("selten1975/fig1") - assert result["shared_terminal_depth"] is False - result2 = update.catalog_draw_tree_settings("watson2013/fig29_1") - assert result2["shared_terminal_depth"] is True # selten override not applied + # "othergroup1999" override sets shared_terminal_depth = False + result_other = update.catalog_draw_tree_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_draw_tree_settings("testgroup2000/fig1") + assert result_test["shared_terminal_depth"] is True def test_no_overrides_section_returns_defaults(self, tmp_path, monkeypatch): - config = "defaults:\n color_scheme: gambit\n sublevel_scaling: 0\n" + """A config with no ``overrides`` key at all returns only the defaults.""" + config = textwrap.dedent("""\ + defaults: + color_scheme: gambit + sublevel_scaling: 0 + """) yaml_file = _write_yaml(tmp_path / "settings.yaml", config) monkeypatch.setattr(update, "DRAW_TREE_SETTINGS_CONFIG", yaml_file) - result = update.catalog_draw_tree_settings("any/game") + result = update.catalog_draw_tree_settings("anygame/v1") assert result == {"color_scheme": "gambit", "sublevel_scaling": 0} # --------------------------------------------------------------------------- -# generate_rst_table +# Tests for generate_rst_table # --------------------------------------------------------------------------- @pytest.mark.catalog_update class TestGenerateRstTable: + """Tests for ``generate_rst_table(df, rst_path, ...)``. + + Image generation (generate_tex / generate_png / etc.) is mocked out so + that tests can run without LaTeX installed and without reading + from the real catalog directory. + + ``_mock_generates`` uses ``monkeypatch.setattr`` to replace each of the + four draw_tree generate functions in the ``update`` module's namespace with + a no-op. Because the replacement is scoped to the test, the originals are + automatically restored afterward. + + Tests that need to verify *whether* generation was triggered replace the + functions with lambdas that append to a ``calls`` list instead. + + All catalog directories and RST output files are created inside ``tmp_path`` + (pytest's per-test temporary directory) so nothing is written to the repo. + """ + def _no_op_generate(self, *args, **kwargs): - """Replacement for draw_tree generate_* functions that does nothing.""" + """Stand-in for draw_tree generate_* functions; does nothing.""" def _mock_generates(self, monkeypatch): + """Replace all four draw_tree image-generation functions with no-ops.""" for name in ["generate_tex", "generate_png", "generate_pdf", "generate_svg"]: monkeypatch.setattr(update, 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.""" self._mock_generates(monkeypatch) catalog_dir = tmp_path / "catalog" - slug = "bagwell1995" + slug = "fakeauthor2000/fig1" _make_image_files(catalog_dir, slug, "efg") - df = _make_df(_efg_row(slug, title="Bagwell 1995")) + 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) rst = rst_path.read_text() - assert "Bagwell 1995" in rst + assert "Fake Author (2000) Figure 1" in rst assert f'pygambit.catalog.load("{slug}")' in rst - assert f":download:`{slug}.efg" in rst - assert f":download:`{slug}.ef" in rst + assert f":download:`{slug}.efg" in rst # source game file download link + assert f":download:`{slug}.ef" in rst # draw_tree intermediate file download link def test_nfg_row_produces_rst_with_save_to(self, tmp_path, monkeypatch): + """An NFG game row uses the ``save_to`` form of the draw_tree call (no .ef involved).""" self._mock_generates(monkeypatch) catalog_dir = tmp_path / "catalog" - slug = "nau2004/sec3" + slug = "fakeauthor2001/matrix1" _make_image_files(catalog_dir, slug, "nfg") df = _make_df(_nfg_row(slug)) rst_path = tmp_path / "out.rst" @@ -175,38 +287,42 @@ def test_nfg_row_produces_rst_with_save_to(self, tmp_path, monkeypatch): assert f'save_to="../catalog/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.""" self._mock_generates(monkeypatch) catalog_dir = tmp_path / "catalog" catalog_dir.mkdir() row = { - "Game": "bogus/game", - "Title": "Bogus", + "Game": "fakegame/v1", + "Title": "Fake Game", "Description": "Has a description.", "Download": "", - "Format": "efg_2", + "Format": "efg_2", # not in SUPPORTED_GAME_FORMATS } df = _make_df(row) rst_path = tmp_path / "out.rst" update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) rst = rst_path.read_text() - assert "Bogus" not in rst + assert "Fake Game" not in rst def test_row_without_description_is_skipped(self, tmp_path, monkeypatch): + """A game with an empty description is not included in the RST output.""" self._mock_generates(monkeypatch) catalog_dir = tmp_path / "catalog" catalog_dir.mkdir() - df = _make_df(_efg_row("bagwell1995", description="")) + 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) rst = rst_path.read_text() - assert "bagwell1995" not in rst + assert "fakeauthor2000/fig1" not in rst def test_curated_ef_used_in_draw_tree_call(self, tmp_path, monkeypatch): + """When a curated .ef file exists alongside the .efg, the RST draw_tree call + references the .ef path directly rather than ``pygambit.catalog.load``.""" self._mock_generates(monkeypatch) catalog_dir = tmp_path / "catalog" - slug = "selten1975/fig1" + slug = "fakeauthor1999/fig1" _make_image_files(catalog_dir, slug, "efg") - # Place a curated .ef alongside the .efg + # Place a curated .ef file alongside the game — this is what update.py checks for curated = catalog_dir / f"{slug}.ef" curated.parent.mkdir(parents=True, exist_ok=True) curated.touch() @@ -214,36 +330,47 @@ def test_curated_ef_used_in_draw_tree_call(self, tmp_path, monkeypatch): rst_path = tmp_path / "out.rst" update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) rst = rst_path.read_text() - # The draw_tree() call line should reference the .ef file path directly + # Find the draw_tree( call line in the jupyter-execute block draw_tree_call = next(line for line in rst.splitlines() if "draw_tree(" in line) assert f'"../catalog/{slug}.ef"' in draw_tree_call assert "catalog.load" not in draw_tree_call def test_images_not_regenerated_when_all_exist(self, tmp_path, monkeypatch): + """If all expected image files are already present and ``regenerate_images`` is + False, none of the draw_tree generate functions are called.""" calls = [] + # Replace generate_* with lambdas that record invocations monkeypatch.setattr(update, "generate_tex", lambda *a, **k: calls.append("tex")) monkeypatch.setattr(update, "generate_png", lambda *a, **k: calls.append("png")) monkeypatch.setattr(update, "generate_pdf", lambda *a, **k: calls.append("pdf")) monkeypatch.setattr(update, "generate_svg", lambda *a, **k: calls.append("svg")) catalog_dir = tmp_path / "catalog" - slug = "bagwell1995" - _make_image_files(catalog_dir, slug, "efg") + 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) 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 + as the draw_tree source rather than calling ``gbt.catalog.load``, which + would require the real catalog to be present. + """ calls = [] monkeypatch.setattr(update, "generate_tex", lambda *a, **k: calls.append("tex")) monkeypatch.setattr(update, "generate_png", lambda *a, **k: calls.append("png")) monkeypatch.setattr(update, "generate_pdf", lambda *a, **k: calls.append("pdf")) monkeypatch.setattr(update, "generate_svg", lambda *a, **k: calls.append("svg")) catalog_dir = tmp_path / "catalog" - slug = "bagwell1995" + slug = "fakeauthor2000/fig1" _make_image_files(catalog_dir, slug, "efg") - # Use a curated .ef so gbt.catalog.load is not called (avoids needing real catalog) + # Place a curated .ef file alongside the game — this is what update.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" @@ -252,58 +379,75 @@ def test_images_regenerated_when_flag_set(self, tmp_path, monkeypatch): # --------------------------------------------------------------------------- -# update_makefile +# Tests for update_makefile # --------------------------------------------------------------------------- @pytest.mark.catalog_update class TestUpdateMakefile: + """Tests for ``update_makefile(catalog_dir, am_path)``. + + Both arguments are injected via ``tmp_path`` so the real catalog directory + and the real ``catalog.am`` file are never read or written. + """ + def test_efg_and_nfg_files_included(self, tmp_path): - (tmp_path / "foo.efg").touch() - (tmp_path / "sub").mkdir() - (tmp_path / "sub" / "bar.nfg").touch() + """Game files with .efg and .nfg extensions appear in the generated catalog.am.""" + (tmp_path / "standalone.efg").touch() + (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) content = am.read_text() - assert "catalog/foo.efg" in content - assert "catalog/sub/bar.nfg" in content + assert "catalog/standalone.efg" in content + assert "catalog/subfolder/matrix.nfg" in content def test_curated_ef_included(self, tmp_path): - (tmp_path / "myerson1991").mkdir() - (tmp_path / "myerson1991" / "fig1.efg").touch() - (tmp_path / "myerson1991" / "fig1.ef").touch() + """A curated .ef file committed alongside a game file appears in catalog.am.""" + (tmp_path / "fakegame").mkdir() + (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) content = am.read_text() - assert "catalog/myerson1991/fig1.ef" in content + assert "catalog/fakegame/fig1.ef" in content def test_ef_in_img_dir_excluded(self, tmp_path): - img = tmp_path / "img" / "selten1975" + """Generated .ef files under the img/ subdirectory are excluded from catalog.am.""" + img = tmp_path / "img" / "fakegame" img.mkdir(parents=True) - (img / "fig1.ef").touch() + (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) content = am.read_text() assert "img" not in content def test_non_game_file_excluded(self, tmp_path): - (tmp_path / "foo.efg_2").touch() - (tmp_path / "readme.txt").touch() + """Files with non-game extensions (e.g. .efg_2, .txt) are not included.""" + (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) content = am.read_text() assert "efg_2" not in content - assert "readme" not in content + assert "README" not in content def test_no_write_when_content_unchanged(self, tmp_path): - (tmp_path / "foo.efg").touch() + """If catalog.am already contains the correct content, it is not rewritten. + + The mtime of the file is captured after the first write and compared + after the second call. If the file were overwritten, the mtime would + change; if the content-equality check works correctly, it stays the same. + """ + (tmp_path / "standalone.efg").touch() am = tmp_path / "catalog.am" update.update_makefile(catalog_dir=tmp_path, am_path=am) - mtime_after_first = am.stat().st_mtime + mtime_after_first_write = am.stat().st_mtime update.update_makefile(catalog_dir=tmp_path, am_path=am) - assert am.stat().st_mtime == mtime_after_first + 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) content = am.read_text() From eaaf8b5a27a7921ad928b64ecefc3479bd8b4b3e Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 27 May 2026 14:17:53 +0100 Subject: [PATCH 16/38] fix: use as_posix() for cross-platform slug path consistency in catalog updates --- build_support/catalog/update.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index a6f9991bd..b6db67758 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -157,17 +157,17 @@ def update_makefile( for resource_path in sorted(catalog_dir.rglob("*.efg")): if resource_path.is_file(): rel_path = resource_path.relative_to(catalog_dir) - slugs.append(str(rel_path)) + slugs.append(rel_path.as_posix()) for resource_path in sorted(catalog_dir.rglob("*.nfg")): if resource_path.is_file(): rel_path = resource_path.relative_to(catalog_dir) - slugs.append(str(rel_path)) + slugs.append(rel_path.as_posix()) for resource_path in sorted(catalog_dir.rglob("*.ef")): # Exclude the generated .ef files under catalog/img/; only curated # .ef files committed alongside game files should be distributed. if resource_path.is_file() and catalog_dir / "img" not in resource_path.parents: rel_path = resource_path.relative_to(catalog_dir) - slugs.append(str(rel_path)) + slugs.append(rel_path.as_posix()) game_files = [] for slug in slugs: From ebc8727b4ee6ce6ce137cce883955de35331b2a1 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 27 May 2026 14:54:15 +0100 Subject: [PATCH 17/38] chore: update draw-tree dependency to version 0.9.1 in CI and ReadTheDocs configurations --- .github/workflows/python.yml | 8 ++++---- .readthedocs.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index f380bff25..a31e9152f 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -30,7 +30,7 @@ jobs: cd dist sdist=$(ls pygambit-*.tar.gz) pip install -v "${sdist}[test,doc]" - pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.9.0" + pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.9.1" - name: Run tests run: pytest --run-tutorials @@ -53,7 +53,7 @@ jobs: - name: Build extension run: | python -m pip install -v .[test,doc] - pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.9.0" + pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.9.1" - name: Run tests run: pytest --run-tutorials @@ -76,7 +76,7 @@ jobs: - name: Build extension run: | python -m pip install -v .[test,doc] - pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.9.0" + pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.9.1" - name: Run tests run: pytest --run-tutorials @@ -99,6 +99,6 @@ jobs: - name: Build extension run: | python -m pip install -v .[test,doc] - pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.9.0" + pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.9.1" - name: Run tests run: pytest --run-tutorials diff --git a/.readthedocs.yml b/.readthedocs.yml index 16e06bb4c..d06f69e2b 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -17,7 +17,7 @@ build: - pdf2svg jobs: post_install: - - pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.9.0" + - pip install "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.9.1" # Create RST for catalog table in docs - $READTHEDOCS_VIRTUALENV_PATH/bin/python build_support/catalog/update.py From dc9eb622935e7c842a23ebcbd51a62398a0e0b13 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 28 May 2026 11:02:47 +0100 Subject: [PATCH 18/38] feat: enable support for multiple game layout variants in catalog via .ef file suffixing --- build_support/catalog/test_update.py | 167 +++++++++++++++++++++++++++ build_support/catalog/update.py | 151 +++++++++++++++++++----- doc/developer.catalog.rst | 20 +++- 3 files changed, 302 insertions(+), 36 deletions(-) diff --git a/build_support/catalog/test_update.py b/build_support/catalog/test_update.py index 05edea358..978e1deaf 100644 --- a/build_support/catalog/test_update.py +++ b/build_support/catalog/test_update.py @@ -226,6 +226,94 @@ def test_no_overrides_section_returns_defaults(self, tmp_path, monkeypatch): assert result == {"color_scheme": "gambit", "sublevel_scaling": 0} +# --------------------------------------------------------------------------- +# Tests for catalog_ef_file_variants +# --------------------------------------------------------------------------- + + +@pytest.mark.catalog_update +class TestCatalogEfFileVariants: + """Tests for ``catalog_ef_file_variants(slug, catalog_dir) -> list[dict] | None``. + + The function scans the directory for the game slug inside ``catalog_dir`` + for ``.ef`` files matching the naming convention:: + + {stem}.ef primary variant + {stem}__{suffix}.ef additional variant + + All slugs use the fictional prefix ``fakevariant2000`` to make it clear + these tests do not depend on the real catalog contents. + """ + + def _game_dir(self, catalog_dir, slug): + """Create and return the directory that would contain the game's files.""" + game_dir = (catalog_dir / slug).parent + game_dir.mkdir(parents=True, exist_ok=True) + return game_dir + + def test_no_ef_files_returns_none(self, tmp_path): + """No .ef files in the game directory → returns None (no tab-set needed).""" + 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 + + def test_single_ef_file_returns_none(self, tmp_path): + """A single curated .ef file → returns None (single image, no tabs needed).""" + catalog_dir = tmp_path / "catalog" + 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 + + 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.""" + catalog_dir = tmp_path / "catalog" + slug = "fakevariant2000/fig1" + 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) + assert result is not None + assert len(result) == 2 + assert {v["label"] for v in result} == {"Default", "Wide"} + assert {v["variant_key"] for v in result} == {slug, f"{slug}__wide"} + + def test_label_derived_from_filename_suffix(self, tmp_path): + """The tab label is the suffix after ``__``, title-cased.""" + catalog_dir = tmp_path / "catalog" + slug = "fakevariant2000/fig1" + 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) + assert {v["label"] for v in result} == {"Default", "Compact"} + + def test_multi_word_suffix_title_cased(self, tmp_path): + """Underscores in the suffix become spaces: ``fig1__very_wide.ef`` → "Very Wide".""" + catalog_dir = tmp_path / "catalog" + slug = "fakevariant2000/fig1" + 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) + assert "Very Wide" in {v["label"] for v in result} + + def test_file_without_double_underscore_excluded(self, tmp_path): + """A file whose stem is ``{stem}extra`` (no ``__``) is not treated as a variant. + + Only files matching exactly ``{stem}.ef`` or ``{stem}__*.ef`` are counted. + If the non-conforming file is the only candidate alongside the base, the + function still returns None (only one conforming file found). + """ + catalog_dir = tmp_path / "catalog" + slug = "fakevariant2000/fig1" + 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 + + # --------------------------------------------------------------------------- # Tests for generate_rst_table # --------------------------------------------------------------------------- @@ -377,6 +465,85 @@ def test_images_regenerated_when_flag_set(self, tmp_path, monkeypatch): update.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): + """Two curated .ef files alongside a game trigger a ``tab-set`` in the RST. + + The RST should contain ``.. tab-set::`` and one ``.. tab-item::`` per + variant, with labels derived from the filename suffixes. + """ + self._mock_generates(monkeypatch) + catalog_dir = tmp_path / "catalog" + slug = "fakevariant2001/fig1" + game_dir = catalog_dir / "fakevariant2001" + game_dir.mkdir(parents=True) + (game_dir / "fig1.ef").touch() + (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) + rst = rst_path.read_text() + assert ".. tab-set::" in rst + assert ".. tab-item:: Default" in rst + assert ".. tab-item:: Wide" 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``.""" + self._mock_generates(monkeypatch) + catalog_dir = tmp_path / "catalog" + slug = "fakevariant2001/fig1" + _make_image_files(catalog_dir, slug, "efg") + game_dir = catalog_dir / "fakevariant2001" + game_dir.mkdir(parents=True, exist_ok=True) + (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) + rst = rst_path.read_text() + assert ".. tab-set::" not in rst + + def test_per_variant_images_generated(self, tmp_path, monkeypatch): + """When multiple .ef variants exist, image generation is called once per variant. + + Two variants × four generate functions = eight total calls. + """ + calls = [] + monkeypatch.setattr(update, "generate_tex", lambda *a, **k: calls.append("tex")) + monkeypatch.setattr(update, "generate_png", lambda *a, **k: calls.append("png")) + monkeypatch.setattr(update, "generate_pdf", lambda *a, **k: calls.append("pdf")) + monkeypatch.setattr(update, "generate_svg", lambda *a, **k: calls.append("svg")) + catalog_dir = tmp_path / "catalog" + slug = "fakevariant2001/fig1" + game_dir = catalog_dir / "fakevariant2001" + game_dir.mkdir(parents=True) + (game_dir / "fig1.ef").touch() + (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) + 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, "generate_tex", lambda *a, **k: calls.append("tex")) + monkeypatch.setattr(update, "generate_png", lambda *a, **k: calls.append("png")) + monkeypatch.setattr(update, "generate_pdf", lambda *a, **k: calls.append("pdf")) + monkeypatch.setattr(update, "generate_svg", lambda *a, **k: calls.append("svg")) + catalog_dir = tmp_path / "catalog" + slug = "fakevariant2001/fig1" + game_dir = catalog_dir / "fakevariant2001" + game_dir.mkdir(parents=True) + (game_dir / "fig1.ef").touch() + (game_dir / "fig1__wide.ef").touch() + # Pre-create all image files for both variants so nothing needs regenerating + for vkey in [slug, f"{slug}__wide"]: + _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) + assert calls == [] + # --------------------------------------------------------------------------- # Tests for update_makefile diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index b6db67758..8c1f23b91 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -29,6 +29,39 @@ def catalog_draw_tree_settings(slug: str) -> dict: return settings +def catalog_ef_file_variants(slug: str, catalog_dir: Path) -> list[dict] | None: + """Scan catalog_dir for multiple curated .ef files for a game slug. + + Returns a list of dicts with keys ``label``, ``ef_path``, and + ``variant_key`` when more than one .ef file is found alongside the game; + returns None when zero or one .ef file exists (no tabs needed). + + File-naming convention:: + + catalog/{slug}.ef primary variant → label "Default" + catalog/{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". + """ + game_dir = (catalog_dir / slug).parent + stem = Path(slug).name + + ef_files = sorted( + p for p in game_dir.glob(f"{stem}*.ef") if p.stem == stem or p.stem.startswith(stem + "__") + ) + if len(ef_files) <= 1: + return None + + variants = [] + for ef_file in ef_files: + suffix = "" if ef_file.stem == stem else ef_file.stem[len(stem) + 2:] + label = "Default" if not suffix else suffix.replace("_", " ").title() + variant_key = slug if not suffix else f"{slug}__{suffix}" + variants.append({"label": label, "ef_path": ef_file, "variant_key": variant_key}) + return variants + + def generate_rst_table( df: pd.DataFrame, rst_path: Path, @@ -58,25 +91,49 @@ def generate_rst_table( if row["Format"] not in SUPPORTED_GAME_FORMATS: continue # Skip any games which lack a description - if description: - # Build the list of expected image files so we can check whether - # any are missing and need to be generated. + if not description: + continue + + # Detect whether this EFG has multiple curated .ef layout variants. + ef_variants = ( + catalog_ef_file_variants(slug, catalog_dir) if row["Format"] == "efg" else None + ) + + if ef_variants: + # Multi-variant: generate one image set per variant + _variant_img_exts = ["ef", "tex", "png", "pdf", "svg"] + for variant in ef_variants: + vkey = variant["variant_key"] + variant_paths = [ + catalog_dir / "img" / f"{vkey}.{ext}" for ext in _variant_img_exts + ] + if regenerate_images or not all(p.exists() for p in variant_paths): + viz_path = catalog_dir / "img" / vkey + viz_path.parent.mkdir(parents=True, exist_ok=True) + source = str(variant["ef_path"]) + for func in [generate_tex, generate_png, generate_pdf, generate_svg]: + func( + source, + save_to=str(viz_path), + **catalog_draw_tree_settings(vkey), + ) + img_ef = catalog_dir / "img" / f"{vkey}.ef" + if not img_ef.exists(): + shutil.copy2(variant["ef_path"], img_ef) + else: + # Single variant all_exts = [] all_paths = [] if row["Format"] == "efg": - ef_path = catalog_dir / "img" / f"{slug}.ef" all_exts.append("ef") - all_paths.append(ef_path) - all_exts = all_exts + ["tex", "png", "pdf", "svg"] - tex_path = catalog_dir / "img" / f"{slug}.tex" - all_paths.append(tex_path) - all_paths.append(catalog_dir / "img" / f"{slug}.png") - all_paths.append(catalog_dir / "img" / f"{slug}.pdf") - all_paths.append(catalog_dir / "img" / f"{slug}.svg") + all_paths.append(catalog_dir / "img" / f"{slug}.ef") + all_exts += ["tex", "png", "pdf", "svg"] + for ext in ["tex", "png", "pdf", "svg"]: + all_paths.append(catalog_dir / "img" / f"{slug}.{ext}") missing_any = not all(p.exists() for p in all_paths) if regenerate_images or missing_any: - viz_path = catalog_dir / "img" / f"{slug}" + viz_path = catalog_dir / "img" / slug viz_path.parent.mkdir(parents=True, exist_ok=True) # Use a committed curated .ef file if present; otherwise derive # the layout automatically from the game object. @@ -90,33 +147,63 @@ def generate_rst_table( if not img_ef.exists() and curated_ef.exists(): shutil.copy2(curated_ef, img_ef) - # Main dropdown - f.write(f" * - .. dropdown:: {title}\n") - f.write(" :open:\n") - f.write(" \n") - for line in description.splitlines(): - f.write(f" {line}\n") + # Main dropdown + f.write(f" * - .. dropdown:: {title}\n") + f.write(" :open:\n") + f.write(" \n") + for line in description.splitlines(): + f.write(f" {line}\n") - f.write(" \n") - f.write(" **Load in PyGambit:**\n") - f.write(" \n") - f.write(" .. code-block:: python\n") - f.write(" \n") - f.write(f' pygambit.catalog.load("{slug}")\n') - f.write(" \n") + f.write(" \n") + f.write(" **Load in PyGambit:**\n") + f.write(" \n") + f.write(" .. code-block:: python\n") + f.write(" \n") + f.write(f' pygambit.catalog.load("{slug}")\n') + f.write(" \n") - # Download links - download_links = [row["Download"]] + # Download links + download_links = [row["Download"]] + if ef_variants: + _variant_img_exts = ["ef", "tex", "png", "pdf", "svg"] + for variant in ef_variants: + vkey = variant["variant_key"] + for ext in _variant_img_exts: + download_links.append( + f":download:`{vkey}.{ext} <../catalog/img/{vkey}.{ext}>`" + ) + else: for ext in all_exts: download_links.append( f":download:`{slug}.{ext} <../catalog/img/{slug}.{ext}>`" ) - f.write(" **Download game and image files:**\n") + f.write(" **Download game and image files:**\n") + f.write(" \n") + f.write(f" {' '.join(download_links)}\n") + f.write(" \n") + + # Draw image — tab-set for multiple variants, single block otherwise + if ef_variants: + f.write(" .. tab-set::\n") f.write(" \n") - f.write(f" {' '.join(download_links)}\n") + for variant in ef_variants: + label = variant["label"] + vkey = variant["variant_key"] + settings = catalog_draw_tree_settings(vkey) + settings_str = ", ".join(f"{k}={val!r}" for k, val in settings.items()) + f.write(f" .. tab-item:: {label}\n") + f.write(" \n") + f.write(" .. jupyter-execute::\n") + f.write(" :hide-code:\n") + f.write(" \n") + f.write(" import pygambit\n") + f.write(" from draw_tree import draw_tree\n") + f.write( + f' draw_tree("../catalog/{vkey}.ef", {settings_str})\n' + ) + f.write(" \n") f.write(" \n") - - # Draw image + else: f.write(" .. jupyter-execute::\n") f.write(" :hide-code:\n") f.write(" \n") @@ -124,7 +211,7 @@ def generate_rst_table( f.write(" from draw_tree import draw_tree\n") if row["Format"] == "efg": settings = catalog_draw_tree_settings(slug) - settings_str = ", ".join(f"{k}={v!r}" for k, v in settings.items()) + settings_str = ", ".join(f"{k}={val!r}" for k, val in settings.items()) curated_ef = catalog_dir / f"{slug}.ef" if curated_ef.exists(): f.write( diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index deacfb6c3..eb9dcfa5c 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -39,6 +39,14 @@ Currently supported representations are: Add your new game file(s) inside the ``catalog`` dir and commit them, or edit an existing game. If there are multiple games from a particular source, place them in an appropriately named folder. + .. important:: + + The name of the game file will determine it's "slug", used by the load function of the catalog module: + + .. code-block:: python + + pygambit.catalog.load("watson2013/exercise29_6") + .. note:: For extensive form games, you may optionally commit a curated ``.ef`` file alongside the ``.efg`` @@ -47,13 +55,17 @@ Currently supported representations are: auto-generating the layout from the ``.efg``, preserving any hand-tuned layout. Consult the `DrawTree docs `_ for the ``.ef`` format. - .. important:: + **Layout variants:** To display multiple layout variants on the catalog page (rendered as + clickable tabs), commit additional ``.ef`` files using the ``{slug}__{label}.ef`` naming + convention (double underscore separator), e.g.: - The name of the game file will determine it's "slug", used by the load function of the catalog module: + .. code-block:: text - .. code-block:: python + catalog/example/game.ef # primary (tab label "Default") + catalog/example/game__wide.ef # additional (tab label "Wide") - pygambit.catalog.load("watson2013/exercise29_6") + 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. ``__very_wide`` → "Very Wide"). 3. **Update the build files:** From aa56574fb32be56797c9263d63ecb2cd62427155 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 28 May 2026 11:21:30 +0100 Subject: [PATCH 19/38] feat: re-add new vonstengel2022 game files and custom draw tree settings to the catalog --- build_support/catalog/catalog.am | 7 +++++++ build_support/catalog/draw_tree_settings.yaml | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/build_support/catalog/catalog.am b/build_support/catalog/catalog.am index 980afb135..2a9095c9f 100644 --- a/build_support/catalog/catalog.am +++ b/build_support/catalog/catalog.am @@ -15,5 +15,12 @@ CATALOG_FILES = \ catalog/selten1975/fig1.efg \ catalog/selten1975/fig2.efg \ catalog/selten1975/fig3.efg \ + catalog/vonstengel2022/fig10.1.efg \ + catalog/vonstengel2022/fig10.12.efg \ + catalog/vonstengel2022/fig10.5.efg \ + catalog/vonstengel2022/fig10.7.efg \ + catalog/vonstengelforges2008/fig1.efg \ + catalog/vonstengelforges2008/fig6.efg \ + catalog/vonstengelforges2008/fig9.efg \ catalog/watson2013/exercise29_6.efg \ catalog/watson2013/fig29_1.efg diff --git a/build_support/catalog/draw_tree_settings.yaml b/build_support/catalog/draw_tree_settings.yaml index d969f4d11..00b42675a 100644 --- a/build_support/catalog/draw_tree_settings.yaml +++ b/build_support/catalog/draw_tree_settings.yaml @@ -22,3 +22,10 @@ overrides: action_label_position: 0.4 reiley2008/fig1: action_label_position: 0.4 + vonstengel2022/fig10.1: + sublevel_scaling: 0.75 + shared_terminal_depth: false + vonstengelforges2008/fig1: + sublevel_scaling: 1 + vonstengelforges2008/fig9: + sublevel_scaling: 0.5 From a1bc4530c2fe69d5865ae8a3c746de53a4c9a738 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 29 May 2026 10:26:22 +0100 Subject: [PATCH 20/38] refactor: migrate bibliography and catalog references to Sphinx :cite:p: citation format --- catalog/bagwell1995.efg | 2 +- catalog/gilboa1997/fig2.efg | 2 +- catalog/jakobsen2016/fig1a.efg | 2 +- catalog/jakobsen2016/fig1b.efg | 2 +- catalog/jakobsen2016/fig1c.efg | 2 +- catalog/jakobsen2016/fig3.efg | 2 +- catalog/myerson1991/fig2_1.efg | 4 +- catalog/myerson1991/fig4_2.efg | 2 +- catalog/nau2004/sec3.nfg | 2 +- catalog/nau2004/sec4.nfg | 2 +- catalog/nau2004/sec5.nfg | 2 +- catalog/nau2004/sec6.nfg | 2 +- catalog/reiley2008/fig1.efg | 4 +- catalog/selten1975/fig1.efg | 2 +- catalog/selten1975/fig2.efg | 2 +- catalog/selten1975/fig3.efg | 2 +- catalog/vonstengel2022/fig10.1.efg | 4 +- catalog/vonstengel2022/fig10.12.efg | 2 +- catalog/vonstengel2022/fig10.5.efg | 2 +- catalog/vonstengel2022/fig10.7.efg | 2 +- catalog/vonstengelforges2008/fig1.efg | 2 +- catalog/vonstengelforges2008/fig6.efg | 2 +- catalog/vonstengelforges2008/fig9.efg | 2 +- catalog/watson2013/exercise29_6.efg | 2 +- catalog/watson2013/fig29_1.efg | 2 +- doc/algorithms.rst | 26 +- doc/biblio.rst | 167 +------- doc/conf.py | 5 +- doc/developer.catalog.rst | 3 +- doc/gui.nash.rst | 6 +- doc/references.bib | 392 ++++++++++++++++++ doc/tools.enummixed.rst | 2 +- doc/tools.enumpoly.rst | 2 +- doc/tools.enumpure.rst | 2 +- doc/tools.gnm.rst | 2 +- doc/tools.ipa.rst | 2 +- doc/tools.lcp.rst | 2 +- doc/tools.liap.rst | 2 +- doc/tools.logit.rst | 2 +- doc/tools.rst | 2 +- doc/tools.simpdiv.rst | 2 +- .../pygambit.external_programs.rst | 6 +- pyproject.toml | 1 + src/pygambit/behavmixed.pxi | 4 +- src/pygambit/stratmixed.pxi | 2 +- 45 files changed, 469 insertions(+), 219 deletions(-) create mode 100644 doc/references.bib diff --git a/catalog/bagwell1995.efg b/catalog/bagwell1995.efg index 3783d1649..84a6fba78 100644 --- a/catalog/bagwell1995.efg +++ b/catalog/bagwell1995.efg @@ -1,6 +1,6 @@ EFG 2 R "Bagwell (GEB 1995) commitment and (un)observability" { "Player 1" "Player 2" } "This is a Stackelberg-type game with imperfectly observed commitment, following the -analysis of `Bag1995 `_. The outcomes and payoffs are the same as in Bagwell's +analysis of :cite:p:`Bag1995`. The outcomes and payoffs are the same as in Bagwell's model. This example sets the probability that the follower 'correctly' observes the leader's action as .99 (99/100). The key result is that the only pure-strategy equilibrium that survives if observability is imperfect is the one in which players diff --git a/catalog/gilboa1997/fig2.efg b/catalog/gilboa1997/fig2.efg index cf686fac8..3fd89cd14 100644 --- a/catalog/gilboa1997/fig2.efg +++ b/catalog/gilboa1997/fig2.efg @@ -1,6 +1,6 @@ EFG 2 R "Gilboa (1997) Two-Selves Absent-Minded Driver" { "Player 1" } "A reformulation of the absent-minded driver problem from -`Gil97 `_ +:cite:p:`Gil97` using a multi-self approach. A chance move determines the order in which two selves act, each facing a binary choice. Neither self knows the order of play, capturing absent-mindedness through information sets that cross the chance branches rather than through imperfect recall. diff --git a/catalog/jakobsen2016/fig1a.efg b/catalog/jakobsen2016/fig1a.efg index a509ab4a7..00199b518 100644 --- a/catalog/jakobsen2016/fig1a.efg +++ b/catalog/jakobsen2016/fig1a.efg @@ -1,5 +1,5 @@ EFG 2 R "Jakobsen, Sorensen, Conitzer (2016) Figure 1(a)" { "Player 1" "Player 2" } -"An example from `JakSorCon16 `_ illustrating a game +"An example from :cite:p:`JakSorCon16` illustrating a game that is not exactly timeable. A coin toss determines which player moves first. Each player guesses whether she went first, without distinguishing going first from going second. Each player receives a payoff of 1 for a correct guess and 0 otherwise. diff --git a/catalog/jakobsen2016/fig1b.efg b/catalog/jakobsen2016/fig1b.efg index c8ff21ab7..0232d5834 100644 --- a/catalog/jakobsen2016/fig1b.efg +++ b/catalog/jakobsen2016/fig1b.efg @@ -1,5 +1,5 @@ EFG 2 R "Jakobsen, Sorensen, Conitzer (2016) Figure 1(b)" { "Player 1" "Player 2" } -"An example from `JakSorCon16 `_ illustrating a game +"An example from :cite:p:`JakSorCon16` illustrating a game that has an exact deterministic timing. A coin toss determines the flow of the game; player 1 only plays if the coin comes up Heads, and if so plays first. Player 2 always plays, but cannot distinguish whether the coin came up Heads or Tails. diff --git a/catalog/jakobsen2016/fig1c.efg b/catalog/jakobsen2016/fig1c.efg index f72bf9284..51bbc6c11 100644 --- a/catalog/jakobsen2016/fig1c.efg +++ b/catalog/jakobsen2016/fig1c.efg @@ -1,5 +1,5 @@ EFG 2 R "Jakobsen, Sorensen, Conitzer (2016) Figure 1(c)" { "Player 1" "Player 2" } -"An example from `JakSorCon16 `_ illustrating a game +"An example from :cite:p:`JakSorCon16` illustrating a game that is not exactly timeable. A coin toss determines the order of players. The player moving second is only offered a bet if the player moving first guessed correctly. Each player receives a payoff of 1 for a correct guess and 0 otherwise. diff --git a/catalog/jakobsen2016/fig3.efg b/catalog/jakobsen2016/fig3.efg index 1429f7481..430f25559 100644 --- a/catalog/jakobsen2016/fig3.efg +++ b/catalog/jakobsen2016/fig3.efg @@ -1,5 +1,5 @@ EFG 2 R "Jakobsen, Sorensen, Conitzer (2016) Figure 3" { "Player 1" "Player 2" "Player 3" "Player 4" } -"An example from `JakSorCon16 `_ illustrating +"An example from :cite:p:`JakSorCon16` illustrating the extensive form of an onion routing game that is not exactly timeable. Chance chooses a sender by drawing a signal from {0, 1, 2, 3} with equal probability. The sender does not make a strategic choice; only the two intermediary players act. diff --git a/catalog/myerson1991/fig2_1.efg b/catalog/myerson1991/fig2_1.efg index 3c45dedee..a42b9eff9 100644 --- a/catalog/myerson1991/fig2_1.efg +++ b/catalog/myerson1991/fig2_1.efg @@ -1,12 +1,12 @@ EFG 2 R "A simple Poker game" { "Fred" "Alice" } -"This is a simple game of one-card poker from `Mye91 `_, used as the +"This is a simple game of one-card poker from :cite:p:`Mye91`, used as the introductory example for game models. Note that as specified in the text, the game has the slightly unusual feature that folding with the high (red) card results in the player winning rather than losing. -See also `Rei2008 `_ +See also :cite:p:`Rei2008` Another one-card poker game where folding with the high card is a loss rather than a win. " diff --git a/catalog/myerson1991/fig4_2.efg b/catalog/myerson1991/fig4_2.efg index d7b503bea..ff391a1a5 100644 --- a/catalog/myerson1991/fig4_2.efg +++ b/catalog/myerson1991/fig4_2.efg @@ -1,5 +1,5 @@ EFG 2 R "Myerson (1991) Figure 4.2" { "Player 1" "Player 2" } -"An example from `Mye91 `_ which illustrates the distinction between +"An example from :cite:p:`Mye91` which illustrates the distinction between an equilibrium of an extensive form game and an equilibrium of its (multi)agent representation. The actions B1, Z1, and W2 form a behavior profile which is an equilibrium in the (multi)agent diff --git a/catalog/nau2004/sec3.nfg b/catalog/nau2004/sec3.nfg index bc203d6b2..812ca8fad 100644 --- a/catalog/nau2004/sec3.nfg +++ b/catalog/nau2004/sec3.nfg @@ -3,7 +3,7 @@ NFG 1 R "Battle of the Sexes" { "Player 1" "Player 2" } { { "Top" "Bottom" } { "Left" "Right" } } -"The coordination game known as Battle of the Sexes (section 3 of `Nau2004 `_). Has three Nash equilibria: two pure-strategy (TL and BR) and one completely mixed." +"The coordination game known as Battle of the Sexes (section 3 of :cite:p:`Nau2004`). Has three Nash equilibria: two pure-strategy (TL and BR) and one completely mixed." { { "" 3, 2 } diff --git a/catalog/nau2004/sec4.nfg b/catalog/nau2004/sec4.nfg index 57b754b6f..0a3ae1b87 100644 --- a/catalog/nau2004/sec4.nfg +++ b/catalog/nau2004/sec4.nfg @@ -4,7 +4,7 @@ NFG 1 R "Three-player game with a unique Nash solution in irrational strategies" { "Left" "Right" } { "1" "2" } } -"A three-player game with a unique Nash equilibrium in irrational mixed strategies (section 4 of `Nau2004 `_). The correlated equilibrium polytope is seven-dimensional with 33 vertices." +"A three-player game with a unique Nash equilibrium in irrational mixed strategies (section 4 of :cite:p:`Nau2004`). The correlated equilibrium polytope is seven-dimensional with 33 vertices." { { "" 3, 0, 2 } diff --git a/catalog/nau2004/sec5.nfg b/catalog/nau2004/sec5.nfg index fa9f40d53..dadb0507e 100644 --- a/catalog/nau2004/sec5.nfg +++ b/catalog/nau2004/sec5.nfg @@ -4,7 +4,7 @@ NFG 1 R "Game with a continuum of completely mixed-strategy Nash equilibria" { " { "Left" "Right" } { "1" "2" } } -"A three-player 2x2x2 game with 3 pure, 2 incompletely mixed, and a continuum of completely mixed Nash equilibria (section 5 of `Nau2004 `_). The correlated equilibrium polytope is seven-dimensional with 8 vertices." +"A three-player 2x2x2 game with 3 pure, 2 incompletely mixed, and a continuum of completely mixed Nash equilibria (section 5 of :cite:p:`Nau2004`). The correlated equilibrium polytope is seven-dimensional with 8 vertices." { { "" 0, 0, 2 } diff --git a/catalog/nau2004/sec6.nfg b/catalog/nau2004/sec6.nfg index bb378500a..68ef9934c 100644 --- a/catalog/nau2004/sec6.nfg +++ b/catalog/nau2004/sec6.nfg @@ -4,7 +4,7 @@ NFG 1 R "2x2x4 game with Nash equilibria in the relative interior of the correla { "Left" "Right" } { "1" "2" "3" "4" } } -"A three-player 2x2x4 game (section 6 of `Nau2004 `_). The correlated equilibrium polytope is four-dimensional with six vertices. The set of Nash equilibria is a line segment in the relative interior of the polytope." +"A three-player 2x2x4 game (section 6 of :cite:p:`Nau2004`). The correlated equilibrium polytope is four-dimensional with six vertices. The set of Nash equilibria is a line segment in the relative interior of the polytope." { { "" 2, 0, 0 } diff --git a/catalog/reiley2008/fig1.efg b/catalog/reiley2008/fig1.efg index df8c5a4d8..520edb0f7 100644 --- a/catalog/reiley2008/fig1.efg +++ b/catalog/reiley2008/fig1.efg @@ -1,7 +1,7 @@ EFG 2 R "Stripped-down poker (Reiley et al 2008)" { "Professor" "Student" } -"This is a one-card poker game used in `Rei2008 `_ as a teaching exercise. +"This is a one-card poker game used in :cite:p:`Rei2008` as a teaching exercise. -See also `Mye91 `_ +See also :cite:p:`Mye91` Another one-card poker game with slightly different rules. " diff --git a/catalog/selten1975/fig1.efg b/catalog/selten1975/fig1.efg index 039d50f8f..ac33bb27d 100644 --- a/catalog/selten1975/fig1.efg +++ b/catalog/selten1975/fig1.efg @@ -1,5 +1,5 @@ EFG 2 R "Selten's horse (Selten IJGT 1975, Figure 1)" { "Player 1" "Player 2" "Player 3" } -"This is a three-player game presented in `Sel75 `_, commonly referred +"This is a three-player game presented in :cite:p:`Sel75`, commonly referred to as \"Selten's horse\" owing to the layout in which it can be drawn. It is the motivating example for his definition of (trembling-hand) perfect equilibrium, by showing a game that has an equilibrium which diff --git a/catalog/selten1975/fig2.efg b/catalog/selten1975/fig2.efg index 116dadb5d..c9775a720 100644 --- a/catalog/selten1975/fig2.efg +++ b/catalog/selten1975/fig2.efg @@ -1,5 +1,5 @@ EFG 2 R "Selten (IJGT 1975) Figure 2" { "Player 1" "Player 2" } -"This is a counterexample presented in `Sel75 `_, to show that extensive and +"This is a counterexample presented in :cite:p:`Sel75`, to show that extensive and normal form concepts of perfectness do not coincide. This game has one perfect equilibrium in the extensive from, but a distinct (pure) strategy equilibrium is also perfect in the normal form. diff --git a/catalog/selten1975/fig3.efg b/catalog/selten1975/fig3.efg index f7364f91b..309d89411 100644 --- a/catalog/selten1975/fig3.efg +++ b/catalog/selten1975/fig3.efg @@ -1,5 +1,5 @@ EFG 2 R "Selten (IJGT 1975) Figure 3" { "Player 1" "Player 2" "Player 3" } -"This is a counterexample presented in `Sel75 `_, to show that extensive and +"This is a counterexample presented in :cite:p:`Sel75`, to show that extensive and normal form concepts of perfectness do not coincide. Specifically, there are two equilibria which are perfect in the normal form but not perfect in the extensive form. diff --git a/catalog/vonstengel2022/fig10.1.efg b/catalog/vonstengel2022/fig10.1.efg index f94d07eab..af6ccd020 100644 --- a/catalog/vonstengel2022/fig10.1.efg +++ b/catalog/vonstengel2022/fig10.1.efg @@ -1,9 +1,9 @@ EFG 2 R "Figure 10.1 from von Stengel (2022)" { "I" "II" } " -Figure 10.1 from `vS22 `_. +Figure 10.1 from :cite:p:`vS22`. It is essentially a type of poker game. Its description as a competition between software firms is due to -`TvS02 `_. +:cite:p:`TvS02`. " c "" 1 "" { "1/2" 1/2 "1/2" 1/2 } 0 diff --git a/catalog/vonstengel2022/fig10.12.efg b/catalog/vonstengel2022/fig10.12.efg index 44bcd679b..16a65e247 100644 --- a/catalog/vonstengel2022/fig10.12.efg +++ b/catalog/vonstengel2022/fig10.12.efg @@ -1,6 +1,6 @@ EFG 2 R "Figure 10.12 from von Stengel (2022)" { "I" "II" } " -Figure 10.12 from `vS22 `_. +Figure 10.12 from :cite:p:`vS22`. It refers to a non-standard version of the Monty Hall problem where the television show host Monty Hall has the option of opening another door without a prize to the contestant (player I), rather than opening such a door all the time. Its purpose is to demonstrate a whole convex set of optimal diff --git a/catalog/vonstengel2022/fig10.5.efg b/catalog/vonstengel2022/fig10.5.efg index 637a9db22..8e4f29c2d 100644 --- a/catalog/vonstengel2022/fig10.5.efg +++ b/catalog/vonstengel2022/fig10.5.efg @@ -1,6 +1,6 @@ EFG 2 R "Figure 10.5 from von Stengel (2022)" { "I" "II" } " -Figure 10.5 from `vS22 `_. +Figure 10.5 from :cite:p:`vS22`. Player II has four reduced strategies in this game, compared to eight unreduced strategies. " diff --git a/catalog/vonstengel2022/fig10.7.efg b/catalog/vonstengel2022/fig10.7.efg index d4f9bc1b6..690b7f49f 100644 --- a/catalog/vonstengel2022/fig10.7.efg +++ b/catalog/vonstengel2022/fig10.7.efg @@ -1,6 +1,6 @@ EFG 2 R "Figure 10.7 from von Stengel (2022)" { "I" "II" } " -Figure 10.7 from `vS22 `_. +Figure 10.7 from :cite:p:`vS22`. It is a game with imperfect recall, but it has the same strategic form as the game in Figure 10.5 from the same book. One equilibrium strategy of player II as computed from the strategic form is not realization diff --git a/catalog/vonstengelforges2008/fig1.efg b/catalog/vonstengelforges2008/fig1.efg index 5c74de79d..5541be301 100644 --- a/catalog/vonstengelforges2008/fig1.efg +++ b/catalog/vonstengelforges2008/fig1.efg @@ -1,6 +1,6 @@ EFG 2 R "Figure 1 from von Stengel and Forges (2008)" { "1" "2" } " -Figure 1 from `vSF08 `_. +Figure 1 from :cite:p:`vSF08`. It is a kind of a kind of signaling game. " diff --git a/catalog/vonstengelforges2008/fig6.efg b/catalog/vonstengelforges2008/fig6.efg index 8a798de32..a0cc458f1 100644 --- a/catalog/vonstengelforges2008/fig6.efg +++ b/catalog/vonstengelforges2008/fig6.efg @@ -1,6 +1,6 @@ EFG 2 R "Figure 6 from von Stengel and Forges (2008)" { "1" "2" } " -Figure 6 from `vSF08 `_. +Figure 6 from :cite:p:`vSF08`. This game has perfect recall and no chance moves, yet it has a circular precedence structure among the information sets of the two players. The payoffs are not important. " diff --git a/catalog/vonstengelforges2008/fig9.efg b/catalog/vonstengelforges2008/fig9.efg index 6850a0865..779ec01f3 100644 --- a/catalog/vonstengelforges2008/fig9.efg +++ b/catalog/vonstengelforges2008/fig9.efg @@ -1,6 +1,6 @@ EFG 2 R "Figure 9 from von Stengel and Forges (2008)" { "1" "2" } " -Figure 9 from `vSF08 `_. +Figure 9 from :cite:p:`vSF08`. It encodes the SAT formula :math:`x\wedge (\neg x \vee y) \wedge (\neg x \vee \neg y)` (writing $-x$ for :math:`\neg x`). The formula is satisfiable (which here it is not) if and only diff --git a/catalog/watson2013/exercise29_6.efg b/catalog/watson2013/exercise29_6.efg index e1fbc7551..6b38145e6 100644 --- a/catalog/watson2013/exercise29_6.efg +++ b/catalog/watson2013/exercise29_6.efg @@ -1,5 +1,5 @@ EFG 2 R "Princess Bride signaling game (from Watson)" { "Wesley" "Prince" } -"This game is Exercise 29.6 from Watson `Wat13 `_, based on a scene from +"This game is Exercise 29.6 from Watson :cite:p:`Wat13`, based on a scene from the Rob Reiner film, The Princess Bride: Wesley (the protagonist) confronts the evil prince Humperdinck. Wesley diff --git a/catalog/watson2013/fig29_1.efg b/catalog/watson2013/fig29_1.efg index 6b08714b2..63b065174 100644 --- a/catalog/watson2013/fig29_1.efg +++ b/catalog/watson2013/fig29_1.efg @@ -1,6 +1,6 @@ EFG 2 R "Job-market signaling game (version from Watson)" { "You" "Firm" } "This is a version of Spence's classic model of education being a job-market -signal, as presented in Figure 29.1 of Watson `Wat13 `_. +signal, as presented in Figure 29.1 of Watson :cite:p:`Wat13`. " c "" 1 "" { "High" 1/3 "Low" 2/3 } 0 diff --git a/doc/algorithms.rst b/doc/algorithms.rst index 6d1b17962..058262814 100644 --- a/doc/algorithms.rst +++ b/doc/algorithms.rst @@ -36,9 +36,9 @@ enummixed Computes Nash equilibria using extreme point enumeration. In a two-player strategic game, the set of Nash equilibria can be expressed as the union of convex sets. -This program generates all the extreme points of those convex sets. (Mangasarian [Man64]_) +This program generates all the extreme points of those convex sets. (Mangasarian :cite:p:`Man64`) This is a superset of the points generated by the path-following procedure of Lemke and Howson (see :ref:`lcp`). -It was shown by Shapley [Sha74]_ that there are equilibria not accessible via the method in :ref:`lcp`, whereas the output of +It was shown by Shapley :cite:p:`Sha74` that there are equilibria not accessible via the method in :ref:`lcp`, whereas the output of :program:`enummixed` is guaranteed to return all the extreme points. .. _enumpoly: @@ -61,7 +61,7 @@ polynomials, the subdivision constructed is such that each cell contains either no equilibria or exactly one equilibrium. For strategic games, the program searches supports in the order proposed -by Porter, Nudelman, and Shoham [PNS04]_. For two-player games, this +by Porter, Nudelman, and Shoham :cite:p:`PNS04`. For two-player games, this prioritises supports for which both players have the same number of strategies. For games with three or more players, this prioritises supports which have the fewest strategies in total. For many classes @@ -89,7 +89,7 @@ lp Computes a Nash equilibrium in a two-player game by solving a linear program. For extensive games, the program uses the sequence form formulation of Koller, Megiddo, and von -Stengel [KolMegSte94]_. +Stengel :cite:p:`KolMegSte94`. While the set of equilibria in a two-player constant-sum strategic game is convex, this method will only identify one of the extreme @@ -103,15 +103,15 @@ lcp Computes Nash equilibria of a two-player game by finding solutions to a linear complementarity problem. For extensive games, the program uses the sequence form representation of the extensive game, as defined by -Koller, Megiddo, and von Stengel [KolMegSte94]_, and applies the +Koller, Megiddo, and von Stengel :cite:p:`KolMegSte94`, and applies the algorithm developed by Lemke. For strategic games, the program uses the method of Lemke and Howson -[LemHow64]_. In this case, the method will find all "accessible" +:cite:p:`LemHow64`. In this case, the method will find all "accessible" equilibria, i.e., those that can be found as concatenations of Lemke-Howson paths that start at the artificial equilibrium. There exist strategic-form games for which some equilibria cannot be found -by this method, i.e., some equilibria are inaccessible; see Shapley [Sha74]_. +by this method, i.e., some equilibria are inaccessible; see Shapley :cite:p:`Sha74`. In a two-player strategic game, the set of Nash equilibria can be expressed as the union of convex sets. This program will find extreme points @@ -142,8 +142,8 @@ logit Computes the principal branch of the (logit) quantal response correspondence. -The method is based on the procedure described in Turocy [Tur05]_ for -strategic games and Turocy [Tur10]_ for extensive games. +The method is based on the procedure described in Turocy :cite:p:`Tur05` for +strategic games and Turocy :cite:p:`Tur10` for extensive games. It uses standard path-following methods (as described in Allgower and Georg's "Numerical Continuation Methods") to adaptively trace the principal branch of the correspondence @@ -161,7 +161,7 @@ convergence of Newton's method in the corrector step. If the convergence is fast, the step size is adjusted upward (accelerated); if it is slow, the step size is decreased (decelerated). The maximum acceleration (or deceleration) can be set as an argument. As described in -Turocy [Tur05]_, this acceleration helps to +Turocy :cite:p:`Tur05`, this acceleration helps to efficiently trace the correspondence when it reaches its asymptotic phase for large values of the precision parameter lambda. @@ -179,7 +179,7 @@ Computes approximations to Nash equilibria using a simplicial subdivision approach. This program implements the algorithm of van der Laan, Talman, and van -Der Heyden [VTH87]_. The algorithm proceeds by constructing a triangulated grid +Der Heyden :cite:p:`VTH87`. The algorithm proceeds by constructing a triangulated grid over the space of mixed strategy profiles, and uses a path-following method to compute an approximate fixed point. This approximate fixed point can then be used as a starting point on a refinement of the @@ -194,7 +194,7 @@ ipa Computes Nash equilibria using an iterated polymatrix approximation approach -developed by Govindan and Wilson [GovWil04]_. +developed by Govindan and Wilson :cite:p:`GovWil04`. This program is based on the `Gametracer 0.2 `_ implementation by Ben Blum and Christian Shelton. @@ -211,7 +211,7 @@ gnm Computes Nash equilibria using a global Newton method approach developed by Govindan -and Wilson [GovWil03]_. This program is based on the +and Wilson :cite:p:`GovWil03`. This program is based on the `Gametracer 0.2 `_ implementation by Ben Blum and Christian Shelton. diff --git a/doc/biblio.rst b/doc/biblio.rst index 7a20e9ebb..ffa4fb6cf 100644 --- a/doc/biblio.rst +++ b/doc/biblio.rst @@ -5,175 +5,34 @@ Bibliography .. note:: - To reference an entry in this bibliography, use the format ``[key]_``, for example, ``[Mye91]_`` will link to the Myerson (1991) textbook entry. + To reference an entry in this bibliography, use the format ``:cite:p:`key```, for example, ``:cite:p:`Mye91``` will link to the Myerson (1991) textbook entry. Articles on computation of equilibria ------------------------------------- -.. [BlaTur23] Bland, J. R. and Turocy, T. L. 2023, - 'Quantal response equilibrium as a structural model for estimation: the - missing manual', *SSRN Working Paper*, no. 4425515. - -.. [Eav71] Eaves, B. C. 1971, 'The linear complementarity problem', - *Management Science*, vol. 17, pp. 612-634. - -.. [GovWil03] Govindan, S. and Wilson, R. 2003, - 'A global Newton method to compute Nash equilibria', - *Journal of Economic Theory*, vol. 110, no. 1, pp. 65-86. - -.. [GovWil04] Govindan, S. and Wilson, R. 2004, - 'Computing Nash equilibria by iterated polymatrix approximation', - *Journal of Economic Dynamics and Control*, vol. 28, pp. 1229-1241. - -.. [Jiang11] Jiang, A. X., Leyton-Brown, K. and Bhat, N. 2011, - 'Action-graph games', *Games and Economic Behavior*, vol. 71, no. 1, - pp. 141-173. - -.. [KolMegSte94] Koller, D., Megiddo, N. and von Stengel, B. 1996, - 'Efficient computation of equilibria for extensive two-person games', - *Games and Economic Behavior*, vol. 14, pp. 247-259. - -.. [LemHow64] Lemke, C. E. and Howson, J. T. 1964, - 'Equilibrium points of bimatrix games', - *Journal of the Society of Industrial and Applied Mathematics*, - vol. 12, pp. 413-423. - -.. [Man64] Mangasarian, O. 1964, 'Equilibrium points in bimatrix games', - *Journal of the Society for Industrial and Applied Mathematics*, - vol. 12, pp. 778-780. - -.. [McK91] McKelvey, R. 1991, 'A Liapunov function for Nash equilibria', - California Institute of Technology. - -.. [McKMcL96] McKelvey, R. and McLennan, A. 1996, - 'Computation of equilibria in finite games', in Amman, H., Kendrick, - D. and Rust, J. (eds), *Handbook of Computational Economics*, Elsevier, - pp. 87-142. - -.. [Nau2004] Nau, Robert, Gomez Canovas, Sabrina, and Hansen, Pierre (2004). - 'On the geometry of Nash equilibria and correlated equilibria', - *International Journal of Game Theory*, vol. 32, pp. 443-453. - -.. [PNS04] Porter, R., Nudelman, E. and Shoham, Y. 2004, - 'Simple search methods for finding a Nash equilibrium', - *Games and Economic Behavior*, vol. 63, pp. 664-662. - -.. [Ros71] Rosenmuller, J. 1971, - 'On a generalization of the Lemke-Howson algorithm to noncooperative - n-person games', *SIAM Journal of Applied Mathematics*, vol. 21, - pp. 73-79. - -.. [Sha74] Shapley, L. 1974, 'A note on the Lemke-Howson algorithm', - *Mathematical Programming Study*, vol. 1, pp. 175-189. - -.. [Tur05] Turocy, T. L. 2005, - 'A dynamic homotopy interpretation of the logistic quantal response - equilibrium correspondence', *Games and Economic Behavior*, vol. 51, - pp. 243-263. - -.. [Tur10] Turocy, T. L. 2010, - 'Using quantal response to compute Nash and sequential equilibria', - *Economic Theory*, vol. 42, pp. 255-269. - -.. [VTH87] van der Laan, G., Talman, A. J. J. and van Der Heyden, L. 1987, - 'Simplicial variable dimension algorithms for solving the nonlinear - complementarity problem on a product of unit simplices using a general - labelling', *Mathematics of Operations Research*, vol. 12, pp. 377-397. - -.. [vSF08] von Stengel, B. and Forges, F., 2008, - 'Extensive-form correlated equilibrium: Definition and computational complexity', - *Mathematics of Operations Research*, vol. 33, pp. 1002–1022. - -.. [Wil71] Wilson, R. 1971, 'Computing equilibria of n-person games', - *SIAM Applied Math*, vol. 21, pp. 80-87. - -.. [Yam93] Yamamoto, Y. 1993, - 'A path-following procedure to find a proper equilibrium of finite - games', *International Journal of Game Theory*, vol. 22, pp. 249–259. +.. bibliography:: references.bib + :style: alpha + :filter: category == "computation" + :all: General game theory articles and texts -------------------------------------- -.. [Bag1995] Bagwell, K. 1995, 'Commitment and observability in games', - *Games and Economic Behavior*, vol. 8, pp. 271-280. - -.. [Gil97] Gilboa, I. 1997, - 'A Comment on the Absent-Minded Driver Paradox', - *Games and Economic Behavior*, vol. 20, pp. 25-30. - -.. [Harsanyi1967a] Harsanyi, J. 1967, - 'Games of incomplete information played by Bayesian players I', - *Management Science*, vol. 14, pp. 159-182. - -.. [Harsanyi1967b] Harsanyi, J. 1967, - 'Games of incomplete information played by Bayesian players II', - *Management Science*, vol. 14, pp. 320-334. - -.. [Harsanyi1968] Harsanyi, J. 1968, - 'Games of incomplete information played by Bayesian players III', - *Management Science*, vol. 14, pp. 486-502. - -.. [JakSorCon16] Jakobsen, S. K, Sørensen, T. B. and Conitzer, V. 2016, - 'Timeability of Extensive-Form Games', - *Proceedings of the Seventh Innovations in Theoretical Computer Science Conference*, - pp. 191-199. - -.. [KreWil82] Kreps, D. and Wilson, R. 1982, 'Sequential equilibria', - *Econometrica*, vol. 50, pp. 863-894. - -.. [Kre90] Kreps, D. 1990, *A Course in Microeconomic Theory*, - Princeton University Press. - -.. [McKPal95] McKelvey, R. and Palfrey, T. 1995, - 'Quantal response equilibria for normal form games', - *Games and Economic Behavior*, vol. 10, pp. 6-38. - -.. [McKPal98] McKelvey, R. and Palfrey, T. 1998, - 'Quantal response equilibria for extensive form games', - *Experimental Economics*, vol. 1, pp. 9-41. - -.. [Mye78] Myerson, R. 1978, - 'Refinements of the Nash equilibrium concept', - *International Journal of Game Theory*, vol. 7, pp. 73-80. - -.. [Nas50] Nash, J. 1950, 'Equilibrium points in n-person games', - *Proceedings of the National Academy of Sciences*, vol. 36, - pp. 48-49. - -.. [Och95] Ochs, J. 1995, - 'Games with unique, mixed strategy equilibria: an experimental study', - *Games and Economic Behavior*, vol. 10, pp. 202-217. - -.. [Rei2008] Reiley, D. H., Urbancic, M. B. and Walker, M. 2008, - 'Stripped-down poker: a classroom game with signaling and bluffing', - *The Journal of Economic Education*, vol. 4, pp. 323-341. - -.. [Sel75] Selten, R. 1975, - 'Reexamination of the perfectness concept for equilibrium points in - extensive games', *International Journal of Game Theory*, vol. 4, - pp. 25-55. - -.. [vanD83] van Damme, E. 1983, *Stability and Perfection of Nash - Equilibria*, Springer-Verlag, Berlin. +.. bibliography:: references.bib + :style: alpha + :filter: category == "general" + :all: Textbooks and general references -------------------------------- -.. [Mye91] Myerson, R. 1991, *Game Theory: Analysis of Conflict*, - Harvard University Press. - -.. [TvS02] Turocy, T.L. and von Stengel, B., 2002, - 'Game theory', in: *Encyclopedia of Information Systems*, vol. 2, pp. 403–420, - Elsevier Science. - -.. [vS22] von Stengel, B., 2022, *Game Theory Basics*, - Cambridge University Press. - -.. [Wat13] Watson, J. 2013, *Strategy: An Introduction to Game Theory*, - 3rd edn, W. W. Norton & Company. +.. bibliography:: references.bib + :style: alpha + :filter: category == "textbooks" + :all: diff --git a/doc/conf.py b/doc/conf.py index f3926f47d..fbca918ca 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -31,9 +31,12 @@ "nbsphinx", "sphinxcontrib.tikz", "jupyter_sphinx", - "jupyter_sphinx", + "sphinxcontrib.bibtex", ] +# BibTeX configuration +bibtex_bibfiles = ["references.bib"] + # IPython directive configuration ipython_execlines = ["import pygambit as gbt", "import os", "import sys"] ipython_savefig_dir = "savefig" diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index eb9dcfa5c..bd8de3992 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -26,8 +26,7 @@ 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 `. - Use a full link to the bibliography entry, so the link can be accessed from the file directly, as well as being rendered in the docs e.g. ```Rei2008 `_`` + 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```. .. important:: diff --git a/doc/gui.nash.rst b/doc/gui.nash.rst index d0e57b6a1..1891dad4c 100644 --- a/doc/gui.nash.rst +++ b/doc/gui.nash.rst @@ -141,9 +141,9 @@ Computing quantal response equilibria ------------------------------------- Gambit provides methods for computing the logit quantal response -equilibrium correspondence for extensive games [McKPal98]_ -and strategic games [McKPal95]_, -using the tracing method of [Tur05]_. +equilibrium correspondence for extensive games :cite:p:`McKPal98` +and strategic games :cite:p:`McKPal95`, +using the tracing method of :cite:p:`Tur05`. .. image:: screens/qre.* :width: 33% diff --git a/doc/references.bib b/doc/references.bib new file mode 100644 index 000000000..3bb8b2fa4 --- /dev/null +++ b/doc/references.bib @@ -0,0 +1,392 @@ +@article{BlaTur23, + author = {Bland, J. R. and Turocy, T. L.}, + title = {Quantal response equilibrium as a structural model for estimation: the missing manual}, + journal = {SSRN Working Paper}, + number = {4425515}, + year = {2023}, + category = {computation} +} + +@article{Eav71, + author = {Eaves, B. C.}, + title = {The linear complementarity problem}, + journal = {Management Science}, + volume = {17}, + pages = {612--634}, + year = {1971}, + category = {computation} +} + +@article{GovWil03, + author = {Govindan, S. and Wilson, R.}, + title = {A global {N}ewton method to compute {N}ash equilibria}, + journal = {Journal of Economic Theory}, + volume = {110}, + number = {1}, + pages = {65--86}, + year = {2003}, + category = {computation} +} + +@article{GovWil04, + author = {Govindan, S. and Wilson, R.}, + title = {Computing {N}ash equilibria by iterated polymatrix approximation}, + journal = {Journal of Economic Dynamics and Control}, + volume = {28}, + pages = {1229--1241}, + year = {2004}, + category = {computation} +} + +@article{Jiang11, + author = {Jiang, A. X. and Leyton-Brown, K. and Bhat, N.}, + title = {Action-graph games}, + journal = {Games and Economic Behavior}, + volume = {71}, + number = {1}, + pages = {141--173}, + year = {2011}, + category = {computation} +} + +@article{KolMegSte94, + author = {Koller, D. and Megiddo, N. and von Stengel, B.}, + title = {Efficient computation of equilibria for extensive two-person games}, + journal = {Games and Economic Behavior}, + volume = {14}, + pages = {247--259}, + year = {1996}, + category = {computation} +} + +@article{LemHow64, + author = {Lemke, C. E. and Howson, J. T.}, + title = {Equilibrium points of bimatrix games}, + journal = {Journal of the Society of Industrial and Applied Mathematics}, + volume = {12}, + pages = {413--423}, + year = {1964}, + category = {computation} +} + +@article{Man64, + author = {Mangasarian, O.}, + title = {Equilibrium points in bimatrix games}, + journal = {Journal of the Society for Industrial and Applied Mathematics}, + volume = {12}, + pages = {778--780}, + year = {1964}, + category = {computation} +} + +@techreport{McK91, + author = {McKelvey, R.}, + title = {A {L}iapunov function for {N}ash equilibria}, + institution = {California Institute of Technology}, + year = {1991}, + category = {computation} +} + +@incollection{McKMcL96, + author = {McKelvey, R. and McLennan, A.}, + title = {Computation of equilibria in finite games}, + booktitle = {Handbook of Computational Economics}, + editor = {Amman, H. and Kendrick, D. and Rust, J.}, + publisher = {Elsevier}, + pages = {87--142}, + year = {1996}, + category = {computation} +} + +@article{Nau2004, + author = {Nau, Robert and Gomez Canovas, Sabrina and Hansen, Pierre}, + title = {On the geometry of {N}ash equilibria and correlated equilibria}, + journal = {International Journal of Game Theory}, + volume = {32}, + pages = {443--453}, + year = {2004}, + category = {computation} +} + +@article{PNS04, + author = {Porter, R. and Nudelman, E. and Shoham, Y.}, + title = {Simple search methods for finding a {N}ash equilibrium}, + journal = {Games and Economic Behavior}, + volume = {63}, + pages = {664--662}, + year = {2004}, + category = {computation} +} + +@article{Ros71, + author = {Rosenmuller, J.}, + title = {On a generalization of the {L}emke-{H}owson algorithm to noncooperative n-person games}, + journal = {SIAM Journal of Applied Mathematics}, + volume = {21}, + pages = {73--79}, + year = {1971}, + category = {computation} +} + +@article{Sha74, + author = {Shapley, L.}, + title = {A note on the {L}emke-{H}owson algorithm}, + journal = {Mathematical Programming Study}, + volume = {1}, + pages = {175--189}, + year = {1974}, + category = {computation} +} + +@article{Tur05, + author = {Turocy, T. L.}, + title = {A dynamic homotopy interpretation of the logistic quantal response equilibrium correspondence}, + journal = {Games and Economic Behavior}, + volume = {51}, + pages = {243--263}, + year = {2005}, + category = {computation} +} + +@article{Tur10, + author = {Turocy, T. L.}, + title = {Using quantal response to compute {N}ash and sequential equilibria}, + journal = {Economic Theory}, + volume = {42}, + pages = {255--269}, + year = {2010}, + category = {computation} +} + +@article{VTH87, + author = {van der Laan, G. and Talman, A. J. J. and van Der Heyden, L.}, + title = {Simplicial variable dimension algorithms for solving the nonlinear complementarity problem on a product of unit simplices using a general labelling}, + journal = {Mathematics of Operations Research}, + volume = {12}, + pages = {377--397}, + year = {1987}, + category = {computation} +} + +@article{vSF08, + author = {von Stengel, B. and Forges, F.}, + title = {Extensive-form correlated equilibrium: {D}efinition and computational complexity}, + journal = {Mathematics of Operations Research}, + volume = {33}, + pages = {1002--1022}, + year = {2008}, + category = {computation} +} + +@article{Wil71, + author = {Wilson, R.}, + title = {Computing equilibria of n-person games}, + journal = {SIAM Applied Math}, + volume = {21}, + pages = {80--87}, + year = {1971}, + category = {computation} +} + +@article{Yam93, + author = {Yamamoto, Y.}, + title = {A path-following procedure to find a proper equilibrium of finite games}, + journal = {International Journal of Game Theory}, + volume = {22}, + pages = {249--259}, + year = {1993}, + category = {computation} +} + +@article{Bag1995, + author = {Bagwell, K.}, + title = {Commitment and observability in games}, + journal = {Games and Economic Behavior}, + volume = {8}, + pages = {271--280}, + year = {1995}, + category = {general} +} + +@article{Gil97, + author = {Gilboa, I.}, + title = {A Comment on the Absent-Minded Driver Paradox}, + journal = {Games and Economic Behavior}, + volume = {20}, + pages = {25--30}, + year = {1997}, + category = {general} +} + +@article{Harsanyi1967a, + author = {Harsanyi, J.}, + title = {Games of incomplete information played by {B}ayesian players {I}}, + journal = {Management Science}, + volume = {14}, + pages = {159--182}, + year = {1967}, + category = {general} +} + +@article{Harsanyi1967b, + author = {Harsanyi, J.}, + title = {Games of incomplete information played by {B}ayesian players {II}}, + journal = {Management Science}, + volume = {14}, + pages = {320--334}, + year = {1967}, + category = {general} +} + +@article{Harsanyi1968, + author = {Harsanyi, J.}, + title = {Games of incomplete information played by {B}ayesian players {III}}, + journal = {Management Science}, + volume = {14}, + pages = {486--502}, + year = {1968}, + category = {general} +} + +@inproceedings{JakSorCon16, + author = {Jakobsen, S. K. and S{\o}rensen, T. B. and Conitzer, V.}, + title = {Timeability of Extensive-Form Games}, + booktitle = {Proceedings of the Seventh Innovations in Theoretical Computer Science Conference}, + pages = {191--199}, + year = {2016}, + category = {general} +} + +@article{KreWil82, + author = {Kreps, D. and Wilson, R.}, + title = {Sequential equilibria}, + journal = {Econometrica}, + volume = {50}, + pages = {863--894}, + year = {1982}, + category = {general} +} + +@book{Kre90, + author = {Kreps, D.}, + title = {A Course in Microeconomic Theory}, + publisher = {Princeton University Press}, + year = {1990}, + category = {general} +} + +@article{McKPal95, + author = {McKelvey, R. and Palfrey, T.}, + title = {Quantal response equilibria for normal form games}, + journal = {Games and Economic Behavior}, + volume = {10}, + pages = {6--38}, + year = {1995}, + category = {general} +} + +@article{McKPal98, + author = {McKelvey, R. and Palfrey, T.}, + title = {Quantal response equilibria for extensive form games}, + journal = {Experimental Economics}, + volume = {1}, + pages = {9--41}, + year = {1998}, + category = {general} +} + +@article{Mye78, + author = {Myerson, R.}, + title = {Refinements of the {N}ash equilibrium concept}, + journal = {International Journal of Game Theory}, + volume = {7}, + pages = {73--80}, + year = {1978}, + category = {general} +} + +@article{Nas50, + author = {Nash, J.}, + title = {Equilibrium points in n-person games}, + journal = {Proceedings of the National Academy of Sciences}, + volume = {36}, + pages = {48--49}, + year = {1950}, + category = {general} +} + +@article{Och95, + author = {Ochs, J.}, + title = {Games with unique, mixed strategy equilibria: an experimental study}, + journal = {Games and Economic Behavior}, + volume = {10}, + pages = {202--217}, + year = {1995}, + category = {general} +} + +@article{Rei2008, + author = {Reiley, D. H. and Urbancic, M. B. and Walker, M.}, + title = {Stripped-down poker: a classroom game with signaling and bluffing}, + journal = {The Journal of Economic Education}, + volume = {39}, + number = {4}, + pages = {323--341}, + year = {2008}, + category = {general} +} + +@article{Sel75, + author = {Selten, R.}, + title = {Reexamination of the perfectness concept for equilibrium points in extensive games}, + journal = {International Journal of Game Theory}, + volume = {4}, + pages = {25--55}, + year = {1975}, + category = {general} +} + +@book{vanD83, + author = {van Damme, E.}, + title = {Stability and Perfection of {N}ash Equilibria}, + publisher = {Springer-Verlag}, + address = {Berlin}, + year = {1983}, + category = {general} +} + +@book{Mye91, + author = {Myerson, R.}, + title = {Game Theory: Analysis of Conflict}, + publisher = {Harvard University Press}, + year = {1991}, + category = {textbooks} +} + +@incollection{TvS02, + author = {Turocy, T. L. and von Stengel, B.}, + title = {Game theory}, + booktitle = {Encyclopedia of Information Systems}, + volume = {2}, + publisher = {Elsevier Science}, + pages = {403--420}, + year = {2002}, + category = {textbooks} +} + +@book{vS22, + author = {von Stengel, B.}, + title = {Game Theory Basics}, + publisher = {Cambridge University Press}, + year = {2022}, + category = {textbooks} +} + +@book{Wat13, + author = {Watson, Joel}, + title = {Strategy: An Introduction to Game Theory}, + edition = {3rd}, + publisher = {W. W. Norton \& Company}, + year = {2013}, + category = {textbooks} +} diff --git a/doc/tools.enummixed.rst b/doc/tools.enummixed.rst index fff7149e9..c9f3d28b3 100644 --- a/doc/tools.enummixed.rst +++ b/doc/tools.enummixed.rst @@ -49,7 +49,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 [Sel75]_:: +the reduced strategic form of the example in Figure 2 of :cite:p:`Sel75`:: $ gambit-enummixed catalog/selten1975/fig2.efg Compute Nash equilibria by enumerating extreme points diff --git a/doc/tools.enumpoly.rst b/doc/tools.enumpoly.rst index d8677041f..d69c41c99 100644 --- a/doc/tools.enumpoly.rst +++ b/doc/tools.enumpoly.rst @@ -73,7 +73,7 @@ support of some set of equilibria. singular supports are identified with the label "singular." By default, no information about supports is printed. -Computing equilibria of the example in Figure 1 of [Sel75]_, sometimes called +Computing equilibria of the example in Figure 1 of :cite:p:`Sel75`, sometimes called "Selten's horse":: $ gambit-enumpoly -S catalog/selten1975/fig1.efg diff --git a/doc/tools.enumpure.rst b/doc/tools.enumpure.rst index c735edbf2..582c5036f 100644 --- a/doc/tools.enumpure.rst +++ b/doc/tools.enumpure.rst @@ -52,7 +52,7 @@ See the :ref:`algorithm description ` for full details. Suppresses printing of the banner at program launch. -Computing the pure-strategy equilibria of extensive game in Figure 2 of [Sel75]_:: +Computing the pure-strategy equilibria of extensive game in Figure 2 of :cite:p:`Sel75`:: $ gambit-enumpure catalog/selten1975/fig2.efg diff --git a/doc/tools.gnm.rst b/doc/tools.gnm.rst index d4a5b63a0..53ed8e687 100644 --- a/doc/tools.gnm.rst +++ b/doc/tools.gnm.rst @@ -74,7 +74,7 @@ subsets of equilibria being found. not specified, only the equilibria found are reported. Computing an equilibrium of -the reduced strategic form of the example in Figure 2 of [Sel75]_:: +the reduced strategic form of the example in Figure 2 of :cite:p:`Sel75`:: $ gambit-gnm catalog/selten1975/fig2.efg Compute Nash equilibria using a global Newton method diff --git a/doc/tools.ipa.rst b/doc/tools.ipa.rst index e08ea7b42..1484a63f1 100644 --- a/doc/tools.ipa.rst +++ b/doc/tools.ipa.rst @@ -40,7 +40,7 @@ equilibria being found. Computing an equilibrium of -the reduced strategic form of the example in Figure 2 of [Sel75]_:: +the reduced strategic form of the example in Figure 2 of :cite:p:`Sel75`:: $ gambit-ipa catalog/selten1975/fig2.efg Compute Nash equilibria using iterated polymatrix approximation diff --git a/doc/tools.lcp.rst b/doc/tools.lcp.rst index ab0d70d2e..0504aee19 100644 --- a/doc/tools.lcp.rst +++ b/doc/tools.lcp.rst @@ -54,7 +54,7 @@ See the :ref:`algorithm description ` for full details. Suppresses printing of the banner at program launch. -Computing an equilibrium of the example in Figure 2 of [Sel75]_:: +Computing an equilibrium of the example in Figure 2 of :cite:p:`Sel75`:: $ gambit-lcp catalog/selten1975/fig2.efg Compute Nash equilibria by solving a linear complementarity program diff --git a/doc/tools.liap.rst b/doc/tools.liap.rst index 9f0748e96..e4232826f 100644 --- a/doc/tools.liap.rst +++ b/doc/tools.liap.rst @@ -87,7 +87,7 @@ See the :ref:`algorithm description ` for full details. that is not a Nash equilibrium, are all output, in addition to any equilibria found. -Computing an equilibrium in mixed strategies of the example in Figure 2 of [Sel75]_:: +Computing an equilibrium in mixed strategies of the example in Figure 2 of :cite:p:`Sel75`:: $ gambit-liap -S catalog/selten1975/fig2.efg Compute Nash equilibria by minimizing the Lyapunov function diff --git a/doc/tools.logit.rst b/doc/tools.logit.rst index 320a96f37..eb030b602 100644 --- a/doc/tools.logit.rst +++ b/doc/tools.logit.rst @@ -76,7 +76,7 @@ See the :ref:`algorithm description ` for full details. Computing the principal branch, in mixed strategies, of the reduced strategic form of the example -in Figure 2 of [Sel75]_:: +in Figure 2 of :cite:p:`Sel75`:: $ gambit-logit -S catalog/selten1975/fig2.efg Compute a branch of the logit equilibrium correspondence diff --git a/doc/tools.rst b/doc/tools.rst index 8a3643b79..7f5aba15a 100644 --- a/doc/tools.rst +++ b/doc/tools.rst @@ -10,7 +10,7 @@ Gambit provides command-line interfaces for each method for computing Nash equilibria. These are suitable for scripting or calling from other programs. This chapter describes the use of these programs. For a general overview of methods for computing equilibria, -see the survey of [McKMcL96]_. +see the survey of :cite:p:`McKMcL96`. The graphical interface also provides a frontend for calling these programs and evaluating their output. Direct use of the command-line diff --git a/doc/tools.simpdiv.rst b/doc/tools.simpdiv.rst index 5ab525d01..5a0114239 100644 --- a/doc/tools.simpdiv.rst +++ b/doc/tools.simpdiv.rst @@ -76,7 +76,7 @@ options to specify additional starting points for the algorithm. in addition to the approximate equilibrium profile found. -Computing an equilibrium in mixed strategies of the example in Figure 2 of [Sel75]_:: +Computing an equilibrium in mixed strategies of the example in Figure 2 of :cite:p:`Sel75`:: $ gambit-simpdiv catalog/selten1975/fig2.efg Compute Nash equilibria using simplicial subdivision diff --git a/doc/tutorials/advanced_tutorials/pygambit.external_programs.rst b/doc/tutorials/advanced_tutorials/pygambit.external_programs.rst index fbed0b9fb..14614d945 100644 --- a/doc/tutorials/advanced_tutorials/pygambit.external_programs.rst +++ b/doc/tutorials/advanced_tutorials/pygambit.external_programs.rst @@ -4,7 +4,7 @@ Using external programs to compute Nash equilibria .. TODO: this needs to be updated, see issue #561 Because the problem of finding Nash equilibria can be expressed in various -mathematical formulations (see [McKMcL96]_), it is helpful to make use +mathematical formulations (see :cite:p:`McKMcL96`), it is helpful to make use of other software packages designed specifically for solving those problems. There are currently two integrations offered for using external programs to solve @@ -27,7 +27,3 @@ processing. .. [#lrslib] http://cgm.cs.mcgill.ca/~avis/C/lrs.html .. [#phcpack] https://homepages.math.uic.edu/~jan/PHCpack/phcpack.html - -.. [McKMcL96] McKelvey, Richard D. and McLennan, Andrew M. (1996) Computation of equilibria - in finite games. In Handbook of Computational Economics, Volume 1, - pages 87-142. diff --git a/pyproject.toml b/pyproject.toml index 22c8faaf9..f7a60e8e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ doc = [ "sphinxcontrib-tikz", "jupyter_sphinx", "pyyaml", + "sphinxcontrib-bibtex", ] [project.urls] diff --git a/src/pygambit/behavmixed.pxi b/src/pygambit/behavmixed.pxi index 6fb71c836..5817a5acb 100644 --- a/src/pygambit/behavmixed.pxi +++ b/src/pygambit/behavmixed.pxi @@ -858,7 +858,7 @@ class MixedBehaviorProfile: return self._agent_max_regret() def agent_liap_value(self) -> ProfileDType: - """Returns the Lyapunov value (see [McK91]_) of the strategy profile. + """Returns the Lyapunov value (see :cite:p:`McK91`) of the strategy profile. The agent Lyapunov value is a non-negative number which is zero exactly at agent Nash equilibria. @@ -895,7 +895,7 @@ class MixedBehaviorProfile: return self._max_regret() def liap_value(self) -> ProfileDType: - """Returns the Lyapunov value (see [McK91]_) of the strategy profile. + """Returns the Lyapunov value (see :cite:p:`McK91`) of the strategy profile. The Lyapunov value is a non-negative number which is zero exactly at Nash equilibria. diff --git a/src/pygambit/stratmixed.pxi b/src/pygambit/stratmixed.pxi index cc87995d1..e4ab2a449 100644 --- a/src/pygambit/stratmixed.pxi +++ b/src/pygambit/stratmixed.pxi @@ -483,7 +483,7 @@ class MixedStrategyProfile: ) def liap_value(self) -> ProfileDType: - """Returns the Lyapunov value (see [McK91]_) of the strategy profile. + """Returns the Lyapunov value (see :cite:p:`McK91`) of the strategy profile. The Lyapunov value is a non-negative number which is zero exactly at Nash equilibria. From 639d2866e30841469e585c4bedff58c06c5b2abc Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 29 May 2026 10:45:20 +0100 Subject: [PATCH 21/38] feat: migrate download links to dropdown --- build_support/catalog/update.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index 8c1f23b91..d1fd6dfee 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -177,9 +177,9 @@ def generate_rst_table( download_links.append( f":download:`{slug}.{ext} <../catalog/img/{slug}.{ext}>`" ) - f.write(" **Download game and image files:**\n") - f.write(" \n") - f.write(f" {' '.join(download_links)}\n") + f.write(" .. dropdown:: Download game and image files\n") + f.write(" \n") + f.write(f" {' '.join(download_links)}\n") f.write(" \n") # Draw image — tab-set for multiple variants, single block otherwise From b51df4aed9397806fc34cb4d207720e10c6614a3 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 1 Jun 2026 10:31:49 +0100 Subject: [PATCH 22/38] feat: support efg tab sets for games with suffix variants even when no primary .ef file exists --- build_support/catalog/test_update.py | 36 ++++++++++++++++++++ build_support/catalog/update.py | 51 ++++++++++++++++++++-------- 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/build_support/catalog/test_update.py b/build_support/catalog/test_update.py index 978e1deaf..70589e664 100644 --- a/build_support/catalog/test_update.py +++ b/build_support/catalog/test_update.py @@ -279,6 +279,21 @@ def test_two_ef_files_returns_variant_list(self, tmp_path): assert {v["label"] for v in result} == {"Default", "Wide"} assert {v["variant_key"] for v in result} == {slug, f"{slug}__wide"} + def test_variant_without_primary_ef_returns_variants(self, tmp_path): + """A variant .ef file exists but no primary .ef file exists -> + returns variants list including Default. + """ + catalog_dir = tmp_path / "catalog" + 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) + assert result is not None + assert len(result) == 2 + assert {v["label"] for v in result} == {"Default", "Wide"} + assert {v["variant_key"] for v in result} == {slug, f"{slug}__wide"} + assert not (game_dir / "fig1.ef").exists() + def test_label_derived_from_filename_suffix(self, tmp_path): """The tab label is the suffix after ``__``, title-cased.""" catalog_dir = tmp_path / "catalog" @@ -486,6 +501,27 @@ def test_multi_variant_efg_produces_tab_set(self, tmp_path, monkeypatch): assert ".. tab-item:: Default" in rst assert ".. tab-item:: Wide" in rst + def test_multi_variant_efg_without_primary_ef_produces_tab_set(self, tmp_path, monkeypatch): + """A variant .ef file without a primary .ef file triggers a tab-set in the RST + 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") + catalog_dir = tmp_path / "catalog" + slug = "fakevariant2001/fig1" + game_dir = catalog_dir / "fakevariant2001" + game_dir.mkdir(parents=True) + (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) + rst = rst_path.read_text() + assert ".. tab-set::" in rst + assert ".. tab-item:: Default" in rst + assert ".. tab-item:: Wide" in rst + assert 'draw_tree(pygambit.catalog.load("fakevariant2001/fig1")' in rst + assert 'draw_tree("../catalog/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``.""" self._mock_generates(monkeypatch) diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index d1fd6dfee..96c52f042 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -33,8 +33,9 @@ def catalog_ef_file_variants(slug: str, catalog_dir: Path) -> list[dict] | None: """Scan catalog_dir for multiple curated .ef files for a game slug. Returns a list of dicts with keys ``label``, ``ef_path``, and - ``variant_key`` when more than one .ef file is found alongside the game; - returns None when zero or one .ef file exists (no tabs needed). + ``variant_key`` when multiple .ef variants are detected alongside the game; + returns None when zero or one .ef file exists and no suffix variants are present + (no tabs needed). File-naming convention:: @@ -47,17 +48,26 @@ def catalog_ef_file_variants(slug: str, catalog_dir: Path) -> list[dict] | None: game_dir = (catalog_dir / slug).parent stem = Path(slug).name - ef_files = sorted( - p for p in game_dir.glob(f"{stem}*.ef") if p.stem == stem or p.stem.startswith(stem + "__") - ) - if len(ef_files) <= 1: + primary_ef = game_dir / f"{stem}.ef" + additional_efs = sorted(game_dir.glob(f"{stem}__*.ef")) + + # If there are no additional/suffix variants, it's not a multi-variant game. + if not additional_efs: return None variants = [] - for ef_file in ef_files: - suffix = "" if ef_file.stem == stem else ef_file.stem[len(stem) + 2:] - label = "Default" if not suffix else suffix.replace("_", " ").title() - variant_key = slug if not suffix else f"{slug}__{suffix}" + # Add the primary/default variant + variants.append({ + "label": "Default", + "ef_path": primary_ef, + "variant_key": slug, + }) + + # Add the additional suffix variants + for ef_file in additional_efs: + suffix = ef_file.stem[len(stem) + 2:] + label = suffix.replace("_", " ").title() + variant_key = f"{slug}__{suffix}" variants.append({"label": label, "ef_path": ef_file, "variant_key": variant_key}) return variants @@ -110,7 +120,10 @@ def generate_rst_table( if regenerate_images or not all(p.exists() for p in variant_paths): viz_path = catalog_dir / "img" / vkey viz_path.parent.mkdir(parents=True, exist_ok=True) - source = str(variant["ef_path"]) + if variant["ef_path"].exists(): + source = str(variant["ef_path"]) + else: + source = gbt.catalog.load(slug) for func in [generate_tex, generate_png, generate_pdf, generate_svg]: func( source, @@ -118,7 +131,7 @@ def generate_rst_table( **catalog_draw_tree_settings(vkey), ) img_ef = catalog_dir / "img" / f"{vkey}.ef" - if not img_ef.exists(): + if not img_ef.exists() and variant["ef_path"].exists(): shutil.copy2(variant["ef_path"], img_ef) else: # Single variant @@ -198,9 +211,17 @@ def generate_rst_table( f.write(" \n") f.write(" import pygambit\n") f.write(" from draw_tree import draw_tree\n") - f.write( - f' draw_tree("../catalog/{vkey}.ef", {settings_str})\n' - ) + if variant["ef_path"].exists(): + f.write( + " draw_tree(" + f'"../catalog/{vkey}.ef", {settings_str})\n' + ) + else: + f.write( + f" draw_tree(" + f'pygambit.catalog.load("{slug}"), ' + f"{settings_str})\n" + ) f.write(" \n") f.write(" \n") else: From cd9d9b3524614f03f1f2c13a6c5d143522b92bfa Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 1 Jun 2026 10:44:36 +0100 Subject: [PATCH 23/38] Add gilboa1997/fig1 to catalog.am --- build_support/catalog/catalog.am | 1 + 1 file changed, 1 insertion(+) diff --git a/build_support/catalog/catalog.am b/build_support/catalog/catalog.am index 2a9095c9f..8074da383 100644 --- a/build_support/catalog/catalog.am +++ b/build_support/catalog/catalog.am @@ -1,5 +1,6 @@ CATALOG_FILES = \ catalog/bagwell1995.efg \ + catalog/gilboa1997/fig1.efg \ catalog/gilboa1997/fig2.efg \ catalog/jakobsen2016/fig1a.efg \ catalog/jakobsen2016/fig1b.efg \ From 8cd4c09fb7e50458d4a0b07db628cd3e5e2f2084 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 1 Jun 2026 10:51:46 +0100 Subject: [PATCH 24/38] update link for gilboa1997/fig1.efg --- catalog/gilboa1997/fig1.efg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalog/gilboa1997/fig1.efg b/catalog/gilboa1997/fig1.efg index a0d0ceac0..4ec42d05b 100644 --- a/catalog/gilboa1997/fig1.efg +++ b/catalog/gilboa1997/fig1.efg @@ -1,6 +1,6 @@ EFG 2 R "Absent-Minded Driver (Gilboa 1997, GEB, Figure 2)" { "Player 1" } "The original absent-minded driver problem from -`Gil97 `_" +:cite:p:`Gil97` p "" 1 1 "" { "B" "E" } 0 p "" 1 1 "" { "B" "E" } 0 From 52ee100274fc4410ca3fb25fcfdcd573eb1c51d1 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 1 Jun 2026 11:00:24 +0100 Subject: [PATCH 25/38] Update action_label_dist for gilboa1997 --- build_support/catalog/draw_tree_settings.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build_support/catalog/draw_tree_settings.yaml b/build_support/catalog/draw_tree_settings.yaml index 00b42675a..c71846502 100644 --- a/build_support/catalog/draw_tree_settings.yaml +++ b/build_support/catalog/draw_tree_settings.yaml @@ -29,3 +29,5 @@ overrides: sublevel_scaling: 1 vonstengelforges2008/fig9: sublevel_scaling: 0.5 + gilboa1997/fig1: + action_label_dist: 5.0 From 69447b7e1682bdd1132384a2afbb22c87364ea8b Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 1 Jun 2026 11:30:36 +0100 Subject: [PATCH 26/38] fix: resolve syntax error in docstring citation for Gilboa 1997 catalog entry --- catalog/gilboa1997/fig1.efg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalog/gilboa1997/fig1.efg b/catalog/gilboa1997/fig1.efg index 4ec42d05b..5d358eda7 100644 --- a/catalog/gilboa1997/fig1.efg +++ b/catalog/gilboa1997/fig1.efg @@ -1,6 +1,6 @@ EFG 2 R "Absent-Minded Driver (Gilboa 1997, GEB, Figure 2)" { "Player 1" } "The original absent-minded driver problem from -:cite:p:`Gil97` +:cite:p:`Gil97`" p "" 1 1 "" { "B" "E" } 0 p "" 1 1 "" { "B" "E" } 0 From e5cade70de25de205d864cdfa820051e914a7d6c Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 1 Jun 2026 11:47:47 +0100 Subject: [PATCH 27/38] docs: add guide for updating the bibliography and link it from the catalog documentation --- doc/developer.catalog.rst | 3 ++- doc/developer.contributing.rst | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index bd8de3992..bbed7b3e4 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -30,7 +30,8 @@ Currently supported representations are: .. important:: - If no bibliography entry exists, you should add one by editing `doc/biblio.rst`. + If no bibliography entry exists, you should add one. For instructions, + see :ref:`updating-bibliography`. 2. **Add the game(s) to the repo:** diff --git a/doc/developer.contributing.rst b/doc/developer.contributing.rst index ab8851e37..645e90137 100644 --- a/doc/developer.contributing.rst +++ b/doc/developer.contributing.rst @@ -211,6 +211,24 @@ To submit a tutorial for inclusion in the Gambit documentation, please follow th 4. *[Optional]* If your tutorial requires additional dependencies not already listed in the ``doc`` list under ``[project.optional-dependencies]`` inside ``pyproject.toml``, please add them to the file. +.. _updating-bibliography: + +Updating the Bibliography +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The bibliography entries are stored in BibTeX format in the file `doc/references.bib`. +To add a new entry: + +1. Open `doc/references.bib` and add the BibTeX citation. +2. In your BibTeX entry, include a ``category`` field to specify where the reference + should appear on the bibliography page. The allowed categories are: + + * ``computation`` for articles on computation of equilibria. + * ``general`` for general game theory articles and texts. + * ``textbooks`` for textbooks and general references. + +3. To cite the entry in documentation or game files, use the format ``:cite:p:`key```, + where ``key`` is the BibTeX key of your entry. Recognising contributions ------------------------- From b5235bc9725fbbc797de3a6761494506ec9a61b1 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 1 Jun 2026 11:52:47 +0100 Subject: [PATCH 28/38] fix: correct volume number for Reiley et al. citation in references.bib --- doc/references.bib | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/references.bib b/doc/references.bib index 3bb8b2fa4..eebab80d1 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -329,8 +329,7 @@ @article{Rei2008 author = {Reiley, D. H. and Urbancic, M. B. and Walker, M.}, title = {Stripped-down poker: a classroom game with signaling and bluffing}, journal = {The Journal of Economic Education}, - volume = {39}, - number = {4}, + volume = {4}, pages = {323--341}, year = {2008}, category = {general} From 7a3318404db29150ab93c5ce492588a0ea72d49b Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 1 Jun 2026 11:58:46 +0100 Subject: [PATCH 29/38] feat: implement custom keystyle bibliography label and apply it to documentation references --- doc/biblio.rst | 6 +++--- doc/conf.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/doc/biblio.rst b/doc/biblio.rst index ffa4fb6cf..62a81ec0e 100644 --- a/doc/biblio.rst +++ b/doc/biblio.rst @@ -13,7 +13,7 @@ Articles on computation of equilibria ------------------------------------- .. bibliography:: references.bib - :style: alpha + :style: keystyle :filter: category == "computation" :all: @@ -23,7 +23,7 @@ General game theory articles and texts -------------------------------------- .. bibliography:: references.bib - :style: alpha + :style: keystyle :filter: category == "general" :all: @@ -33,6 +33,6 @@ Textbooks and general references -------------------------------- .. bibliography:: references.bib - :style: alpha + :style: keystyle :filter: category == "textbooks" :all: diff --git a/doc/conf.py b/doc/conf.py index fbca918ca..57beb3619 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,5 +1,9 @@ import pathlib +import pybtex.plugin +from pybtex.style.formatting.alpha import Style as AlphaStyle +from pybtex.style.labels import BaseLabelStyle + # # Gambit documentation build configuration file, created by # sphinx-quickstart on Sun Mar 21 14:35:06 2010. @@ -37,6 +41,24 @@ # BibTeX configuration 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" + + +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" From 9ea491ed54197560a81eea3fb4fb530b5743dd53 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 1 Jun 2026 13:10:48 +0100 Subject: [PATCH 30/38] feat: implement custom BibTeX formatting style with dash support in conf.py --- doc/conf.py | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index 57beb3619..7d2f1aec9 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,8 +1,21 @@ 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, +) # # Gambit documentation build configuration file, created by @@ -38,7 +51,15 @@ "sphinxcontrib.bibtex", ] + # 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"] @@ -54,6 +75,127 @@ 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_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) From c2a6c04f2b72c5dbd920bacc402fb81bcee73b87 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 1 Jun 2026 13:22:37 +0100 Subject: [PATCH 31/38] docs: clarify bibliography formatting and the use of custom Python scripts in developer documentation --- doc/developer.contributing.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/developer.contributing.rst b/doc/developer.contributing.rst index 645e90137..630f6bbd7 100644 --- a/doc/developer.contributing.rst +++ b/doc/developer.contributing.rst @@ -230,6 +230,14 @@ To add a new entry: 3. To cite the entry in documentation or game files, use the format ``:cite:p:`key```, where ``key`` is the BibTeX key of your entry. +.. note:: + + The bibliography is formatted using a custom Harvard referencing style. + This style and its formatting templates are defined within ``doc/conf.py``. + A custom Python implementation is used instead of a traditional ``.bst`` + file because `.bst` styles output raw LaTeX formatting, which cannot be + rendered to HTML by Sphinx. + Recognising contributions ------------------------- From 4f1dc1e96607610b07e3cdb06693f39fe0db6d66 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 1 Jun 2026 13:27:40 +0100 Subject: [PATCH 32/38] refactor: rename bibliography categories to articles_equilibria and articles_general --- doc/biblio.rst | 4 +- doc/developer.contributing.rst | 4 +- doc/references.bib | 72 +++++++++++++++++----------------- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/doc/biblio.rst b/doc/biblio.rst index 62a81ec0e..a50a6fcce 100644 --- a/doc/biblio.rst +++ b/doc/biblio.rst @@ -14,7 +14,7 @@ Articles on computation of equilibria .. bibliography:: references.bib :style: keystyle - :filter: category == "computation" + :filter: category == "articles_equilibria" :all: @@ -24,7 +24,7 @@ General game theory articles and texts .. bibliography:: references.bib :style: keystyle - :filter: category == "general" + :filter: category == "articles_general" :all: diff --git a/doc/developer.contributing.rst b/doc/developer.contributing.rst index 630f6bbd7..72b35612e 100644 --- a/doc/developer.contributing.rst +++ b/doc/developer.contributing.rst @@ -223,8 +223,8 @@ To add a new entry: 2. In your BibTeX entry, include a ``category`` field to specify where the reference should appear on the bibliography page. The allowed categories are: - * ``computation`` for articles on computation of equilibria. - * ``general`` for general game theory articles and texts. + * ``articles_equilibria`` for articles on computation of equilibria. + * ``articles_general`` for general game theory articles and texts. * ``textbooks`` for textbooks and general references. 3. To cite the entry in documentation or game files, use the format ``:cite:p:`key```, diff --git a/doc/references.bib b/doc/references.bib index eebab80d1..8d9171a00 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -4,7 +4,7 @@ @article{BlaTur23 journal = {SSRN Working Paper}, number = {4425515}, year = {2023}, - category = {computation} + category = {articles_equilibria} } @article{Eav71, @@ -14,7 +14,7 @@ @article{Eav71 volume = {17}, pages = {612--634}, year = {1971}, - category = {computation} + category = {articles_equilibria} } @article{GovWil03, @@ -25,7 +25,7 @@ @article{GovWil03 number = {1}, pages = {65--86}, year = {2003}, - category = {computation} + category = {articles_equilibria} } @article{GovWil04, @@ -35,7 +35,7 @@ @article{GovWil04 volume = {28}, pages = {1229--1241}, year = {2004}, - category = {computation} + category = {articles_equilibria} } @article{Jiang11, @@ -46,7 +46,7 @@ @article{Jiang11 number = {1}, pages = {141--173}, year = {2011}, - category = {computation} + category = {articles_equilibria} } @article{KolMegSte94, @@ -56,7 +56,7 @@ @article{KolMegSte94 volume = {14}, pages = {247--259}, year = {1996}, - category = {computation} + category = {articles_equilibria} } @article{LemHow64, @@ -66,7 +66,7 @@ @article{LemHow64 volume = {12}, pages = {413--423}, year = {1964}, - category = {computation} + category = {articles_equilibria} } @article{Man64, @@ -76,7 +76,7 @@ @article{Man64 volume = {12}, pages = {778--780}, year = {1964}, - category = {computation} + category = {articles_equilibria} } @techreport{McK91, @@ -84,7 +84,7 @@ @techreport{McK91 title = {A {L}iapunov function for {N}ash equilibria}, institution = {California Institute of Technology}, year = {1991}, - category = {computation} + category = {articles_equilibria} } @incollection{McKMcL96, @@ -95,7 +95,7 @@ @incollection{McKMcL96 publisher = {Elsevier}, pages = {87--142}, year = {1996}, - category = {computation} + category = {articles_equilibria} } @article{Nau2004, @@ -105,7 +105,7 @@ @article{Nau2004 volume = {32}, pages = {443--453}, year = {2004}, - category = {computation} + category = {articles_equilibria} } @article{PNS04, @@ -115,7 +115,7 @@ @article{PNS04 volume = {63}, pages = {664--662}, year = {2004}, - category = {computation} + category = {articles_equilibria} } @article{Ros71, @@ -125,7 +125,7 @@ @article{Ros71 volume = {21}, pages = {73--79}, year = {1971}, - category = {computation} + category = {articles_equilibria} } @article{Sha74, @@ -135,7 +135,7 @@ @article{Sha74 volume = {1}, pages = {175--189}, year = {1974}, - category = {computation} + category = {articles_equilibria} } @article{Tur05, @@ -145,7 +145,7 @@ @article{Tur05 volume = {51}, pages = {243--263}, year = {2005}, - category = {computation} + category = {articles_equilibria} } @article{Tur10, @@ -155,7 +155,7 @@ @article{Tur10 volume = {42}, pages = {255--269}, year = {2010}, - category = {computation} + category = {articles_equilibria} } @article{VTH87, @@ -165,7 +165,7 @@ @article{VTH87 volume = {12}, pages = {377--397}, year = {1987}, - category = {computation} + category = {articles_equilibria} } @article{vSF08, @@ -175,7 +175,7 @@ @article{vSF08 volume = {33}, pages = {1002--1022}, year = {2008}, - category = {computation} + category = {articles_equilibria} } @article{Wil71, @@ -185,7 +185,7 @@ @article{Wil71 volume = {21}, pages = {80--87}, year = {1971}, - category = {computation} + category = {articles_equilibria} } @article{Yam93, @@ -195,7 +195,7 @@ @article{Yam93 volume = {22}, pages = {249--259}, year = {1993}, - category = {computation} + category = {articles_equilibria} } @article{Bag1995, @@ -205,7 +205,7 @@ @article{Bag1995 volume = {8}, pages = {271--280}, year = {1995}, - category = {general} + category = {articles_general} } @article{Gil97, @@ -215,7 +215,7 @@ @article{Gil97 volume = {20}, pages = {25--30}, year = {1997}, - category = {general} + category = {articles_general} } @article{Harsanyi1967a, @@ -225,7 +225,7 @@ @article{Harsanyi1967a volume = {14}, pages = {159--182}, year = {1967}, - category = {general} + category = {articles_general} } @article{Harsanyi1967b, @@ -235,7 +235,7 @@ @article{Harsanyi1967b volume = {14}, pages = {320--334}, year = {1967}, - category = {general} + category = {articles_general} } @article{Harsanyi1968, @@ -245,7 +245,7 @@ @article{Harsanyi1968 volume = {14}, pages = {486--502}, year = {1968}, - category = {general} + category = {articles_general} } @inproceedings{JakSorCon16, @@ -254,7 +254,7 @@ @inproceedings{JakSorCon16 booktitle = {Proceedings of the Seventh Innovations in Theoretical Computer Science Conference}, pages = {191--199}, year = {2016}, - category = {general} + category = {articles_general} } @article{KreWil82, @@ -264,7 +264,7 @@ @article{KreWil82 volume = {50}, pages = {863--894}, year = {1982}, - category = {general} + category = {articles_general} } @book{Kre90, @@ -272,7 +272,7 @@ @book{Kre90 title = {A Course in Microeconomic Theory}, publisher = {Princeton University Press}, year = {1990}, - category = {general} + category = {articles_general} } @article{McKPal95, @@ -282,7 +282,7 @@ @article{McKPal95 volume = {10}, pages = {6--38}, year = {1995}, - category = {general} + category = {articles_general} } @article{McKPal98, @@ -292,7 +292,7 @@ @article{McKPal98 volume = {1}, pages = {9--41}, year = {1998}, - category = {general} + category = {articles_general} } @article{Mye78, @@ -302,7 +302,7 @@ @article{Mye78 volume = {7}, pages = {73--80}, year = {1978}, - category = {general} + category = {articles_general} } @article{Nas50, @@ -312,7 +312,7 @@ @article{Nas50 volume = {36}, pages = {48--49}, year = {1950}, - category = {general} + category = {articles_general} } @article{Och95, @@ -322,7 +322,7 @@ @article{Och95 volume = {10}, pages = {202--217}, year = {1995}, - category = {general} + category = {articles_general} } @article{Rei2008, @@ -332,7 +332,7 @@ @article{Rei2008 volume = {4}, pages = {323--341}, year = {2008}, - category = {general} + category = {articles_general} } @article{Sel75, @@ -342,7 +342,7 @@ @article{Sel75 volume = {4}, pages = {25--55}, year = {1975}, - category = {general} + category = {articles_general} } @book{vanD83, @@ -351,7 +351,7 @@ @book{vanD83 publisher = {Springer-Verlag}, address = {Berlin}, year = {1983}, - category = {general} + category = {articles_general} } @book{Mye91, From 6517b9024ce7748a3f07b8d072a60cf7024308e0 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Mon, 1 Jun 2026 13:46:34 +0100 Subject: [PATCH 33/38] original layout efs for figs 6 and 9 of vonstengelforges2008 --- build_support/catalog/catalog.am | 3 ++ .../fig6__Original_Layout.ef | 41 +++++++++++++++++++ .../fig9__Original_Layout.ef | 23 +++++++++++ 3 files changed, 67 insertions(+) create mode 100644 catalog/vonstengelforges2008/fig6__Original_Layout.ef create mode 100644 catalog/vonstengelforges2008/fig9__Original_Layout.ef diff --git a/build_support/catalog/catalog.am b/build_support/catalog/catalog.am index 8074da383..0fe2e1867 100644 --- a/build_support/catalog/catalog.am +++ b/build_support/catalog/catalog.am @@ -20,8 +20,11 @@ CATALOG_FILES = \ catalog/vonstengel2022/fig10.12.efg \ catalog/vonstengel2022/fig10.5.efg \ catalog/vonstengel2022/fig10.7.efg \ + catalog/vonstengelforges2008/f9-ff.ef \ catalog/vonstengelforges2008/fig1.efg \ catalog/vonstengelforges2008/fig6.efg \ + catalog/vonstengelforges2008/fig6__Original_Layout.ef \ catalog/vonstengelforges2008/fig9.efg \ + catalog/vonstengelforges2008/fig9__Original_Layout.ef \ catalog/watson2013/exercise29_6.efg \ catalog/watson2013/fig29_1.efg diff --git a/catalog/vonstengelforges2008/fig6__Original_Layout.ef b/catalog/vonstengelforges2008/fig6__Original_Layout.ef new file mode 100644 index 000000000..34da71a32 --- /dev/null +++ b/catalog/vonstengelforges2008/fig6__Original_Layout.ef @@ -0,0 +1,41 @@ +player 1 name 1 +player 2 name 2 +level 0 node 1 player 2 +level 1.5 node 1 xshift -a=3.5 from 0,1 move a +level 1.5 node 2 xshift a from 0,1 move b +iset 1.5,1 1.5,2 player 1 +% level 4 node 1 xshift -b=1.6 from 1.5,1 move L +level 2.75 node 1 xshift -b=1.6 from 1.5,1 move L +level 4 node 2 xshift 2b from 1.5,1 move R + +level 4 node 3 xshift -s=1 from 2.75,1 move S payoffs 0 +level 4 node 1 xshift 1.2 from 2.75,1 move T +% iset k +iset 4,1 4,2 player 2 + +level 5 node 1 xshift -c=.6 from 4,1 move c payoffs 0 +level 5 node 2 xshift c from 4,1 move d payoffs 0 +level 5 node 3 xshift -c from 4,2 move c payoffs 0 +level 6 node 1 xshift 2c from 4,2 move d +level 6 node 2 xshift 2r=.5 from 1.5,2 move R +iset 6,1 6,2 player 1 + +level 7 node 1 xshift -u=.6 from 6,1 move U payoffs 0 +level 7 node 2 xshift u from 6,1 move V payoffs 0 +level 7 node 3 xshift -u from 6,2 move U payoffs 0 + +level 8 node 1 xshift -3r from 1.5,2 move L +level 8 node 2 xshift 2u from 6,2 move V +iset 8,1 8,2 player 2 + +% level 4 node 4 xshift 1.02 from 1.5,2 move R +% level 6 node 4 xshift 1.02 from 4,4 move V + +level 10 node 1 xshift -7 from 8,1 move e +level 10 node 2 xshift f=0.7 from 8,1 move f payoffs 0 +level 10 node 3 xshift -f from 8,2 move e payoffs 0 +level 10 node 4 xshift f from 8,2 move f payoffs 0 +iset 2.75,1 10,1 player 1 + +level 11 node 1 xshift -s from 10,1 move S payoffs 0 +level 11 node 2 xshift s from 10,1 move T payoffs 0 diff --git a/catalog/vonstengelforges2008/fig9__Original_Layout.ef b/catalog/vonstengelforges2008/fig9__Original_Layout.ef new file mode 100644 index 000000000..d99f6df04 --- /dev/null +++ b/catalog/vonstengelforges2008/fig9__Original_Layout.ef @@ -0,0 +1,23 @@ +player 1 name 1 +player 2 name 2 +level 0 node 1 player 0 +level 2 node 1 player 2 xshift -a=3.5 from 0,1 move \frac{1}{3} +level 2 node 2 player 2 xshift -0.2 from 0,1 move \frac{1}{3} +level 2 node 3 player 2 xshift a from 0,1 move \frac{1}{3} +level 4 node 1 xshift 0 from 2,1 move x +level 4 node 2 xshift -b=.7 from 2,2 move -x +level 8 node 1 xshift 3b from 2,2 move y +level 6 node 1 xshift -c=.65 from 4,1 move x payoffs 1 1 +level 6 node 2 xshift c from 4,1 move -x payoffs 0 0 +level 6 node 3 xshift -c from 4,2 move x payoffs 0 0 +level 6 node 4 xshift c from 4,2 move -x payoffs 1 1 +level 10 node 1 xshift -c from 8,1 move y payoffs 1 1 +level 10 node 2 xshift c from 8,1 move -y payoffs 0 0 +level 4 node 3 xshift -b from 2,3 move -x +level 8 node 2 xshift 3b from 2,3 move -y +level 6 node 5 xshift -c from 4,3 move x payoffs 0 0 +level 6 node 6 xshift c from 4,3 move -x payoffs 1 1 +level 10 node 3 xshift -c from 8,2 move y payoffs 0 0 +level 10 node 4 xshift c from 8,2 move -y payoffs 1 1 +iset 8,1 8,2 player 1 +iset 4,1 4,2 4,3 player 1 From af21819f85095243df2ad83a032d7fe175c59dc4 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 1 Jun 2026 13:50:07 +0100 Subject: [PATCH 34/38] chore: remove f9-ff.ef from catalog build support files --- build_support/catalog/catalog.am | 1 - 1 file changed, 1 deletion(-) diff --git a/build_support/catalog/catalog.am b/build_support/catalog/catalog.am index 0fe2e1867..d65083cd9 100644 --- a/build_support/catalog/catalog.am +++ b/build_support/catalog/catalog.am @@ -20,7 +20,6 @@ CATALOG_FILES = \ catalog/vonstengel2022/fig10.12.efg \ catalog/vonstengel2022/fig10.5.efg \ catalog/vonstengel2022/fig10.7.efg \ - catalog/vonstengelforges2008/f9-ff.ef \ catalog/vonstengelforges2008/fig1.efg \ catalog/vonstengelforges2008/fig6.efg \ catalog/vonstengelforges2008/fig6__Original_Layout.ef \ From e335fbb5c7904e02ecee2257956ee563260471e6 Mon Sep 17 00:00:00 2001 From: rahulsavani Date: Tue, 2 Jun 2026 12:41:26 +0100 Subject: [PATCH 35/38] shapley1974 --- build_support/catalog/catalog.am | 2 ++ catalog/shapley1974/fig2.nfg | 19 +++++++++++++++++++ catalog/shapley1974/fig3.nfg | 23 +++++++++++++++++++++++ doc/references.bib | 12 ++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 catalog/shapley1974/fig2.nfg create mode 100644 catalog/shapley1974/fig3.nfg diff --git a/build_support/catalog/catalog.am b/build_support/catalog/catalog.am index d65083cd9..2a0a8a4e5 100644 --- a/build_support/catalog/catalog.am +++ b/build_support/catalog/catalog.am @@ -16,6 +16,8 @@ CATALOG_FILES = \ catalog/selten1975/fig1.efg \ catalog/selten1975/fig2.efg \ catalog/selten1975/fig3.efg \ + catalog/shapley1974/fig2.nfg \ + catalog/shapley1974/fig3.nfg \ catalog/vonstengel2022/fig10.1.efg \ catalog/vonstengel2022/fig10.12.efg \ catalog/vonstengel2022/fig10.5.efg \ diff --git a/catalog/shapley1974/fig2.nfg b/catalog/shapley1974/fig2.nfg new file mode 100644 index 000000000..02d31ffbc --- /dev/null +++ b/catalog/shapley1974/fig2.nfg @@ -0,0 +1,19 @@ +NFG 1 R "Fig 2 from 'A Note on the Lemke-Howson Algorithm' (Shapley 1974)" { "1" "2" } + +{ { "1" "2" "3" } +{ "1" "2" "3" } +} +"Fig 2 from :cite:p:`Shap74`. This bimatrix game is used to demonstrate the Lemke-Howson algorithm." + +{ +{ "" 2, 3 } +{ "" 0, 0 } +{ "" 3, 0 } +{ "" 2, 0 } +{ "" 3, 3 } +{ "" 0, 0 } +{ "" 0, 2 } +{ "" 0, 2 } +{ "" 1, 1 } +} +1 2 3 4 5 6 7 8 9 diff --git a/catalog/shapley1974/fig3.nfg b/catalog/shapley1974/fig3.nfg new file mode 100644 index 000000000..d82373984 --- /dev/null +++ b/catalog/shapley1974/fig3.nfg @@ -0,0 +1,23 @@ +NFG 1 R "Fig 3 from 'A Note on the Lemke-Howson Algorithm' (Shapley 1974)" { "1" "2" } + +{ { "1" "2" "3" } +{ "1" "2" "3" } +} +"Fig 3 from :cite:p:`Shap74`. This bimatrix game has a pair of mixed-strategy equilibria +that are inaccessible to the Lemke-Howson algorithm. That is, the two equilibria in this +pair are connected to each other for all dropped labels, and can thus not be reached by +any concatenation of paths from the artifical equilibrium, where the Lemke-Howson algorithm +starts." + +{ +{ "" 0, 0 } +{ "" 2, 3 } +{ "" 3, 0 } +{ "" 3, 2 } +{ "" 2, 2 } +{ "" 0, 0 } +{ "" 0, 3 } +{ "" 0, 0 } +{ "" 1, 1 } +} +1 2 3 4 5 6 7 8 9 diff --git a/doc/references.bib b/doc/references.bib index 8d9171a00..6e2c96b29 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -389,3 +389,15 @@ @book{Wat13 year = {2013}, category = {textbooks} } + + +@inbook{Shap74, + author = {Shapley, Lloyd S.}, + editor = {Balinski, M. L.}, + title = {A note on the Lemke-Howson algorithm}, + booktitle = {Pivoting and Extension: In honor of A.W. Tucker}, + year = {1974}, + publisher = {Springer Berlin Heidelberg}, + pages = {175--189}, + category = {articles_equilibria} +} From d35e1170ac3c3ad5093996d2e4be0d2a7d742991 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 2 Jun 2026 15:05:41 +0100 Subject: [PATCH 36/38] docs: update Shapley author format and add inbook template support to bibliography processor --- doc/conf.py | 3 +++ doc/references.bib | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 7d2f1aec9..18796ca96 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -156,6 +156,9 @@ def get_incollection_template(self, e): 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[ diff --git a/doc/references.bib b/doc/references.bib index 6e2c96b29..312e7a692 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -392,7 +392,7 @@ @book{Wat13 @inbook{Shap74, - author = {Shapley, Lloyd S.}, + author = {Shapley, L. S.}, editor = {Balinski, M. L.}, title = {A note on the Lemke-Howson algorithm}, booktitle = {Pivoting and Extension: In honor of A.W. Tucker}, From 77ef0113f8cbe604852b90762bec52e3c820a38d Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 2 Jun 2026 15:20:04 +0100 Subject: [PATCH 37/38] De-duplicate Shap74 in bib --- doc/algorithms.rst | 4 ++-- doc/references.bib | 13 ++----------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/doc/algorithms.rst b/doc/algorithms.rst index 058262814..0f4b33260 100644 --- a/doc/algorithms.rst +++ b/doc/algorithms.rst @@ -38,7 +38,7 @@ Computes Nash equilibria using extreme point enumeration. In a two-player strategic game, the set of Nash equilibria can be expressed as the union of convex sets. This program generates all the extreme points of those convex sets. (Mangasarian :cite:p:`Man64`) This is a superset of the points generated by the path-following procedure of Lemke and Howson (see :ref:`lcp`). -It was shown by Shapley :cite:p:`Sha74` that there are equilibria not accessible via the method in :ref:`lcp`, whereas the output of +It was shown by Shapley :cite:p:`Shap74` that there are equilibria not accessible via the method in :ref:`lcp`, whereas the output of :program:`enummixed` is guaranteed to return all the extreme points. .. _enumpoly: @@ -111,7 +111,7 @@ For strategic games, the program uses the method of Lemke and Howson equilibria, i.e., those that can be found as concatenations of Lemke-Howson paths that start at the artificial equilibrium. There exist strategic-form games for which some equilibria cannot be found -by this method, i.e., some equilibria are inaccessible; see Shapley :cite:p:`Sha74`. +by this method, i.e., some equilibria are inaccessible; see Shapley :cite:p:`Shap74`. In a two-player strategic game, the set of Nash equilibria can be expressed as the union of convex sets. This program will find extreme points diff --git a/doc/references.bib b/doc/references.bib index 312e7a692..a7453ca1e 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -128,16 +128,6 @@ @article{Ros71 category = {articles_equilibria} } -@article{Sha74, - author = {Shapley, L.}, - title = {A note on the {L}emke-{H}owson algorithm}, - journal = {Mathematical Programming Study}, - volume = {1}, - pages = {175--189}, - year = {1974}, - category = {articles_equilibria} -} - @article{Tur05, author = {Turocy, T. L.}, title = {A dynamic homotopy interpretation of the logistic quantal response equilibrium correspondence}, @@ -395,7 +385,8 @@ @inbook{Shap74 author = {Shapley, L. S.}, editor = {Balinski, M. L.}, title = {A note on the Lemke-Howson algorithm}, - booktitle = {Pivoting and Extension: In honor of A.W. Tucker}, + booktitle = {Pivoting and Extension: Mathematical Programming Studies}, + volume = {1}, year = {1974}, publisher = {Springer Berlin Heidelberg}, pages = {175--189}, From 208fb89429074d751badd4e9bef80400274ec6fe Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 2 Jun 2026 15:21:12 +0100 Subject: [PATCH 38/38] revert unnecessary func --- doc/conf.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 18796ca96..7d2f1aec9 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -156,9 +156,6 @@ def get_incollection_template(self, e): 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[