From 90a5e367095e2b842374dc0de491f7829b17f724 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 3 Feb 2026 10:11:54 +0000 Subject: [PATCH 001/119] Create catalog dir and move one EFG and one NFG into it from contrib/games --- {contrib/games => catalog}/2smp.efg | 0 {contrib/games => catalog}/pd.nfg | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {contrib/games => catalog}/2smp.efg (100%) rename {contrib/games => catalog}/pd.nfg (100%) diff --git a/contrib/games/2smp.efg b/catalog/2smp.efg similarity index 100% rename from contrib/games/2smp.efg rename to catalog/2smp.efg diff --git a/contrib/games/pd.nfg b/catalog/pd.nfg similarity index 100% rename from contrib/games/pd.nfg rename to catalog/pd.nfg From 83dd41034563bf808001be8cde74bece58a315fb Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 3 Feb 2026 10:17:38 +0000 Subject: [PATCH 002/119] ignore catalog files copied into pygambit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9d37e0d3b..85c959981 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ Gambit.app/* *.ef build_support/msw/gambit.wxs build_support/osx/Info.plist +src/pygambit/catalog From 7fc21994294b10d09b2bc82ca3dedcb84ef26afe Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 3 Feb 2026 10:25:18 +0000 Subject: [PATCH 003/119] add failing tests --- tests/test_catalog.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/test_catalog.py diff --git a/tests/test_catalog.py b/tests/test_catalog.py new file mode 100644 index 000000000..6b8888072 --- /dev/null +++ b/tests/test_catalog.py @@ -0,0 +1,11 @@ +import pygambit as gbt + + +def test_catalog_load_efg(): + g = gbt.catalog.load("2smp") + assert isinstance(g, gbt.Game) + + +def test_catalog_load_nfg(): + g = gbt.catalog.load("pd") + assert isinstance(g, gbt.Game) From 3c53beaefce05ca9236bc713a2286bb537705532 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 3 Feb 2026 10:39:07 +0000 Subject: [PATCH 004/119] improve tests --- tests/test_catalog.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 6b8888072..95c3ededa 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -1,11 +1,26 @@ +import pandas as pd + import pygambit as gbt def test_catalog_load_efg(): + """Test loading an extensive form game""" g = gbt.catalog.load("2smp") assert isinstance(g, gbt.Game) + assert g.title == "Two-stage matching pennies game" def test_catalog_load_nfg(): + """Test loading a normal form game""" g = gbt.catalog.load("pd") assert isinstance(g, gbt.Game) + assert g.title == "Two person Prisoner's Dilemma game" + + +def test_catalog_games(): + """Test games() function returns df of game slugs and titles""" + all_games = gbt.catalog.games() + assert isinstance(all_games, pd.DataFrame) + assert len(all_games) > 0 + assert "2smp" in list(all_games.slug) + assert "Two-stage matching pennies game" in list(all_games.title) From 898a916d38c2056e1418863e12250f44acfa3c78 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 3 Feb 2026 10:39:24 +0000 Subject: [PATCH 005/119] add pandas to pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 71ea25bfc..36dfb494b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers=[ dependencies = [ "numpy", "scipy", + "pandas", ] [project.urls] From 8f916db6b0eef5ec03f3ca8afc9d4390c17da3e4 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 3 Feb 2026 11:02:19 +0000 Subject: [PATCH 006/119] add test_catalog_load_invalid_slug --- tests/test_catalog.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 95c3ededa..2564b1209 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -1,4 +1,5 @@ import pandas as pd +import pytest import pygambit as gbt @@ -17,6 +18,12 @@ def test_catalog_load_nfg(): assert g.title == "Two person Prisoner's Dilemma game" +def test_catalog_load_invalid_slug(): + """Test loading an invalid game slug""" + with pytest.raises(FileNotFoundError): + gbt.catalog.load("invalid_slug") + + def test_catalog_games(): """Test games() function returns df of game slugs and titles""" all_games = gbt.catalog.games() From 606dd8aed8424cd17486537d94926317cec83deb Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 3 Feb 2026 11:32:32 +0000 Subject: [PATCH 007/119] create load function --- src/pygambit/catalog.py | 46 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/pygambit/catalog.py diff --git a/src/pygambit/catalog.py b/src/pygambit/catalog.py new file mode 100644 index 000000000..565ea6b33 --- /dev/null +++ b/src/pygambit/catalog.py @@ -0,0 +1,46 @@ +from importlib.resources import files + +import pygambit as gbt + +_GAMEFILES_DIR = files(__package__) / "catalog" + + +def load(slug: str) -> gbt.Game: + """ + Load a game from the package catalog. + + The function looks for a catalog entry matching the given ``slug`` in the + ``catalog`` resource directory. Files are tried in the following order: + + 1. ``.nfg`` (normal-form game) + 2. ``.efg`` (extensive-form game) + + The first matching file found is loaded and returned as a + :class:`pygambit.Game`. + + Parameters + ---------- + slug : str + Base name of the catalog entry, without file extension. + + Returns + ------- + pygambit.Game + The loaded game. + + Raises + ------ + FileNotFoundError + If no ``.nfg`` or ``.efg`` file exists for the given slug. + """ + candidates = { + ".nfg": gbt.read_nfg, + ".efg": gbt.read_efg, + } + + for suffix, reader in candidates.items(): + path = _GAMEFILES_DIR / f"{slug}{suffix}" + if path.is_file(): + return reader(str(path)) + + raise FileNotFoundError(f"No catalog entry called {slug}.nfg or {slug}.efg") From 00beed38fd6897088b0b8172d4cdba6ce3028dd8 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 3 Feb 2026 11:32:55 +0000 Subject: [PATCH 008/119] add catalog to __init__ --- src/pygambit/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pygambit/__init__.py b/src/pygambit/__init__.py index 1d6f730bf..8b72c0682 100644 --- a/src/pygambit/__init__.py +++ b/src/pygambit/__init__.py @@ -26,6 +26,7 @@ nash, # noqa: F401 qre, # noqa: F401 supports, # noqa: F401 + catalog, ) import importlib.metadata From 64eb3106c8a7790643da15d54fca9579937b438a Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 3 Feb 2026 11:43:15 +0000 Subject: [PATCH 009/119] add games() function --- src/pygambit/catalog.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/pygambit/catalog.py b/src/pygambit/catalog.py index 565ea6b33..da1284af6 100644 --- a/src/pygambit/catalog.py +++ b/src/pygambit/catalog.py @@ -1,5 +1,7 @@ from importlib.resources import files +import pandas as pd + import pygambit as gbt _GAMEFILES_DIR = files(__package__) / "catalog" @@ -44,3 +46,41 @@ def load(slug: str) -> gbt.Game: return reader(str(path)) raise FileNotFoundError(f"No catalog entry called {slug}.nfg or {slug}.efg") + + +def games() -> pd.DataFrame: + """ + List games available in the package catalog. + + Iterates over ``.nfg`` and ``.efg`` files found in the catalog resource + directory, loads each game, and returns a pandas DataFrame summarising + the results. + + The returned DataFrame has two columns: + - ``slug``: the filename without its extension + - ``title``: the game's ``title`` attribute + + Returns + ------- + pandas.DataFrame + A DataFrame with columns ``slug`` and ``title``. + """ + records: list[dict[str, str]] = [] + + readers = { + ".nfg": gbt.read_nfg, + ".efg": gbt.read_efg, + } + + for path in sorted(_GAMEFILES_DIR.iterdir()): + reader = readers.get(path.suffix) + if reader is not None and path.is_file(): + game = reader(str(path)) + records.append( + { + "slug": path.stem, + "title": game.title, + } + ) + + return pd.DataFrame.from_records(records, columns=["slug", "title"]) From d24befd117fdde8bbd415639a95999c368a83e98 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 3 Feb 2026 11:44:33 +0000 Subject: [PATCH 010/119] refactor to define READERS once --- src/pygambit/catalog.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/pygambit/catalog.py b/src/pygambit/catalog.py index da1284af6..8c1736ce3 100644 --- a/src/pygambit/catalog.py +++ b/src/pygambit/catalog.py @@ -5,6 +5,10 @@ import pygambit as gbt _GAMEFILES_DIR = files(__package__) / "catalog" +READERS = { + ".nfg": gbt.read_nfg, + ".efg": gbt.read_efg, +} def load(slug: str) -> gbt.Game: @@ -35,12 +39,7 @@ def load(slug: str) -> gbt.Game: FileNotFoundError If no ``.nfg`` or ``.efg`` file exists for the given slug. """ - candidates = { - ".nfg": gbt.read_nfg, - ".efg": gbt.read_efg, - } - - for suffix, reader in candidates.items(): + for suffix, reader in READERS.items(): path = _GAMEFILES_DIR / f"{slug}{suffix}" if path.is_file(): return reader(str(path)) @@ -67,13 +66,8 @@ def games() -> pd.DataFrame: """ records: list[dict[str, str]] = [] - readers = { - ".nfg": gbt.read_nfg, - ".efg": gbt.read_efg, - } - for path in sorted(_GAMEFILES_DIR.iterdir()): - reader = readers.get(path.suffix) + reader = READERS.get(path.suffix) if reader is not None and path.is_file(): game = reader(str(path)) records.append( From 72aade140ff347ad53530cddeee2cfe0e52c3753 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 3 Feb 2026 16:24:23 +0000 Subject: [PATCH 011/119] Big refactor to get catalog files from catalog dir external to pygambit --- MANIFEST.in | 1 + catalog/__init__.py | 51 ++++++++++++++++++++++++++ pyproject.toml | 7 ++++ setup.py | 2 -- src/pygambit/catalog.py | 80 ----------------------------------------- 5 files changed, 59 insertions(+), 82 deletions(-) create mode 100644 catalog/__init__.py delete mode 100644 src/pygambit/catalog.py diff --git a/MANIFEST.in b/MANIFEST.in index d1d71b9a6..69fed7e1d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ recursive-include src/core *.cc *.h *.imp recursive-include src/games *.cc *.h *.imp recursive-include src/solvers *.c *.cc *.h *.imp +recursive-include catalog * include src/gambit.h include src/pygambit/*.pxd include src/pygambit/*.pyx diff --git a/catalog/__init__.py b/catalog/__init__.py new file mode 100644 index 000000000..6e844fe40 --- /dev/null +++ b/catalog/__init__.py @@ -0,0 +1,51 @@ +from importlib.resources import as_file, files + +import pandas as pd + +import pygambit as gbt + +# Use the full string path to the virtual package we created +_CATALOG_RESOURCE = files(__name__) + +READERS = { + ".nfg": gbt.read_nfg, + ".efg": gbt.read_efg, +} + + +def load(slug: str) -> gbt.Game: + """ + Load a game from the package catalog. + """ + for suffix, reader in READERS.items(): + resource_path = _CATALOG_RESOURCE / f"{slug}{suffix}" + + if resource_path.is_file(): + # as_file ensures we have a real filesystem path for the reader + with as_file(resource_path) as path: + return reader(str(path)) + + raise FileNotFoundError(f"No catalog entry called {slug}.nfg or {slug}.efg") + + +def games() -> pd.DataFrame: + """ + List games available in the package catalog. + """ + records: list[dict[str, str]] = [] + + # iterdir() works directly on the Traversable object + for resource_path in sorted(_CATALOG_RESOURCE.iterdir()): + reader = READERS.get(resource_path.suffix) + + if reader is not None and resource_path.is_file(): + with as_file(resource_path) as path: + game = reader(str(path)) + records.append( + { + "slug": resource_path.stem, + "title": game.title, + } + ) + + return pd.DataFrame.from_records(records, columns=["slug", "title"]) diff --git a/pyproject.toml b/pyproject.toml index 36dfb494b..270f77d06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,5 +88,12 @@ markers = [ "slow: all time-consuming tests", ] +[tool.setuptools] +packages = ["pygambit", "pygambit.catalog"] +package-dir = { "pygambit" = "src/pygambit", "pygambit.catalog" = "catalog" } + +[tool.setuptools.package-data] +"pygambit.catalog" = ["*"] + [tool.setuptools.dynamic] version = {file = "build_support/GAMBIT_VERSION"} diff --git a/setup.py b/setup.py index c8e7125f5..4a1e82b5a 100644 --- a/setup.py +++ b/setup.py @@ -103,8 +103,6 @@ def solver_library_config(library_name: str, paths: list) -> tuple: libraries=[cppgambit_bimatrix, cppgambit_liap, cppgambit_logit, cppgambit_simpdiv, cppgambit_gtracer, cppgambit_enumpoly, cppgambit_games, cppgambit_core], - package_dir={"": "src"}, - packages=["pygambit"], ext_modules=Cython.Build.cythonize(libgambit, language_level="3str", compiler_directives={"binding": True}) diff --git a/src/pygambit/catalog.py b/src/pygambit/catalog.py deleted file mode 100644 index 8c1736ce3..000000000 --- a/src/pygambit/catalog.py +++ /dev/null @@ -1,80 +0,0 @@ -from importlib.resources import files - -import pandas as pd - -import pygambit as gbt - -_GAMEFILES_DIR = files(__package__) / "catalog" -READERS = { - ".nfg": gbt.read_nfg, - ".efg": gbt.read_efg, -} - - -def load(slug: str) -> gbt.Game: - """ - Load a game from the package catalog. - - The function looks for a catalog entry matching the given ``slug`` in the - ``catalog`` resource directory. Files are tried in the following order: - - 1. ``.nfg`` (normal-form game) - 2. ``.efg`` (extensive-form game) - - The first matching file found is loaded and returned as a - :class:`pygambit.Game`. - - Parameters - ---------- - slug : str - Base name of the catalog entry, without file extension. - - Returns - ------- - pygambit.Game - The loaded game. - - Raises - ------ - FileNotFoundError - If no ``.nfg`` or ``.efg`` file exists for the given slug. - """ - for suffix, reader in READERS.items(): - path = _GAMEFILES_DIR / f"{slug}{suffix}" - if path.is_file(): - return reader(str(path)) - - raise FileNotFoundError(f"No catalog entry called {slug}.nfg or {slug}.efg") - - -def games() -> pd.DataFrame: - """ - List games available in the package catalog. - - Iterates over ``.nfg`` and ``.efg`` files found in the catalog resource - directory, loads each game, and returns a pandas DataFrame summarising - the results. - - The returned DataFrame has two columns: - - ``slug``: the filename without its extension - - ``title``: the game's ``title`` attribute - - Returns - ------- - pandas.DataFrame - A DataFrame with columns ``slug`` and ``title``. - """ - records: list[dict[str, str]] = [] - - for path in sorted(_GAMEFILES_DIR.iterdir()): - reader = READERS.get(path.suffix) - if reader is not None and path.is_file(): - game = reader(str(path)) - records.append( - { - "slug": path.stem, - "title": game.title, - } - ) - - return pd.DataFrame.from_records(records, columns=["slug", "title"]) From e7953e96efbe5582eb629d4b187eff74317bd05b Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 3 Feb 2026 16:45:17 +0000 Subject: [PATCH 012/119] update Makefile.am for the 2 examples we moved into the catalog so far --- Makefile.am | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile.am b/Makefile.am index eccc92c12..7b88649ac 100644 --- a/Makefile.am +++ b/Makefile.am @@ -101,7 +101,6 @@ EXTRA_DIST = \ src/gui/bitmaps/zoom1.xpm \ src/gui/bitmaps/gambitrc.rc \ contrib/games/2s2x2x2.efg \ - contrib/games/2smp.efg \ contrib/games/2x2x2.efg \ contrib/games/4cards.efg \ contrib/games/artist1.efg \ @@ -224,7 +223,6 @@ EXTRA_DIST = \ contrib/games/mixdom2.nfg \ contrib/games/mixdom.nfg \ contrib/games/oneill.nfg \ - contrib/games/pd.nfg \ contrib/games/perfect1.nfg \ contrib/games/perfect2.nfg \ contrib/games/perfect3.nfg \ @@ -240,7 +238,9 @@ EXTRA_DIST = \ contrib/games/winkels.nfg \ contrib/games/yamamoto.nfg \ contrib/games/zero.nfg \ - src/README.rst + src/README.rst \ + catalog/2smp.efg \ + catalog/pd.nfg core_SOURCES = \ src/core/core.h \ From 7490ae6bbe20601aff1fba435d7d852341e1d8a7 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 09:43:36 +0000 Subject: [PATCH 013/119] update Game.comment to be Game.description in Python code --- doc/pygambit.api.rst | 2 +- src/pygambit/game.pxi | 10 +++++----- tests/test_extensive.py | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index ae029bae7..7568cfd5a 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -95,7 +95,7 @@ Information about the game :toctree: api/ Game.title - Game.comment + Game.description Game.is_const_sum Game.is_tree Game.is_perfect_recall diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 2fbde8186..403eb9f98 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -661,16 +661,16 @@ class Game: self.game.deref().SetTitle(value.encode("ascii")) @property - def comment(self) -> str: - """Get or set the comment of the game. + def description(self) -> str: + """Get or set the description of the game. - A game's comment is an arbitrary string, and may be more discursive + A game's description/comment is an arbitrary string, and may be more discursive than a title. """ return self.game.deref().GetComment().decode("ascii") - @comment.setter - def comment(self, value: str) -> None: + @description.setter + def description(self, value: str) -> None: self.game.deref().SetComment(value.encode("ascii")) @property diff --git a/tests/test_extensive.py b/tests/test_extensive.py index 41528afbe..2695e473d 100644 --- a/tests/test_extensive.py +++ b/tests/test_extensive.py @@ -25,12 +25,12 @@ def test_game_title(title: str): @pytest.mark.parametrize( - "comment", ["This is a comment describing the game in more detail"] + "description", ["This is a description of the game with more detail"] ) -def test_game_comment(comment: str): +def test_game_description(description: str): game = gbt.Game.new_tree() - game.comment = comment - assert game.comment == comment + game.description = description + assert game.description == description @pytest.mark.parametrize("players", [["Alice"], ["Oscar", "Felix"]]) From ebf6829e83e2bf0f0fa2adc7e5848f35c87b89c6 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 09:47:10 +0000 Subject: [PATCH 014/119] Revert "update Game.comment to be Game.description in Python code" This reverts commit 7490ae6bbe20601aff1fba435d7d852341e1d8a7. --- doc/pygambit.api.rst | 2 +- src/pygambit/game.pxi | 10 +++++----- tests/test_extensive.py | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index 7568cfd5a..ae029bae7 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -95,7 +95,7 @@ Information about the game :toctree: api/ Game.title - Game.description + Game.comment Game.is_const_sum Game.is_tree Game.is_perfect_recall diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 403eb9f98..2fbde8186 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -661,16 +661,16 @@ class Game: self.game.deref().SetTitle(value.encode("ascii")) @property - def description(self) -> str: - """Get or set the description of the game. + def comment(self) -> str: + """Get or set the comment of the game. - A game's description/comment is an arbitrary string, and may be more discursive + A game's comment is an arbitrary string, and may be more discursive than a title. """ return self.game.deref().GetComment().decode("ascii") - @description.setter - def description(self, value: str) -> None: + @comment.setter + def comment(self, value: str) -> None: self.game.deref().SetComment(value.encode("ascii")) @property diff --git a/tests/test_extensive.py b/tests/test_extensive.py index 2695e473d..41528afbe 100644 --- a/tests/test_extensive.py +++ b/tests/test_extensive.py @@ -25,12 +25,12 @@ def test_game_title(title: str): @pytest.mark.parametrize( - "description", ["This is a description of the game with more detail"] + "comment", ["This is a comment describing the game in more detail"] ) -def test_game_description(description: str): +def test_game_comment(comment: str): game = gbt.Game.new_tree() - game.description = description - assert game.description == description + game.comment = comment + assert game.comment == comment @pytest.mark.parametrize("players", [["Alice"], ["Oscar", "Felix"]]) From fbf6b89967c1a89b9e57f89d66960687904bad32 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 11:47:55 +0000 Subject: [PATCH 015/119] Add initial update catalog script and RST page --- .gitignore | 1 + doc/catalog.rst | 8 ++++++++ doc/index.rst | 1 + src/pygambit/update_catalog.py | 7 +++++++ 4 files changed, 17 insertions(+) create mode 100644 doc/catalog.rst create mode 100644 src/pygambit/update_catalog.py diff --git a/.gitignore b/.gitignore index 85c959981..f8be35fcf 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ Gambit.app/* build_support/msw/gambit.wxs build_support/osx/Info.plist src/pygambit/catalog +doc/catalog.csv diff --git a/doc/catalog.rst b/doc/catalog.rst new file mode 100644 index 000000000..cf8d45e2c --- /dev/null +++ b/doc/catalog.rst @@ -0,0 +1,8 @@ +Catalog of games +================ + +.. csv-table:: + :file: catalog.csv + :header-rows: 1 + :widths: 20, 80 + :class: tight-table diff --git a/doc/index.rst b/doc/index.rst index 79076153a..72843860d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -64,6 +64,7 @@ We recommended most new users install the PyGambit Python package and read the a pygambit tools gui + catalog samples developer formats diff --git a/src/pygambit/update_catalog.py b/src/pygambit/update_catalog.py new file mode 100644 index 000000000..e1c7d098c --- /dev/null +++ b/src/pygambit/update_catalog.py @@ -0,0 +1,7 @@ +import pygambit as gbt + +DOC = "../../doc" + +if __name__ == "__main__": + # Create CSV used by RST docs page + gbt.catalog.games().to_csv(DOC + "/catalog.csv", index=False) From 1ca3461549b7ed10f587f0ff20a02f07451af28c Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 11:53:24 +0000 Subject: [PATCH 016/119] rename table headers on output df from game() func --- catalog/__init__.py | 6 +++--- tests/test_catalog.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index 6e844fe40..4164041f5 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -43,9 +43,9 @@ def games() -> pd.DataFrame: game = reader(str(path)) records.append( { - "slug": resource_path.stem, - "title": game.title, + "Game": resource_path.stem, + "Title": game.title, } ) - return pd.DataFrame.from_records(records, columns=["slug", "title"]) + return pd.DataFrame.from_records(records, columns=["Game", "Title"]) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 2564b1209..f281cc353 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -29,5 +29,5 @@ def test_catalog_games(): all_games = gbt.catalog.games() assert isinstance(all_games, pd.DataFrame) assert len(all_games) > 0 - assert "2smp" in list(all_games.slug) - assert "Two-stage matching pennies game" in list(all_games.title) + assert "2smp" in list(all_games.Game) + assert "Two-stage matching pennies game" in list(all_games.Title) From bd3c1f3646bc908805b100e116e9246d25a460f5 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 11:58:17 +0000 Subject: [PATCH 017/119] add generating the catalog csv for docs into GH actions --- .github/workflows/python.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 5e23dff61..1fe0f6f6d 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -31,6 +31,10 @@ jobs: run: | cd dist pip install -v pygambit*.tar.gz + - name: Generate Catalog CSV for docs + run: | + cd src/pygambit + python update_catalog.py - name: Run tests run: pytest --run-tutorials @@ -56,6 +60,10 @@ jobs: - name: Build extension run: | python -m pip install -v . + - name: Generate Catalog CSV for docs + run: | + cd src/pygambit + python update_catalog.py - name: Run tests run: pytest --run-tutorials @@ -81,6 +89,10 @@ jobs: - name: Build extension run: | python -m pip install -v . + - name: Generate Catalog CSV for docs + run: | + cd src/pygambit + python update_catalog.py - name: Run tests run: pytest --run-tutorials @@ -106,5 +118,9 @@ jobs: - name: Build extension run: | python -m pip install -v . + - name: Generate Catalog CSV for docs + run: | + cd src/pygambit + python update_catalog.py - name: Run tests run: pytest --run-tutorials From 569148c071f834a7430c2b86d8b37b55ee502fbd Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 13:48:59 +0000 Subject: [PATCH 018/119] Revert "add generating the catalog csv for docs into GH actions" This reverts commit bd3c1f3646bc908805b100e116e9246d25a460f5. --- .github/workflows/python.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 1fe0f6f6d..5e23dff61 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -31,10 +31,6 @@ jobs: run: | cd dist pip install -v pygambit*.tar.gz - - name: Generate Catalog CSV for docs - run: | - cd src/pygambit - python update_catalog.py - name: Run tests run: pytest --run-tutorials @@ -60,10 +56,6 @@ jobs: - name: Build extension run: | python -m pip install -v . - - name: Generate Catalog CSV for docs - run: | - cd src/pygambit - python update_catalog.py - name: Run tests run: pytest --run-tutorials @@ -89,10 +81,6 @@ jobs: - name: Build extension run: | python -m pip install -v . - - name: Generate Catalog CSV for docs - run: | - cd src/pygambit - python update_catalog.py - name: Run tests run: pytest --run-tutorials @@ -118,9 +106,5 @@ jobs: - name: Build extension run: | python -m pip install -v . - - name: Generate Catalog CSV for docs - run: | - cd src/pygambit - python update_catalog.py - name: Run tests run: pytest --run-tutorials From 8cf0396977b3e148ebfc444126ff65de73c31ede Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 13:52:17 +0000 Subject: [PATCH 019/119] refactor update script so its run from repo root --- src/pygambit/update_catalog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pygambit/update_catalog.py b/src/pygambit/update_catalog.py index e1c7d098c..f4b776093 100644 --- a/src/pygambit/update_catalog.py +++ b/src/pygambit/update_catalog.py @@ -1,7 +1,7 @@ import pygambit as gbt -DOC = "../../doc" +CATALOG_CSV = "doc/catalog.csv" if __name__ == "__main__": # Create CSV used by RST docs page - gbt.catalog.games().to_csv(DOC + "/catalog.csv", index=False) + gbt.catalog.games().to_csv(CATALOG_CSV, index=False) From 1b20438eac5474bdb521e12d34a1b55afc666ac7 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 13:54:46 +0000 Subject: [PATCH 020/119] update readthedocs to build the catalog csv before docs build --- .readthedocs.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 73522dfdf..5cc214ac8 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -13,6 +13,10 @@ build: - libgmp-dev - pandoc - texlive-full + jobs: + # Create CSV for catalog table in docs + post_install: + - python src/pygambit/update_catalog.py python: install: From e977e64dab2dbe3ab974a62e1027f4c58c9aa5e8 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 14:25:18 +0000 Subject: [PATCH 021/119] add function which updates Makefile.am --- src/pygambit/update_catalog.py | 56 ++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/pygambit/update_catalog.py b/src/pygambit/update_catalog.py index f4b776093..6d4c59b2d 100644 --- a/src/pygambit/update_catalog.py +++ b/src/pygambit/update_catalog.py @@ -1,7 +1,59 @@ +from pathlib import Path + import pygambit as gbt -CATALOG_CSV = "doc/catalog.csv" +CATALOG_CSV = "doc/catalog.csv" # Relative to where script run from (root) +MAKEFILE_AM = Path(__file__).parent.parent.parent / "Makefile.am" +ALL_GAMES = gbt.catalog.games() +CATALOG_DIR = Path(__file__).parent.parent.parent / "catalog" +efg_files = list(CATALOG_DIR.rglob("*.efg")) +nfg_files = list(CATALOG_DIR.rglob("*.nfg")) + + +def update_makefile(): + """Update the Makefile.am with all games from the catalog.""" + + game_files = [] + for entry in efg_files + nfg_files: + filename = str(entry).split("/")[-1] + game_files.append(f"catalog/{filename}") + game_files.sort() + + with open(MAKEFILE_AM, encoding="utf-8") as f: + content = f.readlines() + + with open(MAKEFILE_AM, "w", encoding="utf-8") as f: + in_gamefiles_section = False + for line in content: + # Add to the EXTRA_DIST after the README.rst line + if line.startswith(" src/README.rst \\"): + in_gamefiles_section = True + f.write(" src/README.rst \\\n") + for gf in game_files: + if gf == game_files[-1]: + f.write(f"\t{gf}\n") + else: + f.write(f"\t{gf} \\\n") + f.write("\n") + elif in_gamefiles_section: + if line.strip() == "": + in_gamefiles_section = False + continue # Skip old gamefiles lines + else: + f.write(line) + + with open(MAKEFILE_AM, encoding="utf-8") as f: + updated_content = f.readlines() + + if content != updated_content: + print(f"Updated {str(MAKEFILE_AM)}") + if __name__ == "__main__": + # Create CSV used by RST docs page - gbt.catalog.games().to_csv(CATALOG_CSV, index=False) + ALL_GAMES.to_csv(CATALOG_CSV, index=False) + print(f"Generated {CATALOG_CSV} for use in docs build.") + + # Update the Makefile.am with the current list of catalog files + update_makefile() From af08d29e0cb10d7dcdb5c27dc31f472951a50d6e Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 14:41:17 +0000 Subject: [PATCH 022/119] use a proper path for CATALOG_CSV --- src/pygambit/update_catalog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pygambit/update_catalog.py b/src/pygambit/update_catalog.py index 6d4c59b2d..2ab6c0e73 100644 --- a/src/pygambit/update_catalog.py +++ b/src/pygambit/update_catalog.py @@ -2,7 +2,7 @@ import pygambit as gbt -CATALOG_CSV = "doc/catalog.csv" # Relative to where script run from (root) +CATALOG_CSV = Path(__file__).parent.parent.parent / "doc" / "catalog.csv" MAKEFILE_AM = Path(__file__).parent.parent.parent / "Makefile.am" ALL_GAMES = gbt.catalog.games() CATALOG_DIR = Path(__file__).parent.parent.parent / "catalog" From cd2ffc9ee4cc6400ecb2fa1be41db45abbfef7aa Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 14:43:10 +0000 Subject: [PATCH 023/119] tidy update script --- src/pygambit/update_catalog.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pygambit/update_catalog.py b/src/pygambit/update_catalog.py index 2ab6c0e73..1b05669da 100644 --- a/src/pygambit/update_catalog.py +++ b/src/pygambit/update_catalog.py @@ -4,14 +4,13 @@ CATALOG_CSV = Path(__file__).parent.parent.parent / "doc" / "catalog.csv" MAKEFILE_AM = Path(__file__).parent.parent.parent / "Makefile.am" -ALL_GAMES = gbt.catalog.games() -CATALOG_DIR = Path(__file__).parent.parent.parent / "catalog" -efg_files = list(CATALOG_DIR.rglob("*.efg")) -nfg_files = list(CATALOG_DIR.rglob("*.nfg")) def update_makefile(): """Update the Makefile.am with all games from the catalog.""" + catalog_dir = Path(__file__).parent.parent.parent / "catalog" + efg_files = list(catalog_dir.rglob("*.efg")) + nfg_files = list(catalog_dir.rglob("*.nfg")) game_files = [] for entry in efg_files + nfg_files: @@ -52,7 +51,7 @@ def update_makefile(): if __name__ == "__main__": # Create CSV used by RST docs page - ALL_GAMES.to_csv(CATALOG_CSV, index=False) + gbt.catalog.games().to_csv(CATALOG_CSV, index=False) print(f"Generated {CATALOG_CSV} for use in docs build.") # Update the Makefile.am with the current list of catalog files From 5675444b1fabb0375a0f602858c04d7dcc101826 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 14:45:20 +0000 Subject: [PATCH 024/119] use explicit python executable from the virtualenv to create CSV for catalog docs table in readthedocs yml --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 5cc214ac8..03dc4edad 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -16,7 +16,7 @@ build: jobs: # Create CSV for catalog table in docs post_install: - - python src/pygambit/update_catalog.py + - $READTHEDOCS_VIRTUALENV_PATH/bin/python src/pygambit/update_catalog.py python: install: From f57422322b210c8286664f091611ac8e490fa2de Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 15:06:39 +0000 Subject: [PATCH 025/119] add developer doc for updating the catalog --- doc/developer.catalog.rst | 41 ++++++++++++++++++++++++++++++++++ doc/developer.contributing.rst | 2 ++ doc/developer.rst | 1 + 3 files changed, 44 insertions(+) create mode 100644 doc/developer.catalog.rst diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst new file mode 100644 index 000000000..0f347670f --- /dev/null +++ b/doc/developer.catalog.rst @@ -0,0 +1,41 @@ +Updating the Games Catalog +========================== + +This page covers the process for contributing to and updating Gambit's :ref:`Games Catalog `. +To do so, you will need to have the `gambit` GitHub repo cloned and be able to submit pull request via GitHub; +you may wish to first review the :ref:`contributor guidelines `. + +You can add games to the catalog saved in a valid representation :ref:`format `. +Currently supported representations are: + +- `.efg` for extensive form games +- `.nfg` for normal form games + +Add new games +------------- + +1. **Create the game file:** + + Use either :ref:`pygambit `, the Gambit :ref:`CLI ` or :ref:`GUI ` to create and save game in a valid representation :ref:`format `. + +2. **Add the game file:** + + Create a new branch in the ``gambit`` repo. Add your new game file(s) inside the ``catalog`` dir and commit them. + +3. **Update the catalog:** + + Use the ``update_catalog.py`` script to update the Gambit's documentation & build files. + + .. code-block:: bash + + python src/pygambit/update_catalog.py + + .. note:: + + Run this script in a Python environment where ``pygambit`` itself is also :ref:`installed `. + +4. **Submit a pull request to GitHub with all changes.** + + .. warning:: + + Make sure you commit all changed files e.g. run ``git add --all`` before committing and pushing. diff --git a/doc/developer.contributing.rst b/doc/developer.contributing.rst index f86939850..02ca37747 100644 --- a/doc/developer.contributing.rst +++ b/doc/developer.contributing.rst @@ -1,3 +1,5 @@ +.. _contributing: + Contributing to Gambit ====================== diff --git a/doc/developer.rst b/doc/developer.rst index 0a1512659..b954f0850 100644 --- a/doc/developer.rst +++ b/doc/developer.rst @@ -11,3 +11,4 @@ This section contains information for developers who want to contribute to the G developer.build developer.contributing + developer.catalog From 5147278cfb188cdb3496bd54da6df5db430fc0bd Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 15:13:32 +0000 Subject: [PATCH 026/119] Don't update Makefile.am by default --- doc/developer.catalog.rst | 4 ++-- src/pygambit/update_catalog.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index 0f347670f..1a4d953e5 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -24,11 +24,11 @@ Add new games 3. **Update the catalog:** - Use the ``update_catalog.py`` script to update the Gambit's documentation & build files. + Use the ``update_catalog.py`` script to update Gambit's documentation & build files. .. code-block:: bash - python src/pygambit/update_catalog.py + python src/pygambit/update_catalog.py --build .. note:: diff --git a/src/pygambit/update_catalog.py b/src/pygambit/update_catalog.py index 1b05669da..c6787f170 100644 --- a/src/pygambit/update_catalog.py +++ b/src/pygambit/update_catalog.py @@ -1,3 +1,4 @@ +import argparse from pathlib import Path import pygambit as gbt @@ -46,13 +47,20 @@ def update_makefile(): if content != updated_content: print(f"Updated {str(MAKEFILE_AM)}") + else: + print(f"No changes to add to {str(MAKEFILE_AM)}") if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--build", action="store_true") + args = parser.parse_args() + # Create CSV used by RST docs page gbt.catalog.games().to_csv(CATALOG_CSV, index=False) - print(f"Generated {CATALOG_CSV} for use in docs build.") + print(f"Generated {CATALOG_CSV} for use in local docs build. DO NOT COMMIT.") # Update the Makefile.am with the current list of catalog files - update_makefile() + if args.build: + update_makefile() From 4b033c9ca4de6b12aac75c04c4618036cdaf3154 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 15:15:13 +0000 Subject: [PATCH 027/119] consistency in notebook comment --- doc/tutorials/01_quickstart.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/tutorials/01_quickstart.ipynb b/doc/tutorials/01_quickstart.ipynb index 06ec6f91e..4c1d85f7b 100644 --- a/doc/tutorials/01_quickstart.ipynb +++ b/doc/tutorials/01_quickstart.ipynb @@ -467,13 +467,13 @@ } ], "source": [ - "# gbt.read_nfg(\"test_games/prisoners_dilemma.nfg\")" + "# gbt.read_nfg(\"prisoners_dilemma.nfg\")" ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "gambitvenv313", "language": "python", "name": "python3" }, From b14402ad9af308bdd9eb75a6866e2c03d47b4650 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 15:29:49 +0000 Subject: [PATCH 028/119] demo loading from catalog in tutorial 1 --- doc/tutorials/01_quickstart.ipynb | 169 +++++++++++++++++++++++------- 1 file changed, 132 insertions(+), 37 deletions(-) diff --git a/doc/tutorials/01_quickstart.ipynb b/doc/tutorials/01_quickstart.ipynb index 4c1d85f7b..012ee466a 100644 --- a/doc/tutorials/01_quickstart.ipynb +++ b/doc/tutorials/01_quickstart.ipynb @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "c58d382d", "metadata": {}, "outputs": [], @@ -50,7 +50,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "2060c1ed", "metadata": {}, "outputs": [ @@ -60,7 +60,7 @@ "pygambit.gambit.Game" ] }, - "execution_count": 1, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -83,7 +83,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "9d8203e8", "metadata": {}, "outputs": [], @@ -111,7 +111,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "61030607", "metadata": {}, "outputs": [], @@ -135,7 +135,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "caecc334", "metadata": {}, "outputs": [ @@ -143,13 +143,13 @@ "data": { "text/html": [ "

Prisoner's Dilemma

\n", - "
CooperateDefect
Cooperate-1,-1-3,0
Defect0,-3-2,-2
\n" + "
CooperateDefect
Cooperate-1,-1-3,0
Defect0,-3-2,-2
CooperateDefect
Cooperate-1,-1-3,0
Defect0,-3-2,-2
CooperateDefect
Cooperate-1,-1-3,0
Defect0,-3-2,-2
CooperateDefect
Cooperate-1,-1-3,0
Defect0,-3-2,-2
\n" ], "text/plain": [ "Game(title='Prisoner's Dilemma')" ] }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -189,7 +189,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "843ba7f3", "metadata": {}, "outputs": [ @@ -197,13 +197,13 @@ "data": { "text/html": [ "

Another Prisoner's Dilemma

\n", - "
12
1-1,-1-3,0
20,-3-2,-2
\n" + "
12
1-1,-1-3,0
20,-3-2,-2
12
1-1,-1-3,0
20,-3-2,-2
12
1-1,-1-3,0
20,-3-2,-2
12
1-1,-1-3,0
20,-3-2,-2
\n" ], "text/plain": [ "Game(title='Another Prisoner's Dilemma')" ] }, - "execution_count": 5, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -233,7 +233,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "5ee752c4", "metadata": {}, "outputs": [ @@ -270,7 +270,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "a81c06c7", "metadata": {}, "outputs": [ @@ -280,7 +280,7 @@ "pygambit.nash.NashComputationResult" ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -300,7 +300,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "bd395180", "metadata": {}, "outputs": [ @@ -310,7 +310,7 @@ "1" ] }, - "execution_count": 8, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -329,7 +329,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "id": "76570ebc", "metadata": {}, "outputs": [ @@ -342,7 +342,7 @@ "[[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]]" ] }, - "execution_count": 9, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -354,7 +354,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "id": "6e8cfcde", "metadata": {}, "outputs": [ @@ -364,7 +364,7 @@ "pygambit.gambit.MixedStrategyProfileRational" ] }, - "execution_count": 10, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -385,7 +385,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "id": "980bf6b1", "metadata": {}, "outputs": [ @@ -417,11 +417,117 @@ }, { "cell_type": "markdown", - "id": "24f36b0d", + "id": "c27c50f0-e8cc-4160-9975-aa02b33c6879", "metadata": {}, "source": [ "The equilibrium shows that both players are playing their dominant strategy, which is to defect. This is because defecting is the best response to the other player's strategy, regardless of what that strategy is.\n", "\n", + "Loading games from the catalog \n", + "------------------------------\n", + "\n", + "Gambit includes a catalog of standard games that can be loaded directly by name. You can list all the available games like so:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "e1b060fb-94cc-432d-a9cc-4ccae5908f79", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
GameTitle
02smpTwo-stage matching pennies game
1pdTwo person Prisoner's Dilemma game
\n", + "
" + ], + "text/plain": [ + " Game Title\n", + "0 2smp Two-stage matching pennies game\n", + "1 pd Two person Prisoner's Dilemma game" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gbt.catalog.games()" + ] + }, + { + "cell_type": "markdown", + "id": "3030ee7e-2d5e-4560-ab1b-7c865d0fe19d", + "metadata": {}, + "source": [ + "You can then load a specific game by its name. For example, to load the \"Prisoner's Dilemma\" game from the catalog, you would do the following:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "9ee2c3bd-22d1-4c8e-996c-cd9e0dfd64cc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "

Two person Prisoner's Dilemma game

\n", + "
12
19,90,10
210,01,1
12
19,90,10
210,01,1
12
19,90,10
210,01,1
12
19,90,10
210,01,1
\n" + ], + "text/plain": [ + "Game(title='Two person Prisoner's Dilemma game')" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g = gbt.catalog.load(\"pd\")\n", + "g" + ] + }, + { + "cell_type": "markdown", + "id": "24f36b0d", + "metadata": {}, + "source": [ "Saving and reading strategic form games to and from file\n", "--------------------\n", "\n", @@ -433,7 +539,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "f58eaa77", "metadata": {}, "outputs": [], @@ -451,21 +557,10 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "4119a2ac", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pygambit.gambit.Game" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# gbt.read_nfg(\"prisoners_dilemma.nfg\")" ] @@ -473,7 +568,7 @@ ], "metadata": { "kernelspec": { - "display_name": "gambitvenv313", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, From 264f7fb742a67e68adfcd55ac545100dc37b08e5 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 15:55:26 +0000 Subject: [PATCH 029/119] load from catalog for game examples in advanced tutorials --- Makefile.am | 2 + {contrib/games => catalog}/2x2x2.nfg | 0 .../games => catalog}/myerson_fig_4_2.efg | 0 .../agent_versus_non_agent_regret.ipynb | 200 +++++++++--------- .../advanced_tutorials/starting_points.ipynb | 28 +-- 5 files changed, 112 insertions(+), 118 deletions(-) rename {contrib/games => catalog}/2x2x2.nfg (100%) rename {contrib/games => catalog}/myerson_fig_4_2.efg (100%) diff --git a/Makefile.am b/Makefile.am index 7b88649ac..b2a4b4395 100644 --- a/Makefile.am +++ b/Makefile.am @@ -240,6 +240,8 @@ EXTRA_DIST = \ contrib/games/zero.nfg \ src/README.rst \ catalog/2smp.efg \ + catalog/2x2x2.nfg \ + catalog/myerson_fig_4_2.efg \ catalog/pd.nfg core_SOURCES = \ diff --git a/contrib/games/2x2x2.nfg b/catalog/2x2x2.nfg similarity index 100% rename from contrib/games/2x2x2.nfg rename to catalog/2x2x2.nfg diff --git a/contrib/games/myerson_fig_4_2.efg b/catalog/myerson_fig_4_2.efg similarity index 100% rename from contrib/games/myerson_fig_4_2.efg rename to catalog/myerson_fig_4_2.efg diff --git a/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb index 68b78ee11..c7e16ba80 100644 --- a/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb +++ b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb @@ -35,235 +35,225 @@ { "data": { "image/svg+xml": [ - "\n", + "\n", "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", + "\n", "\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", - "\n", - "\n", - "\n", + "\n", "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", - "\n", - "\n", - "\n", + "\n", "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", "\n", - "\n", - "\n", + "\n", + "\n", "\n", "\n", - "\n", + "\n", "\n", - "\n", - "\n", - "\n", - "\n", - "\n", + "\n", + "\n", + "\n", + "\n", + "\n", "" ], "text/plain": [ @@ -280,7 +270,7 @@ "\n", "import pygambit as gbt\n", "\n", - "g = gbt.read_efg(\"../../../contrib/games/myerson_fig_4_2.efg\")\n", + "g = gbt.catalog.load(\"myerson_fig_4_2\")\n", "draw_tree(g)" ] }, @@ -436,7 +426,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "[[4.2517925671604327e-07, 0.49999911111761514, 0.5000004637031282], [0.3333333517938241, 0.6666666482061759]]\n" + "[[6.949101896011271e-07, 0.49999858461819596, 0.5000007204716144], [0.33333333942537524, 0.6666666605746248]]\n" ] } ], @@ -455,8 +445,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Liap value: 4.43446520109796e-14\n", - "Max regret: 1.694170896904268e-07\n" + "Liap value: 1.0863970174089946e-13\n", + "Max regret: 2.407747583532682e-07\n" ] } ], @@ -699,7 +689,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.9" + "version": "3.13.5" } }, "nbformat": 4, diff --git a/doc/tutorials/advanced_tutorials/starting_points.ipynb b/doc/tutorials/advanced_tutorials/starting_points.ipynb index fb976245b..4fa281746 100644 --- a/doc/tutorials/advanced_tutorials/starting_points.ipynb +++ b/doc/tutorials/advanced_tutorials/starting_points.ipynb @@ -25,7 +25,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "493cafb8", "metadata": {}, "outputs": [ @@ -33,7 +33,7 @@ "data": { "text/html": [ "

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

\n", - "
Subtable with strategies:
Player 3 Strategy 1
12
19,8,120,0,0
20,0,09,8,2
Subtable with strategies:
Player 3 Strategy 2
12
10,0,03,4,6
23,4,60,0,0
\n" + "
Subtable with strategies:
Player 3 Strategy 1
12
19,8,120,0,0
20,0,09,8,2
Subtable with strategies:
Player 3 Strategy 1
12
19,8,120,0,0
20,0,09,8,2
Subtable with strategies:
Player 3 Strategy 1
12
19,8,120,0,0
20,0,09,8,2
Subtable with strategies:
Player 3 Strategy 1
12
19,8,120,0,0
20,0,09,8,2
Subtable with strategies:
Player 3 Strategy 2
12
10,0,03,4,6
23,4,60,0,0
Subtable with strategies:
Player 3 Strategy 2
12
10,0,03,4,6
23,4,60,0,0
Subtable with strategies:
Player 3 Strategy 2
12
10,0,03,4,6
23,4,60,0,0
Subtable with strategies:
Player 3 Strategy 2
12
10,0,03,4,6
23,4,60,0,0
\n" ], "text/plain": [ "Game(title='2x2x2 Example from McKelvey-McLennan, with 9 Nash equilibria, 2 totally mixed')" @@ -45,8 +45,11 @@ } ], "source": [ + "import numpy as np\n", + "\n", "import pygambit as gbt\n", - "g = gbt.read_nfg(\"../../2x2x2.nfg\")\n", + "\n", + "g = gbt.catalog.load(\"2x2x2\")\n", "g" ] }, @@ -93,10 +96,10 @@ { "data": { "text/latex": [ - "$\\left[[0.3999999026224355, 0.6000000973775644],[0.49999981670851457, 0.5000001832914854],[0.3333329684317666, 0.6666670315682334]\\right]$" + "$\\left[[0.3999998880351315, 0.6000001119648686],[0.5000000683119051, 0.4999999316880949],[0.3333335574724357, 0.6666664425275644]\\right]$" ], "text/plain": [ - "[[0.3999999026224355, 0.6000000973775644], [0.49999981670851457, 0.5000001832914854], [0.3333329684317666, 0.6666670315682334]]" + "[[0.3999998880351315, 0.6000001119648686], [0.5000000683119051, 0.4999999316880949], [0.3333335574724357, 0.6666664425275644]]" ] }, "execution_count": 3, @@ -153,10 +156,10 @@ { "data": { "text/latex": [ - "$\\left[[1.0, 0.0],[0.9999999944750116, 5.524988446860122e-09],[0.9999999991845827, 8.154173380971617e-10]\\right]$" + "$\\left[[1.0, 0.0],[0.9999999916299683, 8.370031632789431e-09],[1.0, 0.0]\\right]$" ], "text/plain": [ - "[[1.0, 0.0], [0.9999999944750116, 5.524988446860122e-09], [0.9999999991845827, 8.154173380971617e-10]]" + "[[1.0, 0.0], [0.9999999916299683, 8.370031632789431e-09], [1.0, 0.0]]" ] }, "execution_count": 5, @@ -185,10 +188,10 @@ { "data": { "text/latex": [ - "$\\left[[0.7187961367413075, 0.2812038632586925],[0.1291105793795489, 0.8708894206204512],[0.12367227612277114, 0.876327723877229]\\right]$" + "$\\left[[0.9835790201705958, 0.01642097982940421],[0.7494285573591715, 0.2505714426408285],[0.14967367720546837, 0.8503263227945317]\\right]$" ], "text/plain": [ - "[[0.7187961367413075, 0.2812038632586925], [0.1291105793795489, 0.8708894206204512], [0.12367227612277114, 0.876327723877229]]" + "[[0.9835790201705958, 0.01642097982940421], [0.7494285573591715, 0.2505714426408285], [0.14967367720546837, 0.8503263227945317]]" ] }, "execution_count": 6, @@ -210,10 +213,10 @@ { "data": { "text/latex": [ - "$\\left[[0.5000003932357804, 0.4999996067642197],[0.3999998501612186, 0.6000001498387814],[0.2500001518113522, 0.7499998481886477]\\right]$" + "$\\left[[0.5000000583093926, 0.49999994169060735],[0.39999989404863995, 0.6000001059513601],[0.2499996298123818, 0.7500003701876182]\\right]$" ], "text/plain": [ - "[[0.5000003932357804, 0.4999996067642197], [0.3999998501612186, 0.6000001498387814], [0.2500001518113522, 0.7499998481886477]]" + "[[0.5000000583093926, 0.49999994169060735], [0.39999989404863995, 0.6000001059513601], [0.2499996298123818, 0.7500003701876182]]" ] }, "execution_count": 7, @@ -254,7 +257,6 @@ } ], "source": [ - "import numpy as np\n", "gen = np.random.default_rng(seed=1234567890)\n", "p1 = g.random_strategy_profile(gen=gen)\n", "gen = np.random.default_rng(seed=1234567890)\n", @@ -427,7 +429,7 @@ ], "metadata": { "kernelspec": { - "display_name": "gambitvenv313", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, From 0e268a2d5d83a9ce499e8a68315f7575b10b0dd2 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 16:03:55 +0000 Subject: [PATCH 030/119] Try using pip instead of setuptools to ensure pyproject.toml deps installed for readthedocs build --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 03dc4edad..1bbd415f8 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -21,5 +21,5 @@ build: python: install: - requirements: doc/requirements.txt - - method: setuptools + - method: pip path: "." From 86499461b8b3de26b9d56cc4c88295bd20684145 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 16:13:40 +0000 Subject: [PATCH 031/119] remove deleted contrib games from Makefile.am --- Makefile.am | 2 -- 1 file changed, 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index b2a4b4395..ca6e0dfd8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -168,7 +168,6 @@ EXTRA_DIST = \ contrib/games/my_3-3d.efg \ contrib/games/my_3-3e.efg \ contrib/games/my_3-4.efg \ - contrib/games/myerson.efg \ contrib/games/nim7.efg \ contrib/games/nim.efg \ contrib/games/palf2.efg \ @@ -194,7 +193,6 @@ EXTRA_DIST = \ contrib/games/2x2a.nfg \ contrib/games/2x2const.nfg \ contrib/games/2x2.nfg \ - contrib/games/2x2x2.nfg \ contrib/games/2x2x2x2.nfg \ contrib/games/2x2x2x2x2.nfg \ contrib/games/3x3x3.nfg \ From adea4f202b26f50514212b01f9587553291141d0 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 16:19:46 +0000 Subject: [PATCH 032/119] add a warning about moving games from contrib --- doc/developer.catalog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index 1a4d953e5..40680c8e9 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -34,6 +34,10 @@ Add new games Run this script in a Python environment where ``pygambit`` itself is also :ref:`installed `. + .. warning:: + + This script updates `Makefile.am` with the game file added to the catalog, but if you moved games that were previously in `contrib/games` you'll want to manually remove those files from `EXTRA_DIST`. + 4. **Submit a pull request to GitHub with all changes.** .. warning:: From a72f7a2f106ba0fcada0a78f8b9a5d2bf55aa69e Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 16:24:28 +0000 Subject: [PATCH 033/119] check if pandas duplications error exists if we dont save outputs on notebooks --- doc/tutorials/01_quickstart.ipynb | 234 ++-------- .../agent_versus_non_agent_regret.ipynb | 399 ++---------------- 2 files changed, 53 insertions(+), 580 deletions(-) diff --git a/doc/tutorials/01_quickstart.ipynb b/doc/tutorials/01_quickstart.ipynb index 012ee466a..62949a4b4 100644 --- a/doc/tutorials/01_quickstart.ipynb +++ b/doc/tutorials/01_quickstart.ipynb @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "c58d382d", "metadata": {}, "outputs": [], @@ -50,21 +50,10 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "2060c1ed", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pygambit.gambit.Game" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "n_strategies = [2, 2]\n", "g = gbt.Game.new_table(n_strategies, title=\"Prisoner's Dilemma\")\n", @@ -83,7 +72,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "9d8203e8", "metadata": {}, "outputs": [], @@ -111,7 +100,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "61030607", "metadata": {}, "outputs": [], @@ -135,25 +124,10 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "caecc334", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "

Prisoner's Dilemma

\n", - "
CooperateDefect
Cooperate-1,-1-3,0
Defect0,-3-2,-2
CooperateDefect
Cooperate-1,-1-3,0
Defect0,-3-2,-2
CooperateDefect
Cooperate-1,-1-3,0
Defect0,-3-2,-2
CooperateDefect
Cooperate-1,-1-3,0
Defect0,-3-2,-2
\n" - ], - "text/plain": [ - "Game(title='Prisoner's Dilemma')" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# View the payout matrix\n", "g" @@ -189,25 +163,10 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "843ba7f3", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "

Another Prisoner's Dilemma

\n", - "
12
1-1,-1-3,0
20,-3-2,-2
12
1-1,-1-3,0
20,-3-2,-2
12
1-1,-1-3,0
20,-3-2,-2
12
1-1,-1-3,0
20,-3-2,-2
\n" - ], - "text/plain": [ - "Game(title='Another Prisoner's Dilemma')" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "player1_payoffs = np.array([[-1, -3], [0, -2]])\n", "player2_payoffs = np.transpose(player1_payoffs)\n", @@ -233,19 +192,10 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "5ee752c4", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-1\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "tom_payoffs, jerry_payoffs = g.to_arrays(\n", " # dtype=float\n", @@ -270,21 +220,10 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "a81c06c7", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pygambit.nash.NashComputationResult" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "result = gbt.nash.enumpure_solve(g)\n", "type(result)" @@ -300,21 +239,10 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "bd395180", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "1" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "len(result.equilibria)" ] @@ -329,24 +257,10 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "76570ebc", "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\left[\\left[0,1\\right],\\left[0,1\\right]\\right]$" - ], - "text/plain": [ - "[[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]]" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "msp = result.equilibria[0]\n", "msp" @@ -354,21 +268,10 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "6e8cfcde", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pygambit.gambit.MixedStrategyProfileRational" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "type(msp)" ] @@ -385,27 +288,10 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "980bf6b1", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Tom plays the equilibrium strategy:\n", - "Probability of cooperating: 0\n", - "Probability of defecting: 1\n", - "Payoff: -2\n", - "\n", - "Jerry plays the equilibrium strategy:\n", - "Probability of cooperating: 0\n", - "Probability of defecting: 1\n", - "Payoff: -2\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "for player in g.players:\n", " print(f\"{player.label} plays the equilibrium strategy:\")\n", @@ -430,61 +316,10 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "e1b060fb-94cc-432d-a9cc-4ccae5908f79", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
GameTitle
02smpTwo-stage matching pennies game
1pdTwo person Prisoner's Dilemma game
\n", - "
" - ], - "text/plain": [ - " Game Title\n", - "0 2smp Two-stage matching pennies game\n", - "1 pd Two person Prisoner's Dilemma game" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "gbt.catalog.games()" ] @@ -499,25 +334,10 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "9ee2c3bd-22d1-4c8e-996c-cd9e0dfd64cc", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "

Two person Prisoner's Dilemma game

\n", - "
12
19,90,10
210,01,1
12
19,90,10
210,01,1
12
19,90,10
210,01,1
12
19,90,10
210,01,1
\n" - ], - "text/plain": [ - "Game(title='Two person Prisoner's Dilemma game')" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "g = gbt.catalog.load(\"pd\")\n", "g" @@ -539,7 +359,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "f58eaa77", "metadata": {}, "outputs": [], @@ -557,7 +377,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "4119a2ac", "metadata": {}, "outputs": [], diff --git a/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb index c7e16ba80..f32c6e1a5 100644 --- a/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb +++ b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb @@ -28,243 +28,10 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "5142d6ba-da13-4500-bca6-e68b608bfae9", "metadata": {}, - "outputs": [ - { - "data": { - "image/svg+xml": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "from draw_tree import draw_tree\n", "\n", @@ -284,19 +51,10 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "7882d327-ce04-43d3-bb5a-36cff6da6e96", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Number of pure equilibria: 1\n", - "Max regret: 0\n" - ] - } - ], + "outputs": [], "source": [ "pure_Nash_equilibria = gbt.nash.enumpure_solve(g).equilibria\n", "print(\"Number of pure equilibria:\", len(pure_Nash_equilibria))\n", @@ -314,20 +72,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "6e3e9303-453a-4bac-a449-fa8fda2ba5ec", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Player 1 infoset: 0 behavior probabilities: [Rational(1, 1), Rational(0, 1)]\n", - "Player 1 infoset: 1 behavior probabilities: [Rational(0, 1), Rational(1, 1)]\n", - "Player 2 infoset: 0 behavior probabilities: [Rational(0, 1), Rational(1, 1)]\n" - ] - } - ], + "outputs": [], "source": [ "eq = pure_Nash_equilibria[0]\n", "for infoset, probs in eq.as_behavior().mixed_actions():\n", @@ -344,18 +92,10 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "804345b9-d32b-4f60-b4a0-f9d69dca10a8", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Liap value: 0\n" - ] - } - ], + "outputs": [], "source": [ "print(\"Liap value:\", pure_eq.liap_value())" ] @@ -380,19 +120,10 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "9d18768b-db9b-41ef-aee7-5fe5f524a59e", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Max regret of starting profile: 3\n", - "Liapunov value of starting profile: 14\n" - ] - } - ], + "outputs": [], "source": [ "starting_profile_double = g.mixed_strategy_profile(data=[[0,1,0],[1,0]], rational=False)\n", "starting_profile_rational = g.mixed_strategy_profile(data=[[0,1,0],[1,0]], rational=True)\n", @@ -418,18 +149,10 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "b885271f-7279-4d87-a0b9-bc28449b00ba", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[6.949101896011271e-07, 0.49999858461819596, 0.5000007204716144], [0.33333333942537524, 0.6666666605746248]]\n" - ] - } - ], + "outputs": [], "source": [ "candidate_eq = gbt.nash.liap_solve(start=starting_profile_double).equilibria[0]\n", "print(candidate_eq)" @@ -437,19 +160,10 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "f8a90a9c-393e-4812-9418-76e705880f6f", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Liap value: 1.0863970174089946e-13\n", - "Max regret: 2.407747583532682e-07\n" - ] - } - ], + "outputs": [], "source": [ "print(\"Liap value:\", candidate_eq.liap_value())\n", "print(\"Max regret:\", candidate_eq.max_regret())" @@ -457,19 +171,10 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "567e6a6a-fc8d-4142-806c-6510b2a4c624", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Liap value: 0\n", - "Max regret: 0\n" - ] - } - ], + "outputs": [], "source": [ "candidate_eq_rat = g.mixed_strategy_profile(data=[[0,\"1/2\",\"1/2\"],[\"1/3\",\"2/3\"]], rational=True)\n", "print(\"Liap value:\", candidate_eq_rat.liap_value())\n", @@ -486,20 +191,10 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "87a62c9e-b109-4f88-ac25-d0e0db3f27ea", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[Rational(0, 1), Rational(1, 1), Rational(0, 1)], [Rational(0, 1), Rational(1, 1)]]\n", - "[[Rational(1, 4), Rational(0, 1), Rational(3, 4)], [Rational(1, 2), Rational(1, 2)]]\n", - "[[Rational(0, 1), Rational(1, 2), Rational(1, 2)], [Rational(1, 3), Rational(2, 3)]]\n" - ] - } - ], + "outputs": [], "source": [ "all_extreme_Nash_equilibria = gbt.nash.enummixed_solve(g).equilibria\n", "for eq in all_extreme_Nash_equilibria:\n", @@ -516,20 +211,10 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "2c8ed3df-958e-4ee9-aed6-a106547fbd37", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[Rational(0, 1), Rational(1, 2), Rational(1, 2)], [Rational(1, 3), Rational(2, 3)]]\n", - "Liap value: 0\n", - "Max regret: 0\n" - ] - } - ], + "outputs": [], "source": [ "print(all_extreme_Nash_equilibria[2])\n", "print(\"Liap value:\", all_extreme_Nash_equilibria[2].liap_value())\n", @@ -563,20 +248,10 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "f46ce825-d2b7-492f-b0cf-6f213607e121", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2\n", - "[[[Rational(1, 1), Rational(0, 1)], [Rational(0, 1), Rational(1, 1)]], [[Rational(0, 1), Rational(1, 1)]]]\n", - "[[[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]], [[Rational(1, 1), Rational(0, 1)]]]\n" - ] - } - ], + "outputs": [], "source": [ "pure_agent_equilibria = gbt.nash.enumpure_agent_solve(g).equilibria\n", "print(len(pure_agent_equilibria))\n", @@ -594,21 +269,10 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "dbfa7035", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "pure_Nash_equilibria[0] == pure_agent_equilibria[0].as_strategy()" ] @@ -623,21 +287,10 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "85760cec-5760-4f9d-8ca2-99fba79c7c3c", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Max regret: 1\n", - "Liapunov value: 1\n", - "Agent max regret 0\n", - "Agent Liapunov value: 0\n" - ] - } - ], + "outputs": [], "source": [ "aeq = pure_agent_equilibria[1]\n", "print(\"Max regret:\", aeq.max_regret())\n", From 57863d4248926e1fbe6dce1a131f044afbb36b3f Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 17:01:04 +0000 Subject: [PATCH 034/119] fix problem with print function --- src/games/writer.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/games/writer.cc b/src/games/writer.cc index 744f3efad..47baedc1e 100644 --- a/src/games/writer.cc +++ b/src/games/writer.cc @@ -91,6 +91,7 @@ std::string WriteHTMLFile(const Game &p_game, const GamePlayer &p_rowPlayer, } theHtml += ""; + break; } theHtml += "\n"; return theHtml; From b687e204db43e66b5999e116ffc15adbabeb6ddc Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 4 Feb 2026 17:01:56 +0000 Subject: [PATCH 035/119] resave notebook outputs --- doc/tutorials/01_quickstart.ipynb | 246 +++++++++-- .../agent_versus_non_agent_regret.ipynb | 399 ++++++++++++++++-- .../advanced_tutorials/starting_points.ipynb | 10 +- 3 files changed, 597 insertions(+), 58 deletions(-) diff --git a/doc/tutorials/01_quickstart.ipynb b/doc/tutorials/01_quickstart.ipynb index 62949a4b4..7e4de7599 100644 --- a/doc/tutorials/01_quickstart.ipynb +++ b/doc/tutorials/01_quickstart.ipynb @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "c58d382d", "metadata": {}, "outputs": [], @@ -50,10 +50,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "2060c1ed", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "pygambit.gambit.Game" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "n_strategies = [2, 2]\n", "g = gbt.Game.new_table(n_strategies, title=\"Prisoner's Dilemma\")\n", @@ -72,7 +83,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "9d8203e8", "metadata": {}, "outputs": [], @@ -100,7 +111,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "61030607", "metadata": {}, "outputs": [], @@ -124,10 +135,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "caecc334", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "

Prisoner's Dilemma

\n", + "
CooperateDefect
Cooperate-1,-1-3,0
Defect0,-3-2,-2
\n" + ], + "text/plain": [ + "Game(title='Prisoner's Dilemma')" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# View the payout matrix\n", "g" @@ -163,10 +189,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "843ba7f3", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "

Another Prisoner's Dilemma

\n", + "
12
1-1,-1-3,0
20,-3-2,-2
\n" + ], + "text/plain": [ + "Game(title='Another Prisoner's Dilemma')" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "player1_payoffs = np.array([[-1, -3], [0, -2]])\n", "player2_payoffs = np.transpose(player1_payoffs)\n", @@ -192,10 +233,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "5ee752c4", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-1\n", + "\n" + ] + } + ], "source": [ "tom_payoffs, jerry_payoffs = g.to_arrays(\n", " # dtype=float\n", @@ -220,10 +270,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "a81c06c7", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "pygambit.nash.NashComputationResult" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "result = gbt.nash.enumpure_solve(g)\n", "type(result)" @@ -239,10 +300,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "bd395180", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "len(result.equilibria)" ] @@ -257,10 +329,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "76570ebc", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/latex": [ + "$\\left[\\left[0,1\\right],\\left[0,1\\right]\\right]$" + ], + "text/plain": [ + "[[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "msp = result.equilibria[0]\n", "msp" @@ -268,10 +354,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "6e8cfcde", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "pygambit.gambit.MixedStrategyProfileRational" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "type(msp)" ] @@ -288,10 +385,27 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "980bf6b1", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tom plays the equilibrium strategy:\n", + "Probability of cooperating: 0\n", + "Probability of defecting: 1\n", + "Payoff: -2\n", + "\n", + "Jerry plays the equilibrium strategy:\n", + "Probability of cooperating: 0\n", + "Probability of defecting: 1\n", + "Payoff: -2\n", + "\n" + ] + } + ], "source": [ "for player in g.players:\n", " print(f\"{player.label} plays the equilibrium strategy:\")\n", @@ -316,10 +430,73 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "e1b060fb-94cc-432d-a9cc-4ccae5908f79", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
GameTitle
02smpTwo-stage matching pennies game
12x2x22x2x2 Example from McKelvey-McLennan, with 9 N...
2myerson_fig_4_2Myerson (1991) Fig 4.2
3pdTwo person Prisoner's Dilemma game
\n", + "
" + ], + "text/plain": [ + " Game Title\n", + "0 2smp Two-stage matching pennies game\n", + "1 2x2x2 2x2x2 Example from McKelvey-McLennan, with 9 N...\n", + "2 myerson_fig_4_2 Myerson (1991) Fig 4.2\n", + "3 pd Two person Prisoner's Dilemma game" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "gbt.catalog.games()" ] @@ -334,10 +511,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "9ee2c3bd-22d1-4c8e-996c-cd9e0dfd64cc", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "

Two person Prisoner's Dilemma game

\n", + "
12
19,90,10
210,01,1
\n" + ], + "text/plain": [ + "Game(title='Two person Prisoner's Dilemma game')" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "g = gbt.catalog.load(\"pd\")\n", "g" @@ -359,7 +551,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "f58eaa77", "metadata": {}, "outputs": [], @@ -377,7 +569,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "4119a2ac", "metadata": {}, "outputs": [], diff --git a/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb index f32c6e1a5..c7e16ba80 100644 --- a/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb +++ b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb @@ -28,10 +28,243 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "5142d6ba-da13-4500-bca6-e68b608bfae9", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from draw_tree import draw_tree\n", "\n", @@ -51,10 +284,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "7882d327-ce04-43d3-bb5a-36cff6da6e96", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of pure equilibria: 1\n", + "Max regret: 0\n" + ] + } + ], "source": [ "pure_Nash_equilibria = gbt.nash.enumpure_solve(g).equilibria\n", "print(\"Number of pure equilibria:\", len(pure_Nash_equilibria))\n", @@ -72,10 +314,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "6e3e9303-453a-4bac-a449-fa8fda2ba5ec", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Player 1 infoset: 0 behavior probabilities: [Rational(1, 1), Rational(0, 1)]\n", + "Player 1 infoset: 1 behavior probabilities: [Rational(0, 1), Rational(1, 1)]\n", + "Player 2 infoset: 0 behavior probabilities: [Rational(0, 1), Rational(1, 1)]\n" + ] + } + ], "source": [ "eq = pure_Nash_equilibria[0]\n", "for infoset, probs in eq.as_behavior().mixed_actions():\n", @@ -92,10 +344,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "804345b9-d32b-4f60-b4a0-f9d69dca10a8", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Liap value: 0\n" + ] + } + ], "source": [ "print(\"Liap value:\", pure_eq.liap_value())" ] @@ -120,10 +380,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "9d18768b-db9b-41ef-aee7-5fe5f524a59e", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Max regret of starting profile: 3\n", + "Liapunov value of starting profile: 14\n" + ] + } + ], "source": [ "starting_profile_double = g.mixed_strategy_profile(data=[[0,1,0],[1,0]], rational=False)\n", "starting_profile_rational = g.mixed_strategy_profile(data=[[0,1,0],[1,0]], rational=True)\n", @@ -149,10 +418,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "b885271f-7279-4d87-a0b9-bc28449b00ba", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[6.949101896011271e-07, 0.49999858461819596, 0.5000007204716144], [0.33333333942537524, 0.6666666605746248]]\n" + ] + } + ], "source": [ "candidate_eq = gbt.nash.liap_solve(start=starting_profile_double).equilibria[0]\n", "print(candidate_eq)" @@ -160,10 +437,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "f8a90a9c-393e-4812-9418-76e705880f6f", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Liap value: 1.0863970174089946e-13\n", + "Max regret: 2.407747583532682e-07\n" + ] + } + ], "source": [ "print(\"Liap value:\", candidate_eq.liap_value())\n", "print(\"Max regret:\", candidate_eq.max_regret())" @@ -171,10 +457,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "567e6a6a-fc8d-4142-806c-6510b2a4c624", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Liap value: 0\n", + "Max regret: 0\n" + ] + } + ], "source": [ "candidate_eq_rat = g.mixed_strategy_profile(data=[[0,\"1/2\",\"1/2\"],[\"1/3\",\"2/3\"]], rational=True)\n", "print(\"Liap value:\", candidate_eq_rat.liap_value())\n", @@ -191,10 +486,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "87a62c9e-b109-4f88-ac25-d0e0db3f27ea", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[Rational(0, 1), Rational(1, 1), Rational(0, 1)], [Rational(0, 1), Rational(1, 1)]]\n", + "[[Rational(1, 4), Rational(0, 1), Rational(3, 4)], [Rational(1, 2), Rational(1, 2)]]\n", + "[[Rational(0, 1), Rational(1, 2), Rational(1, 2)], [Rational(1, 3), Rational(2, 3)]]\n" + ] + } + ], "source": [ "all_extreme_Nash_equilibria = gbt.nash.enummixed_solve(g).equilibria\n", "for eq in all_extreme_Nash_equilibria:\n", @@ -211,10 +516,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "2c8ed3df-958e-4ee9-aed6-a106547fbd37", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[Rational(0, 1), Rational(1, 2), Rational(1, 2)], [Rational(1, 3), Rational(2, 3)]]\n", + "Liap value: 0\n", + "Max regret: 0\n" + ] + } + ], "source": [ "print(all_extreme_Nash_equilibria[2])\n", "print(\"Liap value:\", all_extreme_Nash_equilibria[2].liap_value())\n", @@ -248,10 +563,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "f46ce825-d2b7-492f-b0cf-6f213607e121", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2\n", + "[[[Rational(1, 1), Rational(0, 1)], [Rational(0, 1), Rational(1, 1)]], [[Rational(0, 1), Rational(1, 1)]]]\n", + "[[[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]], [[Rational(1, 1), Rational(0, 1)]]]\n" + ] + } + ], "source": [ "pure_agent_equilibria = gbt.nash.enumpure_agent_solve(g).equilibria\n", "print(len(pure_agent_equilibria))\n", @@ -269,10 +594,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "dbfa7035", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "pure_Nash_equilibria[0] == pure_agent_equilibria[0].as_strategy()" ] @@ -287,10 +623,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "85760cec-5760-4f9d-8ca2-99fba79c7c3c", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Max regret: 1\n", + "Liapunov value: 1\n", + "Agent max regret 0\n", + "Agent Liapunov value: 0\n" + ] + } + ], "source": [ "aeq = pure_agent_equilibria[1]\n", "print(\"Max regret:\", aeq.max_regret())\n", diff --git a/doc/tutorials/advanced_tutorials/starting_points.ipynb b/doc/tutorials/advanced_tutorials/starting_points.ipynb index 4fa281746..9e8f72d5e 100644 --- a/doc/tutorials/advanced_tutorials/starting_points.ipynb +++ b/doc/tutorials/advanced_tutorials/starting_points.ipynb @@ -33,7 +33,7 @@ "data": { "text/html": [ "

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

\n", - "
Subtable with strategies:
Player 3 Strategy 1
12
19,8,120,0,0
20,0,09,8,2
Subtable with strategies:
Player 3 Strategy 1
12
19,8,120,0,0
20,0,09,8,2
Subtable with strategies:
Player 3 Strategy 1
12
19,8,120,0,0
20,0,09,8,2
Subtable with strategies:
Player 3 Strategy 1
12
19,8,120,0,0
20,0,09,8,2
Subtable with strategies:
Player 3 Strategy 2
12
10,0,03,4,6
23,4,60,0,0
Subtable with strategies:
Player 3 Strategy 2
12
10,0,03,4,6
23,4,60,0,0
Subtable with strategies:
Player 3 Strategy 2
12
10,0,03,4,6
23,4,60,0,0
Subtable with strategies:
Player 3 Strategy 2
12
10,0,03,4,6
23,4,60,0,0
\n" + "
Subtable with strategies:
Player 3 Strategy 1
12
19,8,120,0,0
20,0,09,8,2
\n" ], "text/plain": [ "Game(title='2x2x2 Example from McKelvey-McLennan, with 9 Nash equilibria, 2 totally mixed')" @@ -188,10 +188,10 @@ { "data": { "text/latex": [ - "$\\left[[0.9835790201705958, 0.01642097982940421],[0.7494285573591715, 0.2505714426408285],[0.14967367720546837, 0.8503263227945317]\\right]$" + "$\\left[[0.5172260574334439, 0.48277394256655615],[0.5372523987305369, 0.462747601269463],[0.8261013405886477, 0.17389865941135238]\\right]$" ], "text/plain": [ - "[[0.9835790201705958, 0.01642097982940421], [0.7494285573591715, 0.2505714426408285], [0.14967367720546837, 0.8503263227945317]]" + "[[0.5172260574334439, 0.48277394256655615], [0.5372523987305369, 0.462747601269463], [0.8261013405886477, 0.17389865941135238]]" ] }, "execution_count": 6, @@ -213,10 +213,10 @@ { "data": { "text/latex": [ - "$\\left[[0.5000000583093926, 0.49999994169060735],[0.39999989404863995, 0.6000001059513601],[0.2499996298123818, 0.7500003701876182]\\right]$" + "$\\left[[0.49999999983005444, 0.5000000001699455],[0.4999999947343101, 0.5000000052656899],[0.9999989483984311, 1.0516015689453635e-06]\\right]$" ], "text/plain": [ - "[[0.5000000583093926, 0.49999994169060735], [0.39999989404863995, 0.6000001059513601], [0.2499996298123818, 0.7500003701876182]]" + "[[0.49999999983005444, 0.5000000001699455], [0.4999999947343101, 0.5000000052656899], [0.9999989483984311, 1.0516015689453635e-06]]" ] }, "execution_count": 7, From 6562433b4223d7915a81b6ff4b3d69abce913ccb Mon Sep 17 00:00:00 2001 From: Ted Turocy Date: Tue, 10 Feb 2026 12:30:59 +0000 Subject: [PATCH 036/119] Update writer.cc --- src/games/writer.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/src/games/writer.cc b/src/games/writer.cc index 47baedc1e..744f3efad 100644 --- a/src/games/writer.cc +++ b/src/games/writer.cc @@ -91,7 +91,6 @@ std::string WriteHTMLFile(const Game &p_game, const GamePlayer &p_rowPlayer, } theHtml += ""; - break; } theHtml += "\n"; return theHtml; From f84bcc8c885116ea5cbaa0abc637714aea36e60a Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 15:11:12 +0000 Subject: [PATCH 037/119] move catalog update script into build support --- {src/pygambit => build_support/catalog}/update_catalog.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {src/pygambit => build_support/catalog}/update_catalog.py (100%) diff --git a/src/pygambit/update_catalog.py b/build_support/catalog/update_catalog.py similarity index 100% rename from src/pygambit/update_catalog.py rename to build_support/catalog/update_catalog.py From d17f976382c18ed6efe73299760636f97900f3ce Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 15:11:46 +0000 Subject: [PATCH 038/119] rename script --- build_support/catalog/{update_catalog.py => update.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename build_support/catalog/{update_catalog.py => update.py} (100%) diff --git a/build_support/catalog/update_catalog.py b/build_support/catalog/update.py similarity index 100% rename from build_support/catalog/update_catalog.py rename to build_support/catalog/update.py From f07fdaee550295d17314145b90ec962e70217bfc Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 15:14:32 +0000 Subject: [PATCH 039/119] rename var --- 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 c6787f170..c5305d092 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -4,14 +4,14 @@ import pygambit as gbt CATALOG_CSV = Path(__file__).parent.parent.parent / "doc" / "catalog.csv" +CATALOG_DIR = Path(__file__).parent.parent.parent / "catalog" MAKEFILE_AM = Path(__file__).parent.parent.parent / "Makefile.am" def update_makefile(): """Update the Makefile.am with all games from the catalog.""" - catalog_dir = Path(__file__).parent.parent.parent / "catalog" - efg_files = list(catalog_dir.rglob("*.efg")) - nfg_files = list(catalog_dir.rglob("*.nfg")) + efg_files = list(CATALOG_DIR.rglob("*.efg")) + nfg_files = list(CATALOG_DIR.rglob("*.nfg")) game_files = [] for entry in efg_files + nfg_files: From fd66e41de287dcfdaf4951757c482f773c4346b4 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 15:17:54 +0000 Subject: [PATCH 040/119] update path to catalog update script in readthedocs yml and docs page --- .readthedocs.yml | 2 +- doc/developer.catalog.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 1bbd415f8..9cc997431 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -16,7 +16,7 @@ build: jobs: # Create CSV for catalog table in docs post_install: - - $READTHEDOCS_VIRTUALENV_PATH/bin/python src/pygambit/update_catalog.py + - $READTHEDOCS_VIRTUALENV_PATH/bin/python build_support/catalog/update.py python: install: diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index 40680c8e9..ded101f3c 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -24,11 +24,11 @@ Add new games 3. **Update the catalog:** - Use the ``update_catalog.py`` script to update Gambit's documentation & build files. + Use the ``update.py`` script to update Gambit's documentation & build files. .. code-block:: bash - python src/pygambit/update_catalog.py --build + python build_support/catalog/update.py --build .. note:: From bb1e9206748f985dc28385372e73c96e6f11143f Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 15:22:11 +0000 Subject: [PATCH 041/119] move myserson fig into subfolder --- catalog/{myerson_fig_4_2.efg => myerson/fig_4_2.efg} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename catalog/{myerson_fig_4_2.efg => myerson/fig_4_2.efg} (100%) diff --git a/catalog/myerson_fig_4_2.efg b/catalog/myerson/fig_4_2.efg similarity index 100% rename from catalog/myerson_fig_4_2.efg rename to catalog/myerson/fig_4_2.efg From 5c7a60f75046a3d5c6c9b31277fc3df6d0b1cf16 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 15:29:12 +0000 Subject: [PATCH 042/119] clarify script usage --- 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 ded101f3c..b60a56705 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -36,7 +36,7 @@ Add new games .. warning:: - This script updates `Makefile.am` with the game file added to the catalog, but if you moved games that were previously in `contrib/games` you'll want to manually remove those files from `EXTRA_DIST`. + Running the script with the ``--build`` flag updates `Makefile.am`. If you moved games that were previously in `contrib/games` you'll need to also manually remove those files from `EXTRA_DIST`. 4. **Submit a pull request to GitHub with all changes.** From dc7a37380e06468029f78e9f5dd90de287fa103b Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 15:29:23 +0000 Subject: [PATCH 043/119] add test_catalog_load_subdir_slug --- tests/test_catalog.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index f281cc353..1be67c2f1 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -24,6 +24,12 @@ def test_catalog_load_invalid_slug(): gbt.catalog.load("invalid_slug") +def test_catalog_load_subdir_slug(): + """Test loading a game from catalog/somedir""" + g = gbt.catalog.load("myerson/fig_4_2") + assert isinstance(g, gbt.Game) + + def test_catalog_games(): """Test games() function returns df of game slugs and titles""" all_games = gbt.catalog.games() From 78505851e30ecf313e5798bdbd59e16561992d0c Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 15:32:32 +0000 Subject: [PATCH 044/119] update makefile --- Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index ca6e0dfd8..14d5552bf 100644 --- a/Makefile.am +++ b/Makefile.am @@ -239,7 +239,7 @@ EXTRA_DIST = \ src/README.rst \ catalog/2smp.efg \ catalog/2x2x2.nfg \ - catalog/myerson_fig_4_2.efg \ + catalog/fig_4_2.efg \ catalog/pd.nfg core_SOURCES = \ From f6ea5dfda1314a0f3491bbddcc68db36a9611b93 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 15:34:29 +0000 Subject: [PATCH 045/119] update agent nb --- .../agent_versus_non_agent_regret.ipynb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb index c7e16ba80..3c3ac80b1 100644 --- a/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb +++ b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb @@ -270,7 +270,7 @@ "\n", "import pygambit as gbt\n", "\n", - "g = gbt.catalog.load(\"myerson_fig_4_2\")\n", + "g = gbt.catalog.load(\"myerson/fig_4_2\")\n", "draw_tree(g)" ] }, @@ -426,7 +426,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "[[6.949101896011271e-07, 0.49999858461819596, 0.5000007204716144], [0.33333333942537524, 0.6666666605746248]]\n" + "[[4.2517925671604327e-07, 0.49999911111761514, 0.5000004637031282], [0.3333333517938241, 0.6666666482061759]]\n" ] } ], @@ -445,8 +445,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Liap value: 1.0863970174089946e-13\n", - "Max regret: 2.407747583532682e-07\n" + "Liap value: 4.43446520109796e-14\n", + "Max regret: 1.694170896904268e-07\n" ] } ], From f655645edd245882c36a00f04104e38b96327c1b Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 15:39:08 +0000 Subject: [PATCH 046/119] add test for slug in subdir of catalog --- tests/test_catalog.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 1be67c2f1..84d741022 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -33,7 +33,10 @@ def test_catalog_load_subdir_slug(): def test_catalog_games(): """Test games() function returns df of game slugs and titles""" all_games = gbt.catalog.games() + slugs = list(all_games.Game) assert isinstance(all_games, pd.DataFrame) assert len(all_games) > 0 - assert "2smp" in list(all_games.Game) + assert "2smp" in slugs assert "Two-stage matching pennies game" in list(all_games.Title) + # Check slug of game in subdir + assert "myerson/fig_4_2" in slugs From 56cd19a0b6733033c8622a6cf9313f9b96474a2c Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 15:50:45 +0000 Subject: [PATCH 047/119] update games func to list slugs correctly --- catalog/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index 4164041f5..f6bfd5eb0 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -30,20 +30,25 @@ def load(slug: str) -> gbt.Game: def games() -> pd.DataFrame: """ - List games available in the package catalog. + List games available in the package catalog, including subdirectories. """ records: list[dict[str, str]] = [] - # iterdir() works directly on the Traversable object - for resource_path in sorted(_CATALOG_RESOURCE.iterdir()): + # Using rglob("*") to find files in all subdirectories + 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) + game_slug = str(rel_path.with_suffix("")) + with as_file(resource_path) as path: game = reader(str(path)) records.append( { - "Game": resource_path.stem, + "Game": game_slug, "Title": game.title, } ) From 6a4df8f6e5dc4da26d14d1c4cc554e271707aaec Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 15:55:11 +0000 Subject: [PATCH 048/119] update test to avoid duplicates --- tests/test_catalog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 84d741022..317e37477 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -40,3 +40,4 @@ def test_catalog_games(): assert "Two-stage matching pennies game" in list(all_games.Title) # Check slug of game in subdir assert "myerson/fig_4_2" in slugs + assert "myerson_fig_4_2" not in slugs From 886131ca36bdc975484540be5a31572e413fa350 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 16:25:32 +0000 Subject: [PATCH 049/119] fix code for handling slugs that duplicates of those in subfolders --- catalog/__init__.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index f6bfd5eb0..1ec77e0a4 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -43,14 +43,19 @@ def games() -> pd.DataFrame: # and remove the suffix to get the "slug" rel_path = resource_path.relative_to(_CATALOG_RESOURCE) game_slug = str(rel_path.with_suffix("")) - - with as_file(resource_path) as path: - game = reader(str(path)) - records.append( - { - "Game": game_slug, - "Title": game.title, - } - ) + bad_slug = False + dir_names = [p.name for p in _CATALOG_RESOURCE.iterdir() if p.is_dir()] + for d in dir_names: + if d in game_slug and d != game_slug and "/" not in game_slug: + bad_slug = True + if not bad_slug: + with as_file(resource_path) as path: + game = reader(str(path)) + records.append( + { + "Game": game_slug, + "Title": game.title, + } + ) return pd.DataFrame.from_records(records, columns=["Game", "Title"]) From 68909b95350aa67ae32b25bf4c94f9a0a5742dfd Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 16:31:02 +0000 Subject: [PATCH 050/119] tidy the games() refactor --- catalog/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index 1ec77e0a4..bba0d8333 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -6,6 +6,7 @@ # Use the full string path to the virtual package we created _CATALOG_RESOURCE = files(__name__) +_CATALOG_SUBDIRS = [p.name for p in _CATALOG_RESOURCE.iterdir() if p.is_dir()] READERS = { ".nfg": gbt.read_nfg, @@ -39,15 +40,19 @@ def games() -> pd.DataFrame: 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) game_slug = str(rel_path.with_suffix("")) + + # This code prevents duplicate slugs e.g. subdir/game1 and subdir_game1 bad_slug = False - dir_names = [p.name for p in _CATALOG_RESOURCE.iterdir() if p.is_dir()] - for d in dir_names: + for d in _CATALOG_SUBDIRS: if d in game_slug and d != game_slug and "/" not in game_slug: bad_slug = True + + # Update the dataframe if not bad_slug: with as_file(resource_path) as path: game = reader(str(path)) From bb8f483be5fd66a419d3d72cbf9285ab635be427 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 16:31:41 +0000 Subject: [PATCH 051/119] resave notebook --- doc/tutorials/01_quickstart.ipynb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/tutorials/01_quickstart.ipynb b/doc/tutorials/01_quickstart.ipynb index 7e4de7599..7bf6e949d 100644 --- a/doc/tutorials/01_quickstart.ipynb +++ b/doc/tutorials/01_quickstart.ipynb @@ -472,7 +472,7 @@ " \n", " \n", " 2\n", - " myerson_fig_4_2\n", + " myerson/fig_4_2\n", " Myerson (1991) Fig 4.2\n", " \n", " \n", @@ -480,6 +480,11 @@ " pd\n", " Two person Prisoner's Dilemma game\n", " \n", + " \n", + " 4\n", + " pd2\n", + " Two person Prisoner's Dilemma game\n", + " \n", " \n", "\n", "" @@ -488,8 +493,9 @@ " Game Title\n", "0 2smp Two-stage matching pennies game\n", "1 2x2x2 2x2x2 Example from McKelvey-McLennan, with 9 N...\n", - "2 myerson_fig_4_2 Myerson (1991) Fig 4.2\n", - "3 pd Two person Prisoner's Dilemma game" + "2 myerson/fig_4_2 Myerson (1991) Fig 4.2\n", + "3 pd Two person Prisoner's Dilemma game\n", + "4 pd2 Two person Prisoner's Dilemma game" ] }, "execution_count": 13, From 50e618b9c9d486c5230e3615737d00d19e0afc78 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 16:51:45 +0000 Subject: [PATCH 052/119] strip nb outputs --- doc/tutorials/01_quickstart.ipynb | 252 ++++-------------------------- 1 file changed, 27 insertions(+), 225 deletions(-) diff --git a/doc/tutorials/01_quickstart.ipynb b/doc/tutorials/01_quickstart.ipynb index 7bf6e949d..62949a4b4 100644 --- a/doc/tutorials/01_quickstart.ipynb +++ b/doc/tutorials/01_quickstart.ipynb @@ -38,7 +38,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "c58d382d", "metadata": {}, "outputs": [], @@ -50,21 +50,10 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "2060c1ed", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pygambit.gambit.Game" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "n_strategies = [2, 2]\n", "g = gbt.Game.new_table(n_strategies, title=\"Prisoner's Dilemma\")\n", @@ -83,7 +72,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "9d8203e8", "metadata": {}, "outputs": [], @@ -111,7 +100,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "61030607", "metadata": {}, "outputs": [], @@ -135,25 +124,10 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "caecc334", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "

Prisoner's Dilemma

\n", - "
CooperateDefect
Cooperate-1,-1-3,0
Defect0,-3-2,-2
\n" - ], - "text/plain": [ - "Game(title='Prisoner's Dilemma')" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# View the payout matrix\n", "g" @@ -189,25 +163,10 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "843ba7f3", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "

Another Prisoner's Dilemma

\n", - "
12
1-1,-1-3,0
20,-3-2,-2
\n" - ], - "text/plain": [ - "Game(title='Another Prisoner's Dilemma')" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "player1_payoffs = np.array([[-1, -3], [0, -2]])\n", "player2_payoffs = np.transpose(player1_payoffs)\n", @@ -233,19 +192,10 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "5ee752c4", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-1\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "tom_payoffs, jerry_payoffs = g.to_arrays(\n", " # dtype=float\n", @@ -270,21 +220,10 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "a81c06c7", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pygambit.nash.NashComputationResult" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "result = gbt.nash.enumpure_solve(g)\n", "type(result)" @@ -300,21 +239,10 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "bd395180", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "1" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "len(result.equilibria)" ] @@ -329,24 +257,10 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "76570ebc", "metadata": {}, - "outputs": [ - { - "data": { - "text/latex": [ - "$\\left[\\left[0,1\\right],\\left[0,1\\right]\\right]$" - ], - "text/plain": [ - "[[Rational(0, 1), Rational(1, 1)], [Rational(0, 1), Rational(1, 1)]]" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "msp = result.equilibria[0]\n", "msp" @@ -354,21 +268,10 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "6e8cfcde", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "pygambit.gambit.MixedStrategyProfileRational" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "type(msp)" ] @@ -385,27 +288,10 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "980bf6b1", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Tom plays the equilibrium strategy:\n", - "Probability of cooperating: 0\n", - "Probability of defecting: 1\n", - "Payoff: -2\n", - "\n", - "Jerry plays the equilibrium strategy:\n", - "Probability of cooperating: 0\n", - "Probability of defecting: 1\n", - "Payoff: -2\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "for player in g.players:\n", " print(f\"{player.label} plays the equilibrium strategy:\")\n", @@ -430,79 +316,10 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "e1b060fb-94cc-432d-a9cc-4ccae5908f79", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
GameTitle
02smpTwo-stage matching pennies game
12x2x22x2x2 Example from McKelvey-McLennan, with 9 N...
2myerson/fig_4_2Myerson (1991) Fig 4.2
3pdTwo person Prisoner's Dilemma game
4pd2Two person Prisoner's Dilemma game
\n", - "
" - ], - "text/plain": [ - " Game Title\n", - "0 2smp Two-stage matching pennies game\n", - "1 2x2x2 2x2x2 Example from McKelvey-McLennan, with 9 N...\n", - "2 myerson/fig_4_2 Myerson (1991) Fig 4.2\n", - "3 pd Two person Prisoner's Dilemma game\n", - "4 pd2 Two person Prisoner's Dilemma game" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "gbt.catalog.games()" ] @@ -517,25 +334,10 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "9ee2c3bd-22d1-4c8e-996c-cd9e0dfd64cc", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "

Two person Prisoner's Dilemma game

\n", - "
12
19,90,10
210,01,1
\n" - ], - "text/plain": [ - "Game(title='Two person Prisoner's Dilemma game')" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "g = gbt.catalog.load(\"pd\")\n", "g" @@ -557,7 +359,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "f58eaa77", "metadata": {}, "outputs": [], @@ -575,7 +377,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "4119a2ac", "metadata": {}, "outputs": [], From 67dedb274e75b859136e000d193af2799df6d4cb Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 16:52:39 +0000 Subject: [PATCH 053/119] remove modification to games() that was fixing a local issue --- catalog/__init__.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index bba0d8333..b740a5504 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -46,21 +46,13 @@ def games() -> pd.DataFrame: rel_path = resource_path.relative_to(_CATALOG_RESOURCE) game_slug = str(rel_path.with_suffix("")) - # This code prevents duplicate slugs e.g. subdir/game1 and subdir_game1 - bad_slug = False - for d in _CATALOG_SUBDIRS: - if d in game_slug and d != game_slug and "/" not in game_slug: - bad_slug = True - - # Update the dataframe - if not bad_slug: - with as_file(resource_path) as path: - game = reader(str(path)) - records.append( - { - "Game": game_slug, - "Title": game.title, - } - ) + with as_file(resource_path) as path: + game = reader(str(path)) + records.append( + { + "Game": game_slug, + "Title": game.title, + } + ) return pd.DataFrame.from_records(records, columns=["Game", "Title"]) From 1794a83b924e1906ede4c24f7934f5ef74424fab Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 17:08:05 +0000 Subject: [PATCH 054/119] fix the update script to get correct paths --- Makefile.am | 2 +- build_support/catalog/update.py | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Makefile.am b/Makefile.am index 14d5552bf..4b31fe0a6 100644 --- a/Makefile.am +++ b/Makefile.am @@ -239,7 +239,7 @@ EXTRA_DIST = \ src/README.rst \ catalog/2smp.efg \ catalog/2x2x2.nfg \ - catalog/fig_4_2.efg \ + catalog/myerson/fig_4_2.efg \ catalog/pd.nfg core_SOURCES = \ diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index c5305d092..e42a18ad9 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -10,13 +10,21 @@ def update_makefile(): """Update the Makefile.am with all games from the catalog.""" - efg_files = list(CATALOG_DIR.rglob("*.efg")) - nfg_files = list(CATALOG_DIR.rglob("*.nfg")) + + # Using rglob("*") to find files in all subdirectories + slugs = [] + for resource_path in sorted(CATALOG_DIR.rglob("*.efg")): + if resource_path.is_file(): + rel_path = resource_path.relative_to(CATALOG_DIR) + slugs.append(str(rel_path)) + for resource_path in sorted(CATALOG_DIR.rglob("*.nfg")): + if resource_path.is_file(): + rel_path = resource_path.relative_to(CATALOG_DIR) + slugs.append(str(rel_path)) game_files = [] - for entry in efg_files + nfg_files: - filename = str(entry).split("/")[-1] - game_files.append(f"catalog/{filename}") + for slug in slugs: + game_files.append(f"catalog/{slug}") game_files.sort() with open(MAKEFILE_AM, encoding="utf-8") as f: From 079aacd2fd84dfaaea82467bf2f51a494a5c32ba Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 17:08:45 +0000 Subject: [PATCH 055/119] remove unused var --- catalog/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index b740a5504..1d2ac8535 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -6,7 +6,6 @@ # Use the full string path to the virtual package we created _CATALOG_RESOURCE = files(__name__) -_CATALOG_SUBDIRS = [p.name for p in _CATALOG_RESOURCE.iterdir() if p.is_dir()] READERS = { ".nfg": gbt.read_nfg, From e57cfce21452e8aea6aa3d6c51bd5155b8cdc036 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 10 Feb 2026 17:18:49 +0000 Subject: [PATCH 056/119] Add Windows handling --- catalog/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/catalog/__init__.py b/catalog/__init__.py index 1d2ac8535..d44200551 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -1,3 +1,4 @@ +import sys from importlib.resources import as_file, files import pandas as pd @@ -17,6 +18,10 @@ def load(slug: str) -> gbt.Game: """ Load a game from the package catalog. """ + # Handle backslashes for Windows + if sys.platform == "win32": + game_slug.replace("/", "\\") # noqa: F821 + for suffix, reader in READERS.items(): resource_path = _CATALOG_RESOURCE / f"{slug}{suffix}" @@ -45,6 +50,10 @@ def games() -> pd.DataFrame: rel_path = resource_path.relative_to(_CATALOG_RESOURCE) game_slug = str(rel_path.with_suffix("")) + # Replace backslashes for Windows + if sys.platform == "win32": + game_slug.replace("\\", "/") # noqa: F821 + with as_file(resource_path) as path: game = reader(str(path)) records.append( From bdc5d3a9366a0832f4fe9283f1b8a27218ca6f24 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 11 Feb 2026 12:10:41 +0000 Subject: [PATCH 057/119] fix incorrect var name and make consistent --- catalog/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index d44200551..fd94bb7c7 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -20,7 +20,7 @@ def load(slug: str) -> gbt.Game: """ # Handle backslashes for Windows if sys.platform == "win32": - game_slug.replace("/", "\\") # noqa: F821 + slug.replace("/", "\\") # noqa: F821 for suffix, reader in READERS.items(): resource_path = _CATALOG_RESOURCE / f"{slug}{suffix}" @@ -48,17 +48,17 @@ def games() -> pd.DataFrame: # Calculate the path relative to the root resource # and remove the suffix to get the "slug" rel_path = resource_path.relative_to(_CATALOG_RESOURCE) - game_slug = str(rel_path.with_suffix("")) + slug = str(rel_path.with_suffix("")) # Replace backslashes for Windows if sys.platform == "win32": - game_slug.replace("\\", "/") # noqa: F821 + slug.replace("\\", "/") # noqa: F821 with as_file(resource_path) as path: game = reader(str(path)) records.append( { - "Game": game_slug, + "Game": slug, "Title": game.title, } ) From 69d8cb968a488f6a6def5c27ceb730df6801acab Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 11 Feb 2026 13:32:48 +0000 Subject: [PATCH 058/119] use as_posix for slugs --- catalog/__init__.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index fd94bb7c7..4a972d917 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -1,5 +1,5 @@ -import sys from importlib.resources import as_file, files +from pathlib import Path import pandas as pd @@ -18,9 +18,7 @@ def load(slug: str) -> gbt.Game: """ Load a game from the package catalog. """ - # Handle backslashes for Windows - if sys.platform == "win32": - slug.replace("/", "\\") # noqa: F821 + slug = str(Path(slug)).replace("\\", "/") for suffix, reader in READERS.items(): resource_path = _CATALOG_RESOURCE / f"{slug}{suffix}" @@ -48,11 +46,7 @@ def games() -> pd.DataFrame: # 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 = str(rel_path.with_suffix("")) - - # Replace backslashes for Windows - if sys.platform == "win32": - slug.replace("\\", "/") # noqa: F821 + slug = rel_path.with_suffix("").as_posix() with as_file(resource_path) as path: game = reader(str(path)) From b045083c81a2f58dd59e768de417fe1e3c475b61 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 13 Feb 2026 15:08:38 +0000 Subject: [PATCH 059/119] move load and games functions from __init__.py to utils.py --- catalog/__init__.py | 64 ++++----------------------------------------- catalog/utils.py | 59 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 59 deletions(-) create mode 100644 catalog/utils.py diff --git a/catalog/__init__.py b/catalog/__init__.py index 4a972d917..0e6b12338 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -1,60 +1,6 @@ -from importlib.resources import as_file, files -from pathlib import Path +from .utils import games, load -import pandas as pd - -import pygambit as gbt - -# Use the full string path to the virtual package we created -_CATALOG_RESOURCE = files(__name__) - -READERS = { - ".nfg": gbt.read_nfg, - ".efg": gbt.read_efg, -} - - -def load(slug: str) -> gbt.Game: - """ - Load a game from the package catalog. - """ - slug = str(Path(slug)).replace("\\", "/") - - for suffix, reader in READERS.items(): - resource_path = _CATALOG_RESOURCE / f"{slug}{suffix}" - - if resource_path.is_file(): - # as_file ensures we have a real filesystem path for the reader - with as_file(resource_path) as path: - return reader(str(path)) - - raise FileNotFoundError(f"No catalog entry called {slug}.nfg or {slug}.efg") - - -def games() -> pd.DataFrame: - """ - List games available in the package catalog, including subdirectories. - """ - records: list[dict[str, str]] = [] - - # Using rglob("*") to find files in all subdirectories - for resource_path in sorted(_CATALOG_RESOURCE.rglob("*")): - reader = READERS.get(resource_path.suffix) - - if reader is not None and resource_path.is_file(): - - # Calculate the path relative to the root resource - # and remove the suffix to get the "slug" - rel_path = resource_path.relative_to(_CATALOG_RESOURCE) - slug = rel_path.with_suffix("").as_posix() - - with as_file(resource_path) as path: - game = reader(str(path)) - records.append( - { - "Game": slug, - "Title": game.title, - } - ) - - return pd.DataFrame.from_records(records, columns=["Game", "Title"]) +__all__ = [ + "load", + "games", +] diff --git a/catalog/utils.py b/catalog/utils.py new file mode 100644 index 000000000..1f70808ed --- /dev/null +++ b/catalog/utils.py @@ -0,0 +1,59 @@ +from importlib.resources import as_file, files +from pathlib import Path + +import pandas as pd + +import pygambit as gbt + +# Use the full string path to the virtual package we created +_CATALOG_RESOURCE = files(__name__) + +READERS = { + ".nfg": gbt.read_nfg, + ".efg": gbt.read_efg, +} + + +def load(slug: str) -> gbt.Game: + """ + Load a game from the package catalog. + """ + slug = str(Path(slug)).replace("\\", "/") + + for suffix, reader in READERS.items(): + resource_path = _CATALOG_RESOURCE / f"{slug}{suffix}" + + if resource_path.is_file(): + # as_file ensures we have a real filesystem path for the reader + with as_file(resource_path) as path: + return reader(str(path)) + + raise FileNotFoundError(f"No catalog entry called {slug}.nfg or {slug}.efg") + + +def games() -> pd.DataFrame: + """ + List games available in the package catalog, including subdirectories. + """ + records: list[dict[str, str]] = [] + + # Using rglob("*") to find files in all subdirectories + for resource_path in sorted(_CATALOG_RESOURCE.rglob("*")): + reader = READERS.get(resource_path.suffix) + + if reader is not None and resource_path.is_file(): + # Calculate the path relative to the root resource + # and remove the suffix to get the "slug" + rel_path = resource_path.relative_to(_CATALOG_RESOURCE) + slug = rel_path.with_suffix("").as_posix() + + with as_file(resource_path) as path: + game = reader(str(path)) + records.append( + { + "Game": slug, + "Title": game.title, + } + ) + + return pd.DataFrame.from_records(records, columns=["Game", "Title"]) From d8ee58e69f48f38b091d19275f64566204910a54 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 13 Feb 2026 15:37:41 +0000 Subject: [PATCH 060/119] add families module --- catalog/families.py | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 catalog/families.py diff --git a/catalog/families.py b/catalog/families.py new file mode 100644 index 000000000..854f4cc79 --- /dev/null +++ b/catalog/families.py @@ -0,0 +1,52 @@ + +import pygambit as gbt + + +def family_games() -> dict[str, gbt.Game]: + """ + Generate a dict of games for inclusion in the catalog, + using the game families in this module. + """ + return { + "one_shot_trust": one_shot_trust(), + "oneshot_trust_unique_NE": one_shot_trust(unique_NE_variant=True), + } + + +################################################################################################ +# Families + +def one_shot_trust(unique_NE_variant: bool = False) -> gbt.Game: + """ + The unique_NE_variant makes Trust a dominant strategy, replacing the + non-singleton equilibrium component from the standard version of the game + where the Buyer plays "Not Trust" and the seller can play any mixture with + < 0.5 probability on Honor with a unique NE where the Buyer plays Trust and + the Seller plays Abuse. + + Parameters + ---------- + unique_NE_variant : bool, optional + Whether to modify the game so that it has a unique Nash equilibrium. + Defaults to False. + + Returns + ------- + gbt.Game + The constructed extensive-form game. + """ + g = gbt.Game.new_tree(players=["Buyer", "Seller"], title="One-shot trust game") + g.description = "One-shot trust game with binary actions, originally from Kreps (1990)." + g.append_move(g.root, "Buyer", ["Trust", "Not trust"]) + g.append_move(g.root.children[0], "Seller", ["Honor", "Abuse"]) + g.set_outcome(g.root.children[0].children[0], g.add_outcome([1, 1], label="Trustworthy")) + if unique_NE_variant: + g.set_outcome( + g.root.children[0].children[1], g.add_outcome(["1/2", 2], label="Untrustworthy") + ) + else: + g.set_outcome( + g.root.children[0].children[1], g.add_outcome([-1, 2], label="Untrustworthy") + ) + g.set_outcome(g.root.children[1], g.add_outcome([0, 0], label="Opt-out")) + return g From a0b66c0aa15f0b0bbc6e6113da810da614b044e0 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 13 Feb 2026 15:46:19 +0000 Subject: [PATCH 061/119] add family games to games() --- catalog/utils.py | 13 ++++++++++++- tests/test_catalog.py | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/catalog/utils.py b/catalog/utils.py index 1f70808ed..35f1c307c 100644 --- a/catalog/utils.py +++ b/catalog/utils.py @@ -5,6 +5,8 @@ import pygambit as gbt +from .families import family_games + # Use the full string path to the virtual package we created _CATALOG_RESOURCE = files(__name__) @@ -37,7 +39,7 @@ def games() -> pd.DataFrame: """ records: list[dict[str, str]] = [] - # Using rglob("*") to find files in all subdirectories + # Add all the games stored as EFG/NFG files for resource_path in sorted(_CATALOG_RESOURCE.rglob("*")): reader = READERS.get(resource_path.suffix) @@ -56,4 +58,13 @@ def games() -> pd.DataFrame: } ) + # Add all the games from families + for slug, game in family_games().items(): + records.append( + { + "Game": slug, + "Title": game.title, + } + ) + return pd.DataFrame.from_records(records, columns=["Game", "Title"]) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 317e37477..3b84cfb89 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -41,3 +41,5 @@ def test_catalog_games(): # Check slug of game in subdir assert "myerson/fig_4_2" in slugs assert "myerson_fig_4_2" not in slugs + # Check family game present + assert "one_shot_trust" in slugs From 939a56fa7bc85f744fcaf8e83ae4bb23ed74286a Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 13 Feb 2026 16:09:52 +0000 Subject: [PATCH 062/119] update load function to look in family games --- catalog/utils.py | 11 ++++++++--- tests/test_catalog.py | 6 ++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/catalog/utils.py b/catalog/utils.py index 35f1c307c..801039dc7 100644 --- a/catalog/utils.py +++ b/catalog/utils.py @@ -22,15 +22,20 @@ def load(slug: str) -> gbt.Game: """ slug = str(Path(slug)).replace("\\", "/") + # Try to load from file for suffix, reader in READERS.items(): resource_path = _CATALOG_RESOURCE / f"{slug}{suffix}" - if resource_path.is_file(): - # as_file ensures we have a real filesystem path for the reader with as_file(resource_path) as path: return reader(str(path)) - raise FileNotFoundError(f"No catalog entry called {slug}.nfg or {slug}.efg") + # Try loading from family games + fg = family_games() + if slug in fg: + return fg[slug] + + # Raise error if game does not exist + raise FileNotFoundError(f"No catalog entry called {slug}") def games() -> pd.DataFrame: diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 3b84cfb89..0f4e3539b 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -30,6 +30,12 @@ def test_catalog_load_subdir_slug(): assert isinstance(g, gbt.Game) +def test_catalog_load_family_game(): + """Test loading a game generated from code with a game family func.""" + g = gbt.catalog.load("one_shot_trust") + assert isinstance(g, gbt.Game) + + def test_catalog_games(): """Test games() function returns df of game slugs and titles""" all_games = gbt.catalog.games() From 1df3db7610a193f7a82fcf0d0ba021e6765401f6 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 13 Feb 2026 16:16:45 +0000 Subject: [PATCH 063/119] alternate titles in example game family --- catalog/families.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/catalog/families.py b/catalog/families.py index 854f4cc79..bcfe1dee5 100644 --- a/catalog/families.py +++ b/catalog/families.py @@ -35,16 +35,18 @@ def one_shot_trust(unique_NE_variant: bool = False) -> gbt.Game: gbt.Game The constructed extensive-form game. """ - g = gbt.Game.new_tree(players=["Buyer", "Seller"], title="One-shot trust game") + g = gbt.Game.new_tree(players=["Buyer", "Seller"]) g.description = "One-shot trust game with binary actions, originally from Kreps (1990)." g.append_move(g.root, "Buyer", ["Trust", "Not trust"]) g.append_move(g.root.children[0], "Seller", ["Honor", "Abuse"]) g.set_outcome(g.root.children[0].children[0], g.add_outcome([1, 1], label="Trustworthy")) if unique_NE_variant: + g.title = "One-shot trust game with unique NE" g.set_outcome( g.root.children[0].children[1], g.add_outcome(["1/2", 2], label="Untrustworthy") ) else: + g.title = "One-shot trust game" g.set_outcome( g.root.children[0].children[1], g.add_outcome([-1, 2], label="Untrustworthy") ) From e0c4b7c39db64f372fc018f10114892e4e25e259 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Fri, 13 Feb 2026 16:36:11 +0000 Subject: [PATCH 064/119] Add to developer doc page --- doc/developer.catalog.rst | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index b60a56705..018e29ac0 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -11,8 +11,8 @@ Currently supported representations are: - `.efg` for extensive form games - `.nfg` for normal form games -Add new games -------------- +Add new game files +------------------ 1. **Create the game file:** @@ -43,3 +43,18 @@ Add new games .. warning:: Make sure you commit all changed files e.g. run ``git add --all`` before committing and pushing. + +Code new games & add game families +---------------------------------- + +1. **Add the game code:** + + Open `catalog/families.py` and create a new function, or modify an existing one. Ensure your function returns a ``Game`` object. + You may wish to vary the game title and/or description based on the chosen parameters. + +2. **Update the catalog:** + + Update the dictionary returned by ``family_games()`` in `catalog/families.py` with all variants of your game(s) you want in the catalog. + Ensure each entry has unique game slug as key (this will be used by ``pygambit.catalog.load('slug')``), and returns a call of the function with specific parameters. + +3. **Submit a pull request to GitHub with all changes.** From 5bea9ea0f7ad8aea2aeeeb3407787fc6cf7b72ef Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 16 Feb 2026 10:04:20 +0000 Subject: [PATCH 065/119] Bump clang-format-version to 18 --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 72efdd0cc..b9579722c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,7 +13,7 @@ jobs: - name: Run clang-format style check for C/C++ uses: jidicula/clang-format-action@v4.16.0 with: - clang-format-version: '17' + clang-format-version: '18' check-path: 'src' include-regex: '^.*\.((((c|C)(c|pp|xx|\+\+)?$)|((h|H)h?(pp|xx|\+\+)?$))|(imp))$' From b63291c1ee7375863514286f1cef8f03fa70b7ed Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 16 Feb 2026 10:06:23 +0000 Subject: [PATCH 066/119] Revert "Bump clang-format-version to 18" This reverts commit 5bea9ea0f7ad8aea2aeeeb3407787fc6cf7b72ef. --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b9579722c..72efdd0cc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,7 +13,7 @@ jobs: - name: Run clang-format style check for C/C++ uses: jidicula/clang-format-action@v4.16.0 with: - clang-format-version: '18' + clang-format-version: '17' check-path: 'src' include-regex: '^.*\.((((c|C)(c|pp|xx|\+\+)?$)|((h|H)h?(pp|xx|\+\+)?$))|(imp))$' From 122c168972c8823fa4f2cb9fca7708ed4a4f43ac Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 17 Feb 2026 10:33:57 +0000 Subject: [PATCH 067/119] move utils.py and families.py back into __init__.py so linux installation from source dist works --- catalog/__init__.py | 130 ++++++++++++++++++++++++++++++++++++++++++-- catalog/families.py | 54 ------------------ catalog/utils.py | 75 ------------------------- 3 files changed, 125 insertions(+), 134 deletions(-) delete mode 100644 catalog/families.py delete mode 100644 catalog/utils.py diff --git a/catalog/__init__.py b/catalog/__init__.py index 0e6b12338..5e2071552 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -1,6 +1,126 @@ -from .utils import games, load +from importlib.resources import as_file, files +from pathlib import Path -__all__ = [ - "load", - "games", -] +import pandas as pd + +import pygambit as gbt + +# Use the full string path to the virtual package we created +_CATALOG_RESOURCE = files(__name__) + +READERS = { + ".nfg": gbt.read_nfg, + ".efg": gbt.read_efg, +} + + +def load(slug: str) -> gbt.Game: + """ + Load a game from the package catalog. + """ + slug = str(Path(slug)).replace("\\", "/") + + # Try to load from file + for suffix, reader in READERS.items(): + resource_path = _CATALOG_RESOURCE / f"{slug}{suffix}" + if resource_path.is_file(): + with as_file(resource_path) as path: + return reader(str(path)) + + # Try loading from family games + fg = family_games() + if slug in fg: + return fg[slug] + + # Raise error if game does not exist + raise FileNotFoundError(f"No catalog entry called {slug}") + + +def games() -> pd.DataFrame: + """ + List games available in the package catalog, including subdirectories. + """ + records: list[dict[str, str]] = [] + + # Add all the games stored as EFG/NFG files + for resource_path in sorted(_CATALOG_RESOURCE.rglob("*")): + reader = READERS.get(resource_path.suffix) + + if reader is not None and resource_path.is_file(): + # Calculate the path relative to the root resource + # and remove the suffix to get the "slug" + rel_path = resource_path.relative_to(_CATALOG_RESOURCE) + slug = rel_path.with_suffix("").as_posix() + + with as_file(resource_path) as path: + game = reader(str(path)) + records.append( + { + "Game": slug, + "Title": game.title, + } + ) + + # Add all the games from families + for slug, game in family_games().items(): + records.append( + { + "Game": slug, + "Title": game.title, + } + ) + + return pd.DataFrame.from_records(records, columns=["Game", "Title"]) + + +def family_games() -> dict[str, gbt.Game]: + """ + Generate a dict of games for inclusion in the catalog, + using the game families in this module. + """ + return { + "one_shot_trust": one_shot_trust(), + "oneshot_trust_unique_NE": one_shot_trust(unique_NE_variant=True), + } + + +################################################################################################ +# Families + + +def one_shot_trust(unique_NE_variant: bool = False) -> gbt.Game: + """ + The unique_NE_variant makes Trust a dominant strategy, replacing the + non-singleton equilibrium component from the standard version of the game + where the Buyer plays "Not Trust" and the seller can play any mixture with + < 0.5 probability on Honor with a unique NE where the Buyer plays Trust and + the Seller plays Abuse. + + Parameters + ---------- + unique_NE_variant : bool, optional + Whether to modify the game so that it has a unique Nash equilibrium. + Defaults to False. + + Returns + ------- + gbt.Game + The constructed extensive-form game. + """ + g = gbt.Game.new_tree(players=["Buyer", "Seller"]) + g.description = "One-shot trust game with binary actions, originally from Kreps (1990)." + g.append_move(g.root, "Buyer", ["Trust", "Not trust"]) + g.append_move(g.root.children[0], "Seller", ["Honor", "Abuse"]) + g.set_outcome(g.root.children[0].children[0], g.add_outcome([1, 1], label="Trustworthy")) + if unique_NE_variant: + g.title = "One-shot trust game with unique NE" + g.set_outcome( + g.root.children[0].children[1], g.add_outcome(["1/2", 2], label="Untrustworthy") + ) + else: + g.title = "One-shot trust game" + g.set_outcome( + g.root.children[0].children[1], g.add_outcome([-1, 2], label="Untrustworthy") + ) + g.set_outcome(g.root.children[1], g.add_outcome([0, 0], label="Opt-out")) + return g diff --git a/catalog/families.py b/catalog/families.py deleted file mode 100644 index bcfe1dee5..000000000 --- a/catalog/families.py +++ /dev/null @@ -1,54 +0,0 @@ - -import pygambit as gbt - - -def family_games() -> dict[str, gbt.Game]: - """ - Generate a dict of games for inclusion in the catalog, - using the game families in this module. - """ - return { - "one_shot_trust": one_shot_trust(), - "oneshot_trust_unique_NE": one_shot_trust(unique_NE_variant=True), - } - - -################################################################################################ -# Families - -def one_shot_trust(unique_NE_variant: bool = False) -> gbt.Game: - """ - The unique_NE_variant makes Trust a dominant strategy, replacing the - non-singleton equilibrium component from the standard version of the game - where the Buyer plays "Not Trust" and the seller can play any mixture with - < 0.5 probability on Honor with a unique NE where the Buyer plays Trust and - the Seller plays Abuse. - - Parameters - ---------- - unique_NE_variant : bool, optional - Whether to modify the game so that it has a unique Nash equilibrium. - Defaults to False. - - Returns - ------- - gbt.Game - The constructed extensive-form game. - """ - g = gbt.Game.new_tree(players=["Buyer", "Seller"]) - g.description = "One-shot trust game with binary actions, originally from Kreps (1990)." - g.append_move(g.root, "Buyer", ["Trust", "Not trust"]) - g.append_move(g.root.children[0], "Seller", ["Honor", "Abuse"]) - g.set_outcome(g.root.children[0].children[0], g.add_outcome([1, 1], label="Trustworthy")) - if unique_NE_variant: - g.title = "One-shot trust game with unique NE" - g.set_outcome( - g.root.children[0].children[1], g.add_outcome(["1/2", 2], label="Untrustworthy") - ) - else: - g.title = "One-shot trust game" - g.set_outcome( - g.root.children[0].children[1], g.add_outcome([-1, 2], label="Untrustworthy") - ) - g.set_outcome(g.root.children[1], g.add_outcome([0, 0], label="Opt-out")) - return g diff --git a/catalog/utils.py b/catalog/utils.py deleted file mode 100644 index 801039dc7..000000000 --- a/catalog/utils.py +++ /dev/null @@ -1,75 +0,0 @@ -from importlib.resources import as_file, files -from pathlib import Path - -import pandas as pd - -import pygambit as gbt - -from .families import family_games - -# Use the full string path to the virtual package we created -_CATALOG_RESOURCE = files(__name__) - -READERS = { - ".nfg": gbt.read_nfg, - ".efg": gbt.read_efg, -} - - -def load(slug: str) -> gbt.Game: - """ - Load a game from the package catalog. - """ - slug = str(Path(slug)).replace("\\", "/") - - # Try to load from file - for suffix, reader in READERS.items(): - resource_path = _CATALOG_RESOURCE / f"{slug}{suffix}" - if resource_path.is_file(): - with as_file(resource_path) as path: - return reader(str(path)) - - # Try loading from family games - fg = family_games() - if slug in fg: - return fg[slug] - - # Raise error if game does not exist - raise FileNotFoundError(f"No catalog entry called {slug}") - - -def games() -> pd.DataFrame: - """ - List games available in the package catalog, including subdirectories. - """ - records: list[dict[str, str]] = [] - - # Add all the games stored as EFG/NFG files - for resource_path in sorted(_CATALOG_RESOURCE.rglob("*")): - reader = READERS.get(resource_path.suffix) - - if reader is not None and resource_path.is_file(): - # Calculate the path relative to the root resource - # and remove the suffix to get the "slug" - rel_path = resource_path.relative_to(_CATALOG_RESOURCE) - slug = rel_path.with_suffix("").as_posix() - - with as_file(resource_path) as path: - game = reader(str(path)) - records.append( - { - "Game": slug, - "Title": game.title, - } - ) - - # Add all the games from families - for slug, game in family_games().items(): - records.append( - { - "Game": slug, - "Title": game.title, - } - ) - - return pd.DataFrame.from_records(records, columns=["Game", "Title"]) From 6b863d7203521e708e2d6513b6ee53ec6ef91fd2 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 17 Feb 2026 10:48:49 +0000 Subject: [PATCH 068/119] add slug collision error --- catalog/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/catalog/__init__.py b/catalog/__init__.py index 5e2071552..1cd30f48f 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -63,6 +63,12 @@ def games() -> pd.DataFrame: # Add all the games from families for slug, game in family_games().items(): + # Throw an error if there's a slug collision between family games and file-based games + if slug in records: + raise ValueError( + f"Slug collision: {slug} is present in both file-based and " + "family games." + ) records.append( { "Game": slug, From 196b8d674b8aedf4ce67da79265917b27d0b3097 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 17 Feb 2026 10:51:16 +0000 Subject: [PATCH 069/119] update doc to reflect move back to __init__.py --- doc/developer.catalog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index 018e29ac0..9a205d82a 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -49,12 +49,12 @@ Code new games & add game families 1. **Add the game code:** - Open `catalog/families.py` and create a new function, or modify an existing one. Ensure your function returns a ``Game`` object. + Open `catalog/__init__.py` and create a new function, or modify an existing one. Ensure your function returns a ``Game`` object. You may wish to vary the game title and/or description based on the chosen parameters. 2. **Update the catalog:** - Update the dictionary returned by ``family_games()`` in `catalog/families.py` with all variants of your game(s) you want in the catalog. + Update the dictionary returned by ``family_games()`` in `catalog/__init__.py` with all variants of your game(s) you want in the catalog. Ensure each entry has unique game slug as key (this will be used by ``pygambit.catalog.load('slug')``), and returns a call of the function with specific parameters. 3. **Submit a pull request to GitHub with all changes.** From 82e5154b5e6f682b387441b20909103ac79d4fac Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 19 Feb 2026 11:35:42 +0000 Subject: [PATCH 070/119] add tests for filtering options based on game object attributes --- tests/test_catalog.py | 88 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 0f4e3539b..a22b168ca 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -49,3 +49,91 @@ def test_catalog_games(): assert "myerson_fig_4_2" not in slugs # Check family game present assert "one_shot_trust" in slugs + + +def test_catalog_games_filter_n_actions(): + """Test games() function can filter on length of gbt.Game attribute 'actions'""" + all_games = gbt.catalog.games() + filtered_games = gbt.catalog.games(n_actions=2) + assert len(filtered_games) < len(all_games) + assert all(len(g.actions) == 2 for g in filtered_games) + + +def test_catalog_games_filter_n_contingencies(): + """Test games() function can filter on length of gbt.Game attribute 'contingencies'""" + all_games = gbt.catalog.games() + filtered_games = gbt.catalog.games(n_contingencies=2) + assert len(filtered_games) < len(all_games) + assert all(len(g.contingencies) == 2 for g in filtered_games) + + +def test_catalog_games_filter_n_info_sets(): + """Test games() function can filter on length of gbt.Game attribute 'info_sets'""" + all_games = gbt.catalog.games() + filtered_games = gbt.catalog.games(n_info_sets=2) + assert len(filtered_games) < len(all_games) + assert all(len(g.info_sets) == 2 for g in filtered_games) + + +def test_catalog_games_filter_is_constant_sum(): + """Test games() function can filter on boolean gbt.Game attribute 'is_constant_sum'""" + all_games = gbt.catalog.games() + filtered_games = gbt.catalog.games(is_constant_sum=True) + assert len(filtered_games) < len(all_games) + assert all(filtered_games.is_constant_sum) + + +def test_catalog_games_filter_is_perfect_recall(): + """Test games() function can filter on boolean gbt.Game attribute 'is_perfect_recall'""" + all_games = gbt.catalog.games() + filtered_games = gbt.catalog.games(is_perfect_recall=True) + assert len(filtered_games) < len(all_games) + assert all(filtered_games.is_perfect_recall) + + +def test_catalog_games_filter_is_tree(): + """Test games() function can filter on boolean gbt.Game attribute 'is_tree'""" + all_games = gbt.catalog.games() + filtered_games = gbt.catalog.games(is_tree=True) + assert len(filtered_games) < len(all_games) + assert all(filtered_games.is_tree) + + +def test_catalog_games_filter_min_payoff_and_max_payoff(): + """Test games() function can filter on min and max payoff values""" + all_games = gbt.catalog.games() + filtered_games = gbt.catalog.games(min_payoff=0, max_payoff=10) + assert len(filtered_games) < len(all_games) + assert all((filtered_games.min_payoff >= 0) & (filtered_games.max_payoff <= 10)) + + +def test_catalog_games_filter_n_nodes(): + """Test games() function can filter on length of gbt.Game attribute 'nodes'""" + all_games = gbt.catalog.games() + filtered_games = gbt.catalog.games(n_nodes=5) + assert len(filtered_games) < len(all_games) + assert all(len(g.nodes) == 5 for g in filtered_games) + + +def test_catalog_games_filter_n_outcomes(): + """Test games() function can filter on length of gbt.Game attribute 'outcomes'""" + all_games = gbt.catalog.games() + filtered_games = gbt.catalog.games(n_outcomes=3) + assert len(filtered_games) < len(all_games) + assert all(len(g.outcomes) == 3 for g in filtered_games) + + +def test_catalog_games_filter_n_players(): + """Test games() function can filter on length of gbt.Game attribute 'players'""" + all_games = gbt.catalog.games() + filtered_games = gbt.catalog.games(n_players=2) + assert len(filtered_games) < len(all_games) + assert all(len(g.players) == 2 for g in filtered_games) + + +def test_catalog_games_filter_n_strategies(): + """Test games() function can filter on length of gbt.Game attribute 'strategies'""" + all_games = gbt.catalog.games() + filtered_games = gbt.catalog.games(n_strategies=4) + assert len(filtered_games) < len(all_games) + assert all(len(g.strategies) == 4 for g in filtered_games) From f9e04244e9c1b2f1860a881e80b7ddabc8b700c3 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 19 Feb 2026 11:41:30 +0000 Subject: [PATCH 071/119] use a fixture for all_games --- tests/test_catalog.py | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index a22b168ca..2fee1198f 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -4,6 +4,11 @@ import pygambit as gbt +@pytest.fixture(scope="module") +def all_games(): + return gbt.catalog.games() + + def test_catalog_load_efg(): """Test loading an extensive form game""" g = gbt.catalog.load("2smp") @@ -36,9 +41,8 @@ def test_catalog_load_family_game(): assert isinstance(g, gbt.Game) -def test_catalog_games(): +def test_catalog_games(all_games): """Test games() function returns df of game slugs and titles""" - all_games = gbt.catalog.games() slugs = list(all_games.Game) assert isinstance(all_games, pd.DataFrame) assert len(all_games) > 0 @@ -51,89 +55,78 @@ def test_catalog_games(): assert "one_shot_trust" in slugs -def test_catalog_games_filter_n_actions(): +def test_catalog_games_filter_n_actions(all_games): """Test games() function can filter on length of gbt.Game attribute 'actions'""" - all_games = gbt.catalog.games() filtered_games = gbt.catalog.games(n_actions=2) assert len(filtered_games) < len(all_games) assert all(len(g.actions) == 2 for g in filtered_games) -def test_catalog_games_filter_n_contingencies(): +def test_catalog_games_filter_n_contingencies(all_games): """Test games() function can filter on length of gbt.Game attribute 'contingencies'""" - all_games = gbt.catalog.games() filtered_games = gbt.catalog.games(n_contingencies=2) assert len(filtered_games) < len(all_games) assert all(len(g.contingencies) == 2 for g in filtered_games) -def test_catalog_games_filter_n_info_sets(): +def test_catalog_games_filter_n_info_sets(all_games): """Test games() function can filter on length of gbt.Game attribute 'info_sets'""" - all_games = gbt.catalog.games() filtered_games = gbt.catalog.games(n_info_sets=2) assert len(filtered_games) < len(all_games) assert all(len(g.info_sets) == 2 for g in filtered_games) -def test_catalog_games_filter_is_constant_sum(): +def test_catalog_games_filter_is_constant_sum(all_games): """Test games() function can filter on boolean gbt.Game attribute 'is_constant_sum'""" - all_games = gbt.catalog.games() filtered_games = gbt.catalog.games(is_constant_sum=True) assert len(filtered_games) < len(all_games) assert all(filtered_games.is_constant_sum) -def test_catalog_games_filter_is_perfect_recall(): +def test_catalog_games_filter_is_perfect_recall(all_games): """Test games() function can filter on boolean gbt.Game attribute 'is_perfect_recall'""" - all_games = gbt.catalog.games() filtered_games = gbt.catalog.games(is_perfect_recall=True) assert len(filtered_games) < len(all_games) assert all(filtered_games.is_perfect_recall) -def test_catalog_games_filter_is_tree(): +def test_catalog_games_filter_is_tree(all_games): """Test games() function can filter on boolean gbt.Game attribute 'is_tree'""" - all_games = gbt.catalog.games() filtered_games = gbt.catalog.games(is_tree=True) assert len(filtered_games) < len(all_games) assert all(filtered_games.is_tree) -def test_catalog_games_filter_min_payoff_and_max_payoff(): +def test_catalog_games_filter_min_payoff_and_max_payoff(all_games): """Test games() function can filter on min and max payoff values""" - all_games = gbt.catalog.games() filtered_games = gbt.catalog.games(min_payoff=0, max_payoff=10) assert len(filtered_games) < len(all_games) assert all((filtered_games.min_payoff >= 0) & (filtered_games.max_payoff <= 10)) -def test_catalog_games_filter_n_nodes(): +def test_catalog_games_filter_n_nodes(all_games): """Test games() function can filter on length of gbt.Game attribute 'nodes'""" - all_games = gbt.catalog.games() filtered_games = gbt.catalog.games(n_nodes=5) assert len(filtered_games) < len(all_games) assert all(len(g.nodes) == 5 for g in filtered_games) -def test_catalog_games_filter_n_outcomes(): +def test_catalog_games_filter_n_outcomes(all_games): """Test games() function can filter on length of gbt.Game attribute 'outcomes'""" - all_games = gbt.catalog.games() filtered_games = gbt.catalog.games(n_outcomes=3) assert len(filtered_games) < len(all_games) assert all(len(g.outcomes) == 3 for g in filtered_games) -def test_catalog_games_filter_n_players(): +def test_catalog_games_filter_n_players(all_games): """Test games() function can filter on length of gbt.Game attribute 'players'""" - all_games = gbt.catalog.games() filtered_games = gbt.catalog.games(n_players=2) assert len(filtered_games) < len(all_games) assert all(len(g.players) == 2 for g in filtered_games) -def test_catalog_games_filter_n_strategies(): +def test_catalog_games_filter_n_strategies(all_games): """Test games() function can filter on length of gbt.Game attribute 'strategies'""" - all_games = gbt.catalog.games() filtered_games = gbt.catalog.games(n_strategies=4) assert len(filtered_games) < len(all_games) assert all(len(g.strategies) == 4 for g in filtered_games) From 95ba60b74f071f5bae61dcf02ead9b4de167649b Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 19 Feb 2026 12:10:09 +0000 Subject: [PATCH 072/119] update tests to assume df always returned by games() --- tests/test_catalog.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 2fee1198f..0aa4ba28f 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -58,75 +58,86 @@ def test_catalog_games(all_games): def test_catalog_games_filter_n_actions(all_games): """Test games() function can filter on length of gbt.Game attribute 'actions'""" filtered_games = gbt.catalog.games(n_actions=2) + assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(len(g.actions) == 2 for g in filtered_games) + assert all(len(g.actions) == 2 for g in list(filtered_games.Game)) def test_catalog_games_filter_n_contingencies(all_games): """Test games() function can filter on length of gbt.Game attribute 'contingencies'""" filtered_games = gbt.catalog.games(n_contingencies=2) + assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(len(g.contingencies) == 2 for g in filtered_games) + assert all(len(g.contingencies) == 2 for g in list(filtered_games.Game)) def test_catalog_games_filter_n_info_sets(all_games): """Test games() function can filter on length of gbt.Game attribute 'info_sets'""" filtered_games = gbt.catalog.games(n_info_sets=2) + assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(len(g.info_sets) == 2 for g in filtered_games) + assert all(len(g.info_sets) == 2 for g in list(filtered_games.Game)) def test_catalog_games_filter_is_constant_sum(all_games): """Test games() function can filter on boolean gbt.Game attribute 'is_constant_sum'""" filtered_games = gbt.catalog.games(is_constant_sum=True) + assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(filtered_games.is_constant_sum) + assert all(g.is_constant_sum for g in list(filtered_games.Game)) def test_catalog_games_filter_is_perfect_recall(all_games): """Test games() function can filter on boolean gbt.Game attribute 'is_perfect_recall'""" filtered_games = gbt.catalog.games(is_perfect_recall=True) + assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(filtered_games.is_perfect_recall) + assert all(g.is_perfect_recall for g in list(filtered_games.Game)) def test_catalog_games_filter_is_tree(all_games): """Test games() function can filter on boolean gbt.Game attribute 'is_tree'""" filtered_games = gbt.catalog.games(is_tree=True) + assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(filtered_games.is_tree) + assert all(g.is_tree for g in list(filtered_games.Game)) def test_catalog_games_filter_min_payoff_and_max_payoff(all_games): """Test games() function can filter on min and max payoff values""" filtered_games = gbt.catalog.games(min_payoff=0, max_payoff=10) + assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all((filtered_games.min_payoff >= 0) & (filtered_games.max_payoff <= 10)) + assert all(g.min_payoff >= 0 and g.max_payoff <= 10 for g in list(filtered_games.Game)) def test_catalog_games_filter_n_nodes(all_games): """Test games() function can filter on length of gbt.Game attribute 'nodes'""" filtered_games = gbt.catalog.games(n_nodes=5) + assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(len(g.nodes) == 5 for g in filtered_games) + assert all(len(g.nodes) == 5 for g in list(filtered_games.Game)) def test_catalog_games_filter_n_outcomes(all_games): """Test games() function can filter on length of gbt.Game attribute 'outcomes'""" filtered_games = gbt.catalog.games(n_outcomes=3) + assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(len(g.outcomes) == 3 for g in filtered_games) + assert all(len(g.outcomes) == 3 for g in list(filtered_games.Game)) def test_catalog_games_filter_n_players(all_games): """Test games() function can filter on length of gbt.Game attribute 'players'""" filtered_games = gbt.catalog.games(n_players=2) + assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(len(g.players) == 2 for g in filtered_games) + assert all(len(g.players) == 2 for g in list(filtered_games.Game)) def test_catalog_games_filter_n_strategies(all_games): """Test games() function can filter on length of gbt.Game attribute 'strategies'""" filtered_games = gbt.catalog.games(n_strategies=4) + assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(len(g.strategies) == 4 for g in filtered_games) + assert all(len(g.strategies) == 4 for g in list(filtered_games.Game)) From 39f8c2758b8c4fe80f8501f38a88d8b482e89652 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 19 Feb 2026 13:24:25 +0000 Subject: [PATCH 073/119] add check_filters --- catalog/__init__.py | 78 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index 1cd30f48f..ca621f9a9 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -1,5 +1,6 @@ from importlib.resources import as_file, files from pathlib import Path +from typing import Any import pandas as pd @@ -36,11 +37,41 @@ def load(slug: str) -> gbt.Game: raise FileNotFoundError(f"No catalog entry called {slug}") -def games() -> pd.DataFrame: +def games(**kwargs) -> pd.DataFrame: """ List games available in the package catalog, including subdirectories. + + If no arguments are provided, returns a pandas DataFrame with columns "Game" and "Title". + + If arguments are provided, they are treated as filters and a pandas DataFrame is returned + where the "Game" column contains pygambit.Game objects matching the criteria. """ - records: list[dict[str, str]] = [] + records: list[dict[str, Any]] = [] + + def check_filters(game: gbt.Game) -> bool: + if "n_actions" in kwargs and len(game.actions) != kwargs["n_actions"]: + return False + if "n_contingencies" in kwargs and len(game.contingencies) != kwargs["n_contingencies"]: + return False + if "n_info_sets" in kwargs and len(game.info_sets) != kwargs["n_info_sets"]: + return False + if "is_constant_sum" in kwargs and game.is_constant_sum != kwargs["is_constant_sum"]: + return False + if "is_perfect_recall" in kwargs and game.is_perfect_recall != kwargs["is_perfect_recall"]: + return False + if "is_tree" in kwargs and game.is_tree != kwargs["is_tree"]: + return False + if "min_payoff" in kwargs and game.min_payoff < kwargs["min_payoff"]: + return False + if "max_payoff" in kwargs and game.max_payoff > kwargs["max_payoff"]: + return False + if "n_nodes" in kwargs and len(game.nodes) != kwargs["n_nodes"]: + return False + if "n_outcomes" in kwargs and len(game.outcomes) != kwargs["n_outcomes"]: + return False + if "n_players" in kwargs and len(game.players) != kwargs["n_players"]: + return False + return not ("n_strategies" in kwargs and len(game.strategies) != kwargs["n_strategies"]) # Add all the games stored as EFG/NFG files for resource_path in sorted(_CATALOG_RESOURCE.rglob("*")): @@ -54,27 +85,40 @@ def games() -> pd.DataFrame: with as_file(resource_path) as path: game = reader(str(path)) - records.append( - { - "Game": slug, - "Title": game.title, - } - ) + if kwargs: + if check_filters(game): + records.append({ + "Game": game, + "Title": game.title, + }) + else: + records.append( + { + "Game": slug, + "Title": game.title, + } + ) # Add all the games from families for slug, game in family_games().items(): # Throw an error if there's a slug collision between family games and file-based games - if slug in records: + if slug in [r["Game"] for r in records]: raise ValueError( - f"Slug collision: {slug} is present in both file-based and " - "family games." + f"Slug collision: {slug} is present in both file-based and family games." + ) + if kwargs: + if check_filters(game): + records.append({ + "Game": game, + "Title": game.title, + }) + else: + records.append( + { + "Game": slug, + "Title": game.title, + } ) - records.append( - { - "Game": slug, - "Title": game.title, - } - ) return pd.DataFrame.from_records(records, columns=["Game", "Title"]) From 6196a0bb0e9c368851e4eca40af85434456b5fe4 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 19 Feb 2026 13:40:17 +0000 Subject: [PATCH 074/119] Fix filter names and rules --- catalog/__init__.py | 11 +++++++---- tests/test_catalog.py | 16 ++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index ca621f9a9..1398b56d4 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -53,9 +53,9 @@ def check_filters(game: gbt.Game) -> bool: return False if "n_contingencies" in kwargs and len(game.contingencies) != kwargs["n_contingencies"]: return False - if "n_info_sets" in kwargs and len(game.info_sets) != kwargs["n_info_sets"]: + if "n_infosets" in kwargs and len(game.infosets) != kwargs["n_infosets"]: return False - if "is_constant_sum" in kwargs and game.is_constant_sum != kwargs["is_constant_sum"]: + if "is_const_sum" in kwargs and game.is_const_sum != kwargs["is_const_sum"]: return False if "is_perfect_recall" in kwargs and game.is_perfect_recall != kwargs["is_perfect_recall"]: return False @@ -65,8 +65,11 @@ def check_filters(game: gbt.Game) -> bool: return False if "max_payoff" in kwargs and game.max_payoff > kwargs["max_payoff"]: return False - if "n_nodes" in kwargs and len(game.nodes) != kwargs["n_nodes"]: - return False + if "n_nodes" in kwargs: + if not game.is_tree: + return False + if len(game.nodes) != kwargs["n_nodes"]: + return False if "n_outcomes" in kwargs and len(game.outcomes) != kwargs["n_outcomes"]: return False if "n_players" in kwargs and len(game.players) != kwargs["n_players"]: diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 0aa4ba28f..84511ed5d 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -71,20 +71,20 @@ def test_catalog_games_filter_n_contingencies(all_games): assert all(len(g.contingencies) == 2 for g in list(filtered_games.Game)) -def test_catalog_games_filter_n_info_sets(all_games): - """Test games() function can filter on length of gbt.Game attribute 'info_sets'""" - filtered_games = gbt.catalog.games(n_info_sets=2) +def test_catalog_games_filter_n_infosets(all_games): + """Test games() function can filter on length of gbt.Game attribute 'infosets'""" + filtered_games = gbt.catalog.games(n_infosets=2) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(len(g.info_sets) == 2 for g in list(filtered_games.Game)) + assert all(len(g.infosets) == 2 for g in list(filtered_games.Game)) -def test_catalog_games_filter_is_constant_sum(all_games): - """Test games() function can filter on boolean gbt.Game attribute 'is_constant_sum'""" - filtered_games = gbt.catalog.games(is_constant_sum=True) +def test_catalog_games_filter_is_const_sum(all_games): + """Test games() function can filter on boolean gbt.Game attribute 'is_const_sum'""" + filtered_games = gbt.catalog.games(is_const_sum=True) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(g.is_constant_sum for g in list(filtered_games.Game)) + assert all(g.is_const_sum for g in list(filtered_games.Game)) def test_catalog_games_filter_is_perfect_recall(all_games): From 63f767f8e4beca96f5b1e3db97c906b261c7da2e Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 19 Feb 2026 13:45:55 +0000 Subject: [PATCH 075/119] comment out perfect recall test --- tests/test_catalog.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 84511ed5d..11d86a816 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -87,12 +87,13 @@ def test_catalog_games_filter_is_const_sum(all_games): assert all(g.is_const_sum for g in list(filtered_games.Game)) -def test_catalog_games_filter_is_perfect_recall(all_games): - """Test games() function can filter on boolean gbt.Game attribute 'is_perfect_recall'""" - filtered_games = gbt.catalog.games(is_perfect_recall=True) - assert isinstance(filtered_games, pd.DataFrame) - assert len(filtered_games) < len(all_games) - assert all(g.is_perfect_recall for g in list(filtered_games.Game)) +# TODO: Re-introduce this test when there are examples in the catalog without perfect recall +# def test_catalog_games_filter_is_perfect_recall(all_games): +# """Test games() function can filter on boolean gbt.Game attribute 'is_perfect_recall'""" +# filtered_games = gbt.catalog.games(is_perfect_recall=True) +# assert isinstance(filtered_games, pd.DataFrame) +# assert len(filtered_games) < len(all_games) +# assert all(g.is_perfect_recall for g in list(filtered_games.Game)) def test_catalog_games_filter_is_tree(all_games): From 87eb32dc27f4de176bdc08929e7c648849f4d783 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 19 Feb 2026 13:53:54 +0000 Subject: [PATCH 076/119] Make sure tree specific filters handled correctly --- catalog/__init__.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index 1398b56d4..d16ef1844 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -49,12 +49,18 @@ def games(**kwargs) -> pd.DataFrame: records: list[dict[str, Any]] = [] def check_filters(game: gbt.Game) -> bool: - if "n_actions" in kwargs and len(game.actions) != kwargs["n_actions"]: - return False + if "n_actions" in kwargs: + if not game.is_tree: + return False + if len(game.actions) != kwargs["n_actions"]: + return False if "n_contingencies" in kwargs and len(game.contingencies) != kwargs["n_contingencies"]: return False - if "n_infosets" in kwargs and len(game.infosets) != kwargs["n_infosets"]: - return False + if "n_infosets" in kwargs: + if not game.is_tree: + return False + if len(game.infosets) != kwargs["n_infosets"]: + return False if "is_const_sum" in kwargs and game.is_const_sum != kwargs["is_const_sum"]: return False if "is_perfect_recall" in kwargs and game.is_perfect_recall != kwargs["is_perfect_recall"]: From d245e8fc87ac3f583bfdc883a59a0c50ef9b7823 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 26 Feb 2026 09:40:58 +0000 Subject: [PATCH 077/119] remove notebook 1 changes --- doc/tutorials/01_quickstart.ipynb | 40 ------------------------------- 1 file changed, 40 deletions(-) diff --git a/doc/tutorials/01_quickstart.ipynb b/doc/tutorials/01_quickstart.ipynb index 07ccc7eb6..388891ae0 100644 --- a/doc/tutorials/01_quickstart.ipynb +++ b/doc/tutorials/01_quickstart.ipynb @@ -309,46 +309,6 @@ "The equilibrium shows that both players are playing their dominant strategy, which is to defect. This is because defecting is the best response to the other player's strategy, regardless of what that strategy is." ] }, - { - "cell_type": "markdown", - "id": "3dfbf327", - "metadata": {}, - "source": [ - "Loading games from the catalog \n", - "------------------------------\n", - "\n", - "Gambit includes a catalog of standard games that can be loaded directly by name. You can list all the available games like so:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e1b060fb-94cc-432d-a9cc-4ccae5908f79", - "metadata": {}, - "outputs": [], - "source": [ - "gbt.catalog.games()" - ] - }, - { - "cell_type": "markdown", - "id": "3030ee7e-2d5e-4560-ab1b-7c865d0fe19d", - "metadata": {}, - "source": [ - "You can then load a specific game by its name. For example, to load the \"Prisoner's Dilemma\" game from the catalog, you would do the following:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9ee2c3bd-22d1-4c8e-996c-cd9e0dfd64cc", - "metadata": {}, - "outputs": [], - "source": [ - "g = gbt.catalog.load(\"pd\")\n", - "g" - ] - }, { "cell_type": "markdown", "id": "24f36b0d", From 957915098f5b155692939d09280348f5bce904fd Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 26 Feb 2026 09:42:06 +0000 Subject: [PATCH 078/119] revert makefile changes --- Makefile.am | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile.am b/Makefile.am index a2cbb62bd..84434fe9a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -165,6 +165,7 @@ EXTRA_DIST = \ contrib/games/my_3-3d.efg \ contrib/games/my_3-3e.efg \ contrib/games/my_3-4.efg \ + contrib/games/myerson.efg \ contrib/games/nim7.efg \ contrib/games/nim.efg \ contrib/games/palf2.efg \ @@ -190,6 +191,7 @@ EXTRA_DIST = \ contrib/games/2x2a.nfg \ contrib/games/2x2const.nfg \ contrib/games/2x2.nfg \ + contrib/games/2x2x2.nfg \ contrib/games/2x2x2x2.nfg \ contrib/games/2x2x2x2x2.nfg \ contrib/games/3x3x3.nfg \ @@ -216,6 +218,7 @@ EXTRA_DIST = \ contrib/games/mixdom2.nfg \ contrib/games/mixdom.nfg \ contrib/games/oneill.nfg \ + contrib/games/pd.nfg \ contrib/games/perfect1.nfg \ contrib/games/perfect2.nfg \ contrib/games/perfect3.nfg \ From da8a4a1022e188b819fcf7c2ca744d3c30f5c403 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 26 Feb 2026 09:42:47 +0000 Subject: [PATCH 079/119] remove catalog games added by merge --- catalog/2x2x2.nfg | 19 ------------------- catalog/pd.nfg | 14 -------------- 2 files changed, 33 deletions(-) delete mode 100644 catalog/2x2x2.nfg delete mode 100644 catalog/pd.nfg diff --git a/catalog/2x2x2.nfg b/catalog/2x2x2.nfg deleted file mode 100644 index 8c97914fe..000000000 --- a/catalog/2x2x2.nfg +++ /dev/null @@ -1,19 +0,0 @@ -NFG 1 R "2x2x2 Example from McKelvey-McLennan, with 9 Nash equilibria, 2 totally mixed" { "Player 1" "Player 2" "Player 3" } - -{ { "1" "2" } -{ "1" "2" } -{ "1" "2" } -} -"" - -{ -{ "" 9, 8, 12 } -{ "" 0, 0, 0 } -{ "" 0, 0, 0 } -{ "" 9, 8, 2 } -{ "" 0, 0, 0 } -{ "" 3, 4, 6 } -{ "" 3, 4, 6 } -{ "" 0, 0, 0 } -} -1 2 3 4 5 6 7 8 diff --git a/catalog/pd.nfg b/catalog/pd.nfg deleted file mode 100644 index 3ff94afd5..000000000 --- a/catalog/pd.nfg +++ /dev/null @@ -1,14 +0,0 @@ -NFG 1 R "Two person Prisoner's Dilemma game" { "Player 1" "Player 2" } - -{ { "1" "2" } -{ "1" "2" } -} -"" - -{ -{ "" 9, 9 } -{ "" 10, 0 } -{ "" 0, 10 } -{ "" 1, 1 } -} -1 2 3 4 From 5e712af56db5f4e62760d635b84bb02b0bef42b4 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 26 Feb 2026 09:43:29 +0000 Subject: [PATCH 080/119] and put them back in contrib --- contrib/games/2x2x2.nfg | 19 +++++++++++++++++++ contrib/games/pd.nfg | 14 ++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 contrib/games/2x2x2.nfg create mode 100644 contrib/games/pd.nfg diff --git a/contrib/games/2x2x2.nfg b/contrib/games/2x2x2.nfg new file mode 100644 index 000000000..8c97914fe --- /dev/null +++ b/contrib/games/2x2x2.nfg @@ -0,0 +1,19 @@ +NFG 1 R "2x2x2 Example from McKelvey-McLennan, with 9 Nash equilibria, 2 totally mixed" { "Player 1" "Player 2" "Player 3" } + +{ { "1" "2" } +{ "1" "2" } +{ "1" "2" } +} +"" + +{ +{ "" 9, 8, 12 } +{ "" 0, 0, 0 } +{ "" 0, 0, 0 } +{ "" 9, 8, 2 } +{ "" 0, 0, 0 } +{ "" 3, 4, 6 } +{ "" 3, 4, 6 } +{ "" 0, 0, 0 } +} +1 2 3 4 5 6 7 8 diff --git a/contrib/games/pd.nfg b/contrib/games/pd.nfg new file mode 100644 index 000000000..3ff94afd5 --- /dev/null +++ b/contrib/games/pd.nfg @@ -0,0 +1,14 @@ +NFG 1 R "Two person Prisoner's Dilemma game" { "Player 1" "Player 2" } + +{ { "1" "2" } +{ "1" "2" } +} +"" + +{ +{ "" 9, 9 } +{ "" 10, 0 } +{ "" 0, 10 } +{ "" 1, 1 } +} +1 2 3 4 From f39f34cda99cf85d96df50cab80df9b2e43ead10 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 26 Feb 2026 10:17:11 +0000 Subject: [PATCH 081/119] Add tests for games filtering --- tests/test_catalog.py | 95 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index c1f9737d4..3fe0c0ac1 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -38,12 +38,101 @@ def test_catalog_load_family_game(): def test_catalog_games(all_games): """Test games() function returns df of game slugs and titles""" - all_games = gbt.catalog.games() slugs = list(all_games.Game) assert isinstance(all_games, pd.DataFrame) assert len(all_games) > 0 # Check slug of game in subdir - assert "myerson/fig_4_2" in slugs - assert "myerson_fig_4_2" not in slugs + assert "myerson1991/fig4_2" in slugs # Check family game present assert "one_shot_trust" in slugs + # Check a known game title is present + assert "Myerson (1991) Figure 4.2" in list(all_games.Title) + + +def test_catalog_games_filter_n_actions(all_games): + """Test games() function can filter on length of gbt.Game attribute 'actions'""" + filtered_games = gbt.catalog.games(n_actions=2) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + assert all(len(g.actions) == 2 for g in list(filtered_games.Game)) + + +def test_catalog_games_filter_n_contingencies(all_games): + """Test games() function can filter on length of gbt.Game attribute 'contingencies'""" + filtered_games = gbt.catalog.games(n_contingencies=2) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + assert all(len(g.contingencies) == 2 for g in list(filtered_games.Game)) + + +def test_catalog_games_filter_n_infosets(all_games): + """Test games() function can filter on length of gbt.Game attribute 'infosets'""" + filtered_games = gbt.catalog.games(n_infosets=2) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + assert all(len(g.infosets) == 2 for g in list(filtered_games.Game)) + + +def test_catalog_games_filter_is_const_sum(all_games): + """Test games() function can filter on boolean gbt.Game attribute 'is_const_sum'""" + filtered_games = gbt.catalog.games(is_const_sum=True) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + assert all(g.is_const_sum for g in list(filtered_games.Game)) + + +# TODO: Re-introduce this test when there are examples in the catalog without perfect recall +# def test_catalog_games_filter_is_perfect_recall(all_games): +# """Test games() function can filter on boolean gbt.Game attribute 'is_perfect_recall'""" +# filtered_games = gbt.catalog.games(is_perfect_recall=True) +# assert isinstance(filtered_games, pd.DataFrame) +# assert len(filtered_games) < len(all_games) +# assert all(g.is_perfect_recall for g in list(filtered_games.Game)) + + +def test_catalog_games_filter_is_tree(all_games): + """Test games() function can filter on boolean gbt.Game attribute 'is_tree'""" + filtered_games = gbt.catalog.games(is_tree=True) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + assert all(g.is_tree for g in list(filtered_games.Game)) + + +def test_catalog_games_filter_min_payoff_and_max_payoff(all_games): + """Test games() function can filter on min and max payoff values""" + filtered_games = gbt.catalog.games(min_payoff=0, max_payoff=10) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + assert all(g.min_payoff >= 0 and g.max_payoff <= 10 for g in list(filtered_games.Game)) + + +def test_catalog_games_filter_n_nodes(all_games): + """Test games() function can filter on length of gbt.Game attribute 'nodes'""" + filtered_games = gbt.catalog.games(n_nodes=5) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + assert all(len(g.nodes) == 5 for g in list(filtered_games.Game)) + + +def test_catalog_games_filter_n_outcomes(all_games): + """Test games() function can filter on length of gbt.Game attribute 'outcomes'""" + filtered_games = gbt.catalog.games(n_outcomes=3) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + assert all(len(g.outcomes) == 3 for g in list(filtered_games.Game)) + + +def test_catalog_games_filter_n_players(all_games): + """Test games() function can filter on length of gbt.Game attribute 'players'""" + filtered_games = gbt.catalog.games(n_players=2) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + assert all(len(g.players) == 2 for g in list(filtered_games.Game)) + + +def test_catalog_games_filter_n_strategies(all_games): + """Test games() function can filter on length of gbt.Game attribute 'strategies'""" + filtered_games = gbt.catalog.games(n_strategies=4) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + assert all(len(g.strategies) == 4 for g in list(filtered_games.Game)) From fb4a90c57b1457608b7a299c954e22d86a1bd6c5 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 26 Feb 2026 10:43:30 +0000 Subject: [PATCH 082/119] suppress is_tree test for now --- tests/test_catalog.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 3fe0c0ac1..355c195ba 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -90,12 +90,13 @@ def test_catalog_games_filter_is_const_sum(all_games): # assert all(g.is_perfect_recall for g in list(filtered_games.Game)) -def test_catalog_games_filter_is_tree(all_games): - """Test games() function can filter on boolean gbt.Game attribute 'is_tree'""" - filtered_games = gbt.catalog.games(is_tree=True) - assert isinstance(filtered_games, pd.DataFrame) - assert len(filtered_games) < len(all_games) - assert all(g.is_tree for g in list(filtered_games.Game)) +# TODO: Re-introduce this test when there are examples in the catalog of normal form games +# def test_catalog_games_filter_is_tree(all_games): +# """Test games() function can filter on boolean gbt.Game attribute 'is_tree'""" +# filtered_games = gbt.catalog.games(is_tree=True) +# assert isinstance(filtered_games, pd.DataFrame) +# assert len(filtered_games) < len(all_games) +# assert all(g.is_tree for g in list(filtered_games.Game)) def test_catalog_games_filter_min_payoff_and_max_payoff(all_games): From 68965d1bfe6f38ec205da7cfaaf2f37187bedafa Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 2 Mar 2026 11:45:05 +0000 Subject: [PATCH 083/119] remove doc duplication --- doc/developer.catalog.rst | 45 --------------------------------------- 1 file changed, 45 deletions(-) diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index aa3540d87..9a205d82a 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -58,48 +58,3 @@ Code new games & add game families Ensure each entry has unique game slug as key (this will be used by ``pygambit.catalog.load('slug')``), and returns a call of the function with specific parameters. 3. **Submit a pull request to GitHub with all changes.** -Updating the Games Catalog -========================== - -This page covers the process for contributing to and updating Gambit's :ref:`Games Catalog `. -To do so, you will need to have the `gambit` GitHub repo cloned and be able to submit pull request via GitHub; -you may wish to first review the :ref:`contributor guidelines `. - -You can add games to the catalog saved in a valid representation :ref:`format `. -Currently supported representations are: - -- `.efg` for extensive form games -- `.nfg` for normal form games - -Add new games -------------- - -1. **Create the game file:** - - Use either :ref:`pygambit `, the Gambit :ref:`CLI ` or :ref:`GUI ` to create and save game in a valid representation :ref:`format `. - -2. **Add the game file:** - - Create a new branch in the ``gambit`` repo. Add your new game file(s) inside the ``catalog`` dir and commit them. - -3. **Update the catalog:** - - Use the ``update.py`` script to update Gambit's documentation & build files. - - .. code-block:: bash - - python build_support/catalog/update.py --build - - .. note:: - - Run this script in a Python environment where ``pygambit`` itself is also :ref:`installed `. - - .. warning:: - - Running the script with the ``--build`` flag updates `Makefile.am`. If you moved games that were previously in `contrib/games` you'll need to also manually remove those files from `EXTRA_DIST`. - -4. **Submit a pull request to GitHub with all changes.** - - .. warning:: - - Make sure you commit all changed files e.g. run ``git add --all`` before committing and pushing. From 97e015db405f1fcfed98cff1f65aa65805545929 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 2 Mar 2026 13:20:42 +0000 Subject: [PATCH 084/119] update docstring for games() --- catalog/__init__.py | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index d16ef1844..826286125 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -39,12 +39,47 @@ def load(slug: str) -> gbt.Game: def games(**kwargs) -> pd.DataFrame: """ - List games available in the package catalog, including subdirectories. + List games available in the package catalog. - If no arguments are provided, returns a pandas DataFrame with columns "Game" and "Title". + If no arguments are provided, returns a pandas DataFrame with columns "Game" + and "Title", where "Game" is the slug to load the game. - If arguments are provided, they are treated as filters and a pandas DataFrame is returned - where the "Game" column contains pygambit.Game objects matching the criteria. + If keyword arguments are provided, they are treated as filters on the + attributes of the game objects. + + Parameters + ---------- + n_actions: int, optional + The number of actions in the game. Only extensive games are returned. + n_contingencies: int, optional + The number of contingencies in the game. + n_infosets: int, optional + The number of information sets in the game. Only extensive games are returned. + is_const_sum: bool, optional + Whether the game is constant-sum. + is_perfect_recall: bool, optional + Whether the game has perfect recall. + is_tree: bool, optional + Whether the game is an extensive game (a tree). + min_payoff: float, optional + The minimum payoff in the game. Games returned have `min_payoff >= value`. + max_payoff: float, optional + The maximum payoff in the game. Games returned have `max_payoff <= value`. + n_nodes: int, optional + The number of nodes in the game. Only extensive games are returned. + n_outcomes: int, optional + The number of outcomes in the game. + n_players: int, optional + The number of players in the game. + n_strategies: int, optional + The number of pure strategies in the game. + + Returns + ------- + pd.DataFrame + If no arguments are provided, the "Game" column contains string slugs. + If arguments are provided, the "Game" column contains `pygambit.Game` + objects that match the filters. """ records: list[dict[str, Any]] = [] From d424d7aec44afc176d62bae175e6ad52f6c02613 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 2 Mar 2026 13:25:48 +0000 Subject: [PATCH 085/119] update tests to assume returned df from games() has slug strings, not game objects --- tests/test_catalog.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 355c195ba..4a6740c50 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -54,7 +54,6 @@ def test_catalog_games_filter_n_actions(all_games): filtered_games = gbt.catalog.games(n_actions=2) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(len(g.actions) == 2 for g in list(filtered_games.Game)) def test_catalog_games_filter_n_contingencies(all_games): @@ -62,7 +61,6 @@ def test_catalog_games_filter_n_contingencies(all_games): filtered_games = gbt.catalog.games(n_contingencies=2) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(len(g.contingencies) == 2 for g in list(filtered_games.Game)) def test_catalog_games_filter_n_infosets(all_games): @@ -70,7 +68,6 @@ def test_catalog_games_filter_n_infosets(all_games): filtered_games = gbt.catalog.games(n_infosets=2) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(len(g.infosets) == 2 for g in list(filtered_games.Game)) def test_catalog_games_filter_is_const_sum(all_games): @@ -78,7 +75,6 @@ def test_catalog_games_filter_is_const_sum(all_games): filtered_games = gbt.catalog.games(is_const_sum=True) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(g.is_const_sum for g in list(filtered_games.Game)) # TODO: Re-introduce this test when there are examples in the catalog without perfect recall @@ -87,7 +83,6 @@ def test_catalog_games_filter_is_const_sum(all_games): # filtered_games = gbt.catalog.games(is_perfect_recall=True) # assert isinstance(filtered_games, pd.DataFrame) # assert len(filtered_games) < len(all_games) -# assert all(g.is_perfect_recall for g in list(filtered_games.Game)) # TODO: Re-introduce this test when there are examples in the catalog of normal form games @@ -96,7 +91,6 @@ def test_catalog_games_filter_is_const_sum(all_games): # filtered_games = gbt.catalog.games(is_tree=True) # assert isinstance(filtered_games, pd.DataFrame) # assert len(filtered_games) < len(all_games) -# assert all(g.is_tree for g in list(filtered_games.Game)) def test_catalog_games_filter_min_payoff_and_max_payoff(all_games): @@ -104,7 +98,6 @@ def test_catalog_games_filter_min_payoff_and_max_payoff(all_games): filtered_games = gbt.catalog.games(min_payoff=0, max_payoff=10) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(g.min_payoff >= 0 and g.max_payoff <= 10 for g in list(filtered_games.Game)) def test_catalog_games_filter_n_nodes(all_games): @@ -112,7 +105,6 @@ def test_catalog_games_filter_n_nodes(all_games): filtered_games = gbt.catalog.games(n_nodes=5) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(len(g.nodes) == 5 for g in list(filtered_games.Game)) def test_catalog_games_filter_n_outcomes(all_games): @@ -120,7 +112,6 @@ def test_catalog_games_filter_n_outcomes(all_games): filtered_games = gbt.catalog.games(n_outcomes=3) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(len(g.outcomes) == 3 for g in list(filtered_games.Game)) def test_catalog_games_filter_n_players(all_games): @@ -128,7 +119,6 @@ def test_catalog_games_filter_n_players(all_games): filtered_games = gbt.catalog.games(n_players=2) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(len(g.players) == 2 for g in list(filtered_games.Game)) def test_catalog_games_filter_n_strategies(all_games): @@ -136,4 +126,3 @@ def test_catalog_games_filter_n_strategies(all_games): filtered_games = gbt.catalog.games(n_strategies=4) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - assert all(len(g.strategies) == 4 for g in list(filtered_games.Game)) From 900b3b81e9cd12e060be791a1ebcc4787f5d5516 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 2 Mar 2026 13:26:41 +0000 Subject: [PATCH 086/119] Ensure games() always returns slug strings, not instantiated Game objects --- catalog/__init__.py | 40 +++++++++++----------------------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index 826286125..f478aa09f 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -77,9 +77,7 @@ def games(**kwargs) -> pd.DataFrame: Returns ------- pd.DataFrame - If no arguments are provided, the "Game" column contains string slugs. - If arguments are provided, the "Game" column contains `pygambit.Game` - objects that match the filters. + A DataFrame with columns "Game" and "Title", where "Game" is the slug to load the game. """ records: list[dict[str, Any]] = [] @@ -129,19 +127,11 @@ def check_filters(game: gbt.Game) -> bool: with as_file(resource_path) as path: game = reader(str(path)) - if kwargs: - if check_filters(game): - records.append({ - "Game": game, - "Title": game.title, - }) - else: - records.append( - { - "Game": slug, - "Title": game.title, - } - ) + if check_filters(game): + records.append({ + "Game": slug, + "Title": game.title, + }) # Add all the games from families for slug, game in family_games().items(): @@ -150,19 +140,11 @@ def check_filters(game: gbt.Game) -> bool: raise ValueError( f"Slug collision: {slug} is present in both file-based and family games." ) - if kwargs: - if check_filters(game): - records.append({ - "Game": game, - "Title": game.title, - }) - else: - records.append( - { - "Game": slug, - "Title": game.title, - } - ) + if check_filters(game): + records.append({ + "Game": slug, + "Title": game.title, + }) return pd.DataFrame.from_records(records, columns=["Game", "Title"]) From b6e371af370cb560bb0c45c7631e323f2172d106 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 2 Mar 2026 13:31:57 +0000 Subject: [PATCH 087/119] add valueerror for invalid kwargs --- catalog/__init__.py | 19 +++++++++++++++++++ tests/test_catalog.py | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/catalog/__init__.py b/catalog/__init__.py index f478aa09f..559c393fb 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -81,6 +81,25 @@ def games(**kwargs) -> pd.DataFrame: """ records: list[dict[str, Any]] = [] + # Raise an error if any invalid filter keys are provided + valid_filter_keys = { + "n_actions", + "n_contingencies", + "n_infosets", + "is_const_sum", + "is_perfect_recall", + "is_tree", + "min_payoff", + "max_payoff", + "n_nodes", + "n_outcomes", + "n_players", + "n_strategies", + } + for key in kwargs: + if key not in valid_filter_keys: + raise ValueError(f"Invalid kwarg: {key}. Valid kwargs are: {valid_filter_keys}") + def check_filters(game: gbt.Game) -> bool: if "n_actions" in kwargs: if not game.is_tree: diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 4a6740c50..bb391945a 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -126,3 +126,9 @@ def test_catalog_games_filter_n_strategies(all_games): filtered_games = gbt.catalog.games(n_strategies=4) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) + + +def test_catalog_games_filter_bad_filter(): + """Test games() function raises error on invalid filter key""" + with pytest.raises(ValueError): + gbt.catalog.games(invalid_filter=123) From b20240a44cd29cebe1e0469491fdb91f4d173cae Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 2 Mar 2026 13:51:18 +0000 Subject: [PATCH 088/119] switch kwargs for args in games() --- catalog/__init__.py | 84 +++++++++++++++++++++---------------------- tests/test_catalog.py | 2 +- 2 files changed, 42 insertions(+), 44 deletions(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index 559c393fb..a411d6572 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -37,7 +37,20 @@ def load(slug: str) -> gbt.Game: raise FileNotFoundError(f"No catalog entry called {slug}") -def games(**kwargs) -> pd.DataFrame: +def games( + n_actions: int | None = None, + n_contingencies: int | None = None, + n_infosets: int | None = None, + is_const_sum: bool | None = None, + is_perfect_recall: bool | None = None, + is_tree: bool | None = None, + min_payoff: float | None = None, + max_payoff: float | None = None, + n_nodes: int | None = None, + n_outcomes: int | None = None, + n_players: int | None = None, + n_strategies: int | None = None, +) -> pd.DataFrame: """ List games available in the package catalog. @@ -81,58 +94,39 @@ def games(**kwargs) -> pd.DataFrame: """ records: list[dict[str, Any]] = [] - # Raise an error if any invalid filter keys are provided - valid_filter_keys = { - "n_actions", - "n_contingencies", - "n_infosets", - "is_const_sum", - "is_perfect_recall", - "is_tree", - "min_payoff", - "max_payoff", - "n_nodes", - "n_outcomes", - "n_players", - "n_strategies", - } - for key in kwargs: - if key not in valid_filter_keys: - raise ValueError(f"Invalid kwarg: {key}. Valid kwargs are: {valid_filter_keys}") - def check_filters(game: gbt.Game) -> bool: - if "n_actions" in kwargs: + if n_actions is not None: if not game.is_tree: return False - if len(game.actions) != kwargs["n_actions"]: + if len(game.actions) != n_actions: return False - if "n_contingencies" in kwargs and len(game.contingencies) != kwargs["n_contingencies"]: + if n_contingencies is not None and len(game.contingencies) != n_contingencies: return False - if "n_infosets" in kwargs: + if n_infosets is not None: if not game.is_tree: return False - if len(game.infosets) != kwargs["n_infosets"]: + if len(game.infosets) != n_infosets: return False - if "is_const_sum" in kwargs and game.is_const_sum != kwargs["is_const_sum"]: + if is_const_sum is not None and game.is_const_sum != is_const_sum: return False - if "is_perfect_recall" in kwargs and game.is_perfect_recall != kwargs["is_perfect_recall"]: + if is_perfect_recall is not None and game.is_perfect_recall != is_perfect_recall: return False - if "is_tree" in kwargs and game.is_tree != kwargs["is_tree"]: + if is_tree is not None and game.is_tree != is_tree: return False - if "min_payoff" in kwargs and game.min_payoff < kwargs["min_payoff"]: + if min_payoff is not None and game.min_payoff < min_payoff: return False - if "max_payoff" in kwargs and game.max_payoff > kwargs["max_payoff"]: + if max_payoff is not None and game.max_payoff > max_payoff: return False - if "n_nodes" in kwargs: + if n_nodes is not None: if not game.is_tree: return False - if len(game.nodes) != kwargs["n_nodes"]: + if len(game.nodes) != n_nodes: return False - if "n_outcomes" in kwargs and len(game.outcomes) != kwargs["n_outcomes"]: + if n_outcomes is not None and len(game.outcomes) != n_outcomes: return False - if "n_players" in kwargs and len(game.players) != kwargs["n_players"]: + if n_players is not None and len(game.players) != n_players: return False - return not ("n_strategies" in kwargs and len(game.strategies) != kwargs["n_strategies"]) + return not (n_strategies is not None and len(game.strategies) != n_strategies) # Add all the games stored as EFG/NFG files for resource_path in sorted(_CATALOG_RESOURCE.rglob("*")): @@ -147,10 +141,12 @@ def check_filters(game: gbt.Game) -> bool: with as_file(resource_path) as path: game = reader(str(path)) if check_filters(game): - records.append({ - "Game": slug, - "Title": game.title, - }) + records.append( + { + "Game": slug, + "Title": game.title, + } + ) # Add all the games from families for slug, game in family_games().items(): @@ -160,10 +156,12 @@ def check_filters(game: gbt.Game) -> bool: f"Slug collision: {slug} is present in both file-based and family games." ) if check_filters(game): - records.append({ - "Game": slug, - "Title": game.title, - }) + records.append( + { + "Game": slug, + "Title": game.title, + } + ) return pd.DataFrame.from_records(records, columns=["Game", "Title"]) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index bb391945a..09889356b 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -130,5 +130,5 @@ def test_catalog_games_filter_n_strategies(all_games): def test_catalog_games_filter_bad_filter(): """Test games() function raises error on invalid filter key""" - with pytest.raises(ValueError): + with pytest.raises(TypeError): gbt.catalog.games(invalid_filter=123) From ef7e446426f0b95f5e36fddee4cb3e8ca8826c07 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 2 Mar 2026 13:59:16 +0000 Subject: [PATCH 089/119] Add catalog functions to API docs --- doc/pygambit.api.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index 7568cfd5a..0b94d6ff1 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -325,3 +325,15 @@ Computation of quantal response equilibria logit_estimate LogitQREMixedStrategyFitResult LogitQREMixedBehaviorFitResult + + +Catalog of games +~~~~~~~~~~~~~~~~ + +.. currentmodule:: pygambit.catalog + +.. autosummary:: + :toctree: api/ + + load + games From e838f511669f1bb820d70ea8095840f4a5f0c855 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 2 Mar 2026 14:00:30 +0000 Subject: [PATCH 090/119] improve docstring of load --- catalog/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/catalog/__init__.py b/catalog/__init__.py index a411d6572..ddf8fbd71 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -18,6 +18,21 @@ def load(slug: str) -> gbt.Game: """ Load a game from the package catalog. + + Parameters + ---------- + slug : str + The slug of the game to load. + + Returns + ------- + gbt.Game + The loaded game. + + Raises + ------ + FileNotFoundError + If the game does not exist in the catalog. """ slug = str(Path(slug)).replace("\\", "/") From b385681e7160edcc866f52f9d856fbe52aefaecf Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 2 Mar 2026 14:01:28 +0000 Subject: [PATCH 091/119] improve docstring for family_games --- catalog/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/catalog/__init__.py b/catalog/__init__.py index ddf8fbd71..c0b242b75 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -185,6 +185,11 @@ def family_games() -> dict[str, gbt.Game]: """ Generate a dict of games for inclusion in the catalog, using the game families in this module. + + Returns + ------- + dict[str, gbt.Game] + A dictionary mapping slugs to game objects for family games. """ return { "one_shot_trust": one_shot_trust(), From d1f4901d7a79babfdb9c33812d92a60c57c3f7e1 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 2 Mar 2026 14:09:15 +0000 Subject: [PATCH 092/119] Add links from the main catalog page to API reference and developer docs --- doc/catalog.rst | 3 +++ doc/developer.catalog.rst | 2 ++ doc/pygambit.api.rst | 2 ++ 3 files changed, 7 insertions(+) diff --git a/doc/catalog.rst b/doc/catalog.rst index cf8d45e2c..d50367996 100644 --- a/doc/catalog.rst +++ b/doc/catalog.rst @@ -1,6 +1,9 @@ Catalog of games ================ +Below is a complete list of games included in Gambit's catalog. +Check out the :ref:`pygambit API reference ` for instructions on how to search and load these games in Python, and the :ref:`Updating the games catalog ` guide for instructions on how to contribute new games to the catalog. + .. csv-table:: :file: catalog.csv :header-rows: 1 diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index 9a205d82a..66a224cb7 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -1,3 +1,5 @@ +.. _updating-catalog: + Updating the Games Catalog ========================== diff --git a/doc/pygambit.api.rst b/doc/pygambit.api.rst index 0b94d6ff1..7f8eea604 100644 --- a/doc/pygambit.api.rst +++ b/doc/pygambit.api.rst @@ -327,6 +327,8 @@ Computation of quantal response equilibria LogitQREMixedBehaviorFitResult +.. _pygambit-catalog: + Catalog of games ~~~~~~~~~~~~~~~~ From 8820a4db44bf359ef59eaeda18c2b6f37bd93df7 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 2 Mar 2026 14:21:17 +0000 Subject: [PATCH 093/119] clarify docstring --- catalog/__init__.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index c0b242b75..e83ea540b 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -69,11 +69,8 @@ def games( """ List games available in the package catalog. - If no arguments are provided, returns a pandas DataFrame with columns "Game" - and "Title", where "Game" is the slug to load the game. - - If keyword arguments are provided, they are treated as filters on the - attributes of the game objects. + Arguments are treated as filters on the + attributes of the Game objects. Parameters ---------- From 8077cd69ab7330567c13ebaa1abac83dee1b78b0 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 2 Mar 2026 14:28:33 +0000 Subject: [PATCH 094/119] update filter tests to assert the first game of the returned dataframe matches the filter criteria --- tests/test_catalog.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 09889356b..fde6546e5 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -54,6 +54,8 @@ def test_catalog_games_filter_n_actions(all_games): filtered_games = gbt.catalog.games(n_actions=2) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.actions) == 2 def test_catalog_games_filter_n_contingencies(all_games): @@ -61,6 +63,8 @@ def test_catalog_games_filter_n_contingencies(all_games): filtered_games = gbt.catalog.games(n_contingencies=2) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.contingencies) == 2 def test_catalog_games_filter_n_infosets(all_games): @@ -68,6 +72,8 @@ def test_catalog_games_filter_n_infosets(all_games): filtered_games = gbt.catalog.games(n_infosets=2) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.infosets) == 2 def test_catalog_games_filter_is_const_sum(all_games): @@ -75,6 +81,8 @@ def test_catalog_games_filter_is_const_sum(all_games): filtered_games = gbt.catalog.games(is_const_sum=True) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert g.is_const_sum # TODO: Re-introduce this test when there are examples in the catalog without perfect recall @@ -83,6 +91,8 @@ def test_catalog_games_filter_is_const_sum(all_games): # filtered_games = gbt.catalog.games(is_perfect_recall=True) # assert isinstance(filtered_games, pd.DataFrame) # assert len(filtered_games) < len(all_games) +# g = gbt.catalog.load(filtered_games.Game.iloc[0]) +# assert g.is_perfect_recall # TODO: Re-introduce this test when there are examples in the catalog of normal form games @@ -91,6 +101,8 @@ def test_catalog_games_filter_is_const_sum(all_games): # filtered_games = gbt.catalog.games(is_tree=True) # assert isinstance(filtered_games, pd.DataFrame) # assert len(filtered_games) < len(all_games) +# g = gbt.catalog.load(filtered_games.Game.iloc[0]) +# assert g.is_tree def test_catalog_games_filter_min_payoff_and_max_payoff(all_games): @@ -98,6 +110,9 @@ def test_catalog_games_filter_min_payoff_and_max_payoff(all_games): filtered_games = gbt.catalog.games(min_payoff=0, max_payoff=10) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert g.min_payoff >= 0 + assert g.max_payoff <= 10 def test_catalog_games_filter_n_nodes(all_games): @@ -105,6 +120,8 @@ def test_catalog_games_filter_n_nodes(all_games): filtered_games = gbt.catalog.games(n_nodes=5) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.nodes) == 5 def test_catalog_games_filter_n_outcomes(all_games): @@ -112,6 +129,8 @@ def test_catalog_games_filter_n_outcomes(all_games): filtered_games = gbt.catalog.games(n_outcomes=3) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.outcomes) == 3 def test_catalog_games_filter_n_players(all_games): @@ -119,6 +138,8 @@ def test_catalog_games_filter_n_players(all_games): filtered_games = gbt.catalog.games(n_players=2) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.players) == 2 def test_catalog_games_filter_n_strategies(all_games): @@ -126,6 +147,8 @@ def test_catalog_games_filter_n_strategies(all_games): filtered_games = gbt.catalog.games(n_strategies=4) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.strategies) == 4 def test_catalog_games_filter_bad_filter(): From 09f2a33f0f35b8e15f298c88d2187ea98ddb9a16 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Mon, 2 Mar 2026 14:44:01 +0000 Subject: [PATCH 095/119] Only assert the filter criteria when at least one game returned --- tests/test_catalog.py | 79 ++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index fde6546e5..6cc53d16a 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -54,8 +54,9 @@ def test_catalog_games_filter_n_actions(all_games): filtered_games = gbt.catalog.games(n_actions=2) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - g = gbt.catalog.load(filtered_games.Game.iloc[0]) - assert len(g.actions) == 2 + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.actions) == 2 def test_catalog_games_filter_n_contingencies(all_games): @@ -63,8 +64,9 @@ def test_catalog_games_filter_n_contingencies(all_games): filtered_games = gbt.catalog.games(n_contingencies=2) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - g = gbt.catalog.load(filtered_games.Game.iloc[0]) - assert len(g.contingencies) == 2 + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.contingencies) == 2 def test_catalog_games_filter_n_infosets(all_games): @@ -72,8 +74,9 @@ def test_catalog_games_filter_n_infosets(all_games): filtered_games = gbt.catalog.games(n_infosets=2) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - g = gbt.catalog.load(filtered_games.Game.iloc[0]) - assert len(g.infosets) == 2 + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.infosets) == 2 def test_catalog_games_filter_is_const_sum(all_games): @@ -81,28 +84,29 @@ def test_catalog_games_filter_is_const_sum(all_games): filtered_games = gbt.catalog.games(is_const_sum=True) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - g = gbt.catalog.load(filtered_games.Game.iloc[0]) - assert g.is_const_sum + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert g.is_const_sum -# TODO: Re-introduce this test when there are examples in the catalog without perfect recall -# def test_catalog_games_filter_is_perfect_recall(all_games): -# """Test games() function can filter on boolean gbt.Game attribute 'is_perfect_recall'""" -# filtered_games = gbt.catalog.games(is_perfect_recall=True) -# assert isinstance(filtered_games, pd.DataFrame) -# assert len(filtered_games) < len(all_games) -# g = gbt.catalog.load(filtered_games.Game.iloc[0]) -# assert g.is_perfect_recall +def test_catalog_games_filter_is_not_perfect_recall(all_games): + """Test games() function can filter on boolean gbt.Game attribute 'is_perfect_recall'""" + filtered_games = gbt.catalog.games(is_perfect_recall=False) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert not g.is_perfect_recall -# TODO: Re-introduce this test when there are examples in the catalog of normal form games -# def test_catalog_games_filter_is_tree(all_games): -# """Test games() function can filter on boolean gbt.Game attribute 'is_tree'""" -# filtered_games = gbt.catalog.games(is_tree=True) -# assert isinstance(filtered_games, pd.DataFrame) -# assert len(filtered_games) < len(all_games) -# g = gbt.catalog.load(filtered_games.Game.iloc[0]) -# assert g.is_tree +def test_catalog_games_filter_is_not_tree(all_games): + """Test games() function can filter on boolean gbt.Game attribute 'is_tree'""" + filtered_games = gbt.catalog.games(is_tree=False) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert not g.is_tree def test_catalog_games_filter_min_payoff_and_max_payoff(all_games): @@ -110,9 +114,10 @@ def test_catalog_games_filter_min_payoff_and_max_payoff(all_games): filtered_games = gbt.catalog.games(min_payoff=0, max_payoff=10) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - g = gbt.catalog.load(filtered_games.Game.iloc[0]) - assert g.min_payoff >= 0 - assert g.max_payoff <= 10 + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert g.min_payoff >= 0 + assert g.max_payoff <= 10 def test_catalog_games_filter_n_nodes(all_games): @@ -120,8 +125,9 @@ def test_catalog_games_filter_n_nodes(all_games): filtered_games = gbt.catalog.games(n_nodes=5) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - g = gbt.catalog.load(filtered_games.Game.iloc[0]) - assert len(g.nodes) == 5 + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.nodes) == 5 def test_catalog_games_filter_n_outcomes(all_games): @@ -129,8 +135,9 @@ def test_catalog_games_filter_n_outcomes(all_games): filtered_games = gbt.catalog.games(n_outcomes=3) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - g = gbt.catalog.load(filtered_games.Game.iloc[0]) - assert len(g.outcomes) == 3 + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.outcomes) == 3 def test_catalog_games_filter_n_players(all_games): @@ -138,8 +145,9 @@ def test_catalog_games_filter_n_players(all_games): filtered_games = gbt.catalog.games(n_players=2) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - g = gbt.catalog.load(filtered_games.Game.iloc[0]) - assert len(g.players) == 2 + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.players) == 2 def test_catalog_games_filter_n_strategies(all_games): @@ -147,8 +155,9 @@ def test_catalog_games_filter_n_strategies(all_games): filtered_games = gbt.catalog.games(n_strategies=4) assert isinstance(filtered_games, pd.DataFrame) assert len(filtered_games) < len(all_games) - g = gbt.catalog.load(filtered_games.Game.iloc[0]) - assert len(g.strategies) == 4 + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.strategies) == 4 def test_catalog_games_filter_bad_filter(): From 0b30460f49d693f243dfc44a71a2f99b8f87bd3f Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 11 Mar 2026 09:37:57 +0000 Subject: [PATCH 096/119] Catalog/761 (#793) * add failing test for games(include_descriptions=True) which gets used in update script * Add description field to df when selected * refactor to reduce code duplication * correctly show descriptions on catalog page * Add biblio links to all games in catalog * fix biblio links * update table row spacing * hide samples page * Add download links * generate rst table instead of CSV for Catalog of games page * Refactor table such that descriptions are nested under titles --- .gitignore | 2 +- build_support/catalog/update.py | 45 ++++++++++++++++++++++++++++++--- catalog/__init__.py | 41 ++++++++++++++++++++---------- catalog/myerson1991/fig4_2.efg | 4 +-- catalog/selten1975/fig1.efg | 4 +-- catalog/selten1975/fig2.efg | 4 +-- catalog/selten1975/fig3.efg | 4 +-- doc/biblio.rst | 8 ++++++ doc/catalog.rst | 6 +---- doc/index.rst | 1 - tests/test_catalog.py | 7 +++++ 11 files changed, 93 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index f8be35fcf..643f8c565 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,4 @@ Gambit.app/* build_support/msw/gambit.wxs build_support/osx/Info.plist src/pygambit/catalog -doc/catalog.csv +doc/catalog_table.rst diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index e42a18ad9..fd012ee65 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -1,13 +1,49 @@ import argparse from pathlib import Path +import pandas as pd + import pygambit as gbt -CATALOG_CSV = Path(__file__).parent.parent.parent / "doc" / "catalog.csv" +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" +def generate_rst_table(df: pd.DataFrame, rst_path: Path): + """Generate a list-table RST file with dropdowns for long descriptions.""" + with open(rst_path, "w", encoding="utf-8") as f: + f.write(".. list-table::\n") + f.write(" :header-rows: 1\n") + f.write(" :widths: 20 80 20\n") + f.write(" :class: tight-table\n") + f.write("\n") + + f.write(" * - **Game**\n") + f.write(" - **Description**\n") + f.write(" - **Download**\n") + + for _, row in df.iterrows(): + f.write(f" * - {row['Game']}\n") + + description_cell_lines = [] + title = str(row.get("Title", "")).strip() + description = str(row.get("Description", "")).strip() + if description: + description_cell_lines.append(f".. dropdown:: {title}") + description_cell_lines.append(" ") # Indented blank line + for line in description.splitlines(): + description_cell_lines.append(f" {line}") + else: + description_cell_lines.append(title) + + f.write(f" - {description_cell_lines[0]}\n") + for line in description_cell_lines[1:]: + f.write(f" {line}\n") + + f.write(f" - {row['Download']}\n") + + def update_makefile(): """Update the Makefile.am with all games from the catalog.""" @@ -65,9 +101,10 @@ def update_makefile(): parser.add_argument("--build", action="store_true") args = parser.parse_args() - # Create CSV used by RST docs page - gbt.catalog.games().to_csv(CATALOG_CSV, index=False) - print(f"Generated {CATALOG_CSV} for use in local docs build. DO NOT COMMIT.") + # Create RST list-table used by doc/catalog.rst + df = gbt.catalog.games(include_descriptions=True) + generate_rst_table(df, CATALOG_RST_TABLE) + print(f"Generated {CATALOG_RST_TABLE} for use in local docs build. DO NOT COMMIT.") # Update the Makefile.am with the current list of catalog files if args.build: diff --git a/catalog/__init__.py b/catalog/__init__.py index e83ea540b..d4ca2995f 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -65,11 +65,12 @@ def games( n_outcomes: int | None = None, n_players: int | None = None, n_strategies: int | None = None, + include_descriptions: bool = False, ) -> pd.DataFrame: """ List games available in the package catalog. - Arguments are treated as filters on the + Most arguments are treated as filters on the attributes of the Game objects. Parameters @@ -98,11 +99,15 @@ def games( The number of players in the game. n_strategies: int, optional The number of pure strategies in the game. + include_descriptions: bool, optional + Whether to include the description of each game in the returned DataFrame. + Defaults to False. Returns ------- pd.DataFrame A DataFrame with columns "Game" and "Title", where "Game" is the slug to load the game. + If `include_descriptions=True`, the DataFrame will also include a "Description" column. """ records: list[dict[str, Any]] = [] @@ -140,6 +145,20 @@ def check_filters(game: gbt.Game) -> bool: return False return not (n_strategies is not None and len(game.strategies) != n_strategies) + def append_record( + slug: str, + game: gbt.Game, + ) -> None: + record = { + "Game": slug, + "Title": game.title, + } + if include_descriptions: + record["Description"] = game.description + ext = "efg" if game.is_tree else "nfg" + record["Download"] = f":download:`{slug}.{ext} <../catalog/{slug}.{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) @@ -153,12 +172,7 @@ def check_filters(game: gbt.Game) -> bool: with as_file(resource_path) as path: game = reader(str(path)) if check_filters(game): - records.append( - { - "Game": slug, - "Title": game.title, - } - ) + append_record(slug, game) # Add all the games from families for slug, game in family_games().items(): @@ -168,13 +182,12 @@ def check_filters(game: gbt.Game) -> bool: f"Slug collision: {slug} is present in both file-based and family games." ) if check_filters(game): - records.append( - { - "Game": slug, - "Title": game.title, - } - ) + append_record(slug, game) + if include_descriptions: + return pd.DataFrame.from_records( + records, columns=["Game", "Title", "Description", "Download"] + ) return pd.DataFrame.from_records(records, columns=["Game", "Title"]) @@ -218,7 +231,7 @@ def one_shot_trust(unique_NE_variant: bool = False) -> gbt.Game: The constructed extensive-form game. """ g = gbt.Game.new_tree(players=["Buyer", "Seller"]) - g.description = "One-shot trust game with binary actions, originally from Kreps (1990)." + g.description = "One-shot trust game with binary actions, originally from [Kre90]_." g.append_move(g.root, "Buyer", ["Trust", "Not trust"]) g.append_move(g.root.children[0], "Seller", ["Honor", "Abuse"]) g.set_outcome(g.root.children[0].children[0], g.add_outcome([1, 1], label="Trustworthy")) diff --git a/catalog/myerson1991/fig4_2.efg b/catalog/myerson1991/fig4_2.efg index 9631090f0..c36cf1e13 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 Myerson [^Mye1991] which illustrates the distinction between +"An example from Myerson [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 @@ -8,7 +8,7 @@ game, because Player 1 would prefer to switch from (B1, Z1) to (A1, Y1); the (multi)agent representation rules out such coordinated deviations across information sets. -[^Mye1991]: Myerson, Roger B. (1991) Game Theory: Analysis of Conflict. +[Mye91]_: Myerson, Roger B. (1991) Game Theory: Analysis of Conflict. Cambridge: Harvard University Press. " diff --git a/catalog/selten1975/fig1.efg b/catalog/selten1975/fig1.efg index c966809b6..ea47ae88f 100644 --- a/catalog/selten1975/fig1.efg +++ b/catalog/selten1975/fig1.efg @@ -1,12 +1,12 @@ 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 Selten [^Sel1975], commonly referred +"This is a three-player game presented in Selten [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 is \"unreasonable\", but which is not ruled out by subgame perfection because this game has no proper subgames. -[^Sel1975]: Selten, Reinhard (1975). A reexamination of the perfectness concept +[Sel75]_: Selten, Reinhard (1975). A reexamination of the perfectness concept for equilibrium points in extensive games. International Journal of Game Theory 4(1): 25-55. " diff --git a/catalog/selten1975/fig2.efg b/catalog/selten1975/fig2.efg index 1d36cb21f..ab52adaa5 100644 --- a/catalog/selten1975/fig2.efg +++ b/catalog/selten1975/fig2.efg @@ -1,10 +1,10 @@ EFG 2 R "Selten (IJGT 1975) Figure 2" { "Player 1" "Player 2" } -"This is a counterexample presented in [^Sel1975], to show that extensive and +"This is a counterexample presented in [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. -[^Sel75]: Selten, Reinhard (1975). A reexamination of the perfectness concept +[Sel75]_: Selten, Reinhard (1975). A reexamination of the perfectness concept for equilibrium points in extensive games. International Journal of Game Theory 4(1): 25-55. " diff --git a/catalog/selten1975/fig3.efg b/catalog/selten1975/fig3.efg index 7df596906..25c123b05 100644 --- a/catalog/selten1975/fig3.efg +++ b/catalog/selten1975/fig3.efg @@ -1,10 +1,10 @@ EFG 2 R "Selten (IJGT 1975) Figure 3" { "Player 1" "Player 2" "Player 3" } -"This is a counterexample presented in [^Sel1975], to show that extensive and +"This is a counterexample presented in [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. -[^Sel75]: Selten, Reinhard (1975). A reexamination of the perfectness concept +[Sel75]_: Selten, Reinhard (1975). A reexamination of the perfectness concept for equilibrium points in extensive games. International Journal of Game Theory 4(1): 25-55. " diff --git a/doc/biblio.rst b/doc/biblio.rst index b84d341a8..8c12265be 100644 --- a/doc/biblio.rst +++ b/doc/biblio.rst @@ -1,6 +1,11 @@ Bibliography ============ +.. note:: + + To reference an entry in this bibliography, use the format ``[key]_``, for example, ``[Mye91]_`` will link to the Myerson (1991) textbook entry. + + Articles on computation of Nash equilibria ------------------------------------------ @@ -89,6 +94,9 @@ General game theory articles and texts .. [KreWil82] David Kreps and Robert Wilson, "Sequential Equilibria", 863-894, Econometrica , 50, 1982. +.. [Kre90] David Kreps, 1990, A Course in Microeconomic Theory, + Princeton University Press. + .. [McKPal95] Richard McKelvey and Tom Palfrey, "Quantal response equilibria for normal form games", 6-38, Games and Economic Behavior , 10, 1995. diff --git a/doc/catalog.rst b/doc/catalog.rst index d50367996..a7d132c4a 100644 --- a/doc/catalog.rst +++ b/doc/catalog.rst @@ -4,8 +4,4 @@ Catalog of games Below is a complete list of games included in Gambit's catalog. Check out the :ref:`pygambit API reference ` for instructions on how to search and load these games in Python, and the :ref:`Updating the games catalog ` guide for instructions on how to contribute new games to the catalog. -.. csv-table:: - :file: catalog.csv - :header-rows: 1 - :widths: 20, 80 - :class: tight-table +.. include:: catalog_table.rst diff --git a/doc/index.rst b/doc/index.rst index 0e1725265..017bdaf88 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -87,7 +87,6 @@ We recommended most new users install the PyGambit Python package and read the a tools gui catalog - samples developer formats biblio diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 6cc53d16a..a9188f1f8 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -164,3 +164,10 @@ def test_catalog_games_filter_bad_filter(): """Test games() function raises error on invalid filter key""" with pytest.raises(TypeError): gbt.catalog.games(invalid_filter=123) + + +def test_catalog_games_include_descriptions(): + """Test games() function can include descriptions""" + games_with_desc = gbt.catalog.games(include_descriptions=True) + assert "Description" in games_with_desc.columns + assert "Download" in games_with_desc.columns From d8129c59aa159b4efc00e946eb3bd4cf6090c49b Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 19 Mar 2026 13:20:20 +0000 Subject: [PATCH 097/119] complete merge for catalog tests --- tests/test_catalog.py | 145 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 139 insertions(+), 6 deletions(-) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 4dd2810ac..8e7363071 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -41,10 +41,143 @@ def test_catalog_load_invalid_slug(): gbt.catalog.load("invalid_slug") -def test_catalog_games(): - """Test games() function returns df of game slugs and titles""" - all_games = gbt.catalog.games() +def test_catalog_load_family_game(): + """Test loading a game generated from code with a game family func.""" + g = gbt.catalog.load("one_shot_trust") + assert isinstance(g, gbt.Game) + + +def test_catalog_games(game_slugs, all_games): + """Test games() function returns df of game slugs and titles.""" assert isinstance(all_games, pd.DataFrame) - assert len(all_games) > 0 - assert "myerson1991/fig4_2" in list(all_games.Game) - assert "Myerson (1991) Figure 4.2" in list(all_games.Title) + + # The games() function should return exactly the set of slugs found above + assert set(all_games["Game"]) == game_slugs + + # Test that standard columns are present + assert "Game" in all_games.columns + assert "Title" in all_games.columns + + +def test_catalog_games_filter_n_actions(all_games): + """Test games() function can filter on length of gbt.Game attribute 'actions'""" + filtered_games = gbt.catalog.games(n_actions=2) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.actions) == 2 + + +def test_catalog_games_filter_n_contingencies(all_games): + """Test games() function can filter on length of gbt.Game attribute 'contingencies'""" + filtered_games = gbt.catalog.games(n_contingencies=2) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.contingencies) == 2 + + +def test_catalog_games_filter_n_infosets(all_games): + """Test games() function can filter on length of gbt.Game attribute 'infosets'""" + filtered_games = gbt.catalog.games(n_infosets=2) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.infosets) == 2 + + +def test_catalog_games_filter_is_const_sum(all_games): + """Test games() function can filter on boolean gbt.Game attribute 'is_const_sum'""" + filtered_games = gbt.catalog.games(is_const_sum=True) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert g.is_const_sum + + +def test_catalog_games_filter_is_not_perfect_recall(all_games): + """Test games() function can filter on boolean gbt.Game attribute 'is_perfect_recall'""" + filtered_games = gbt.catalog.games(is_perfect_recall=False) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert not g.is_perfect_recall + + +def test_catalog_games_filter_is_not_tree(all_games): + """Test games() function can filter on boolean gbt.Game attribute 'is_tree'""" + filtered_games = gbt.catalog.games(is_tree=False) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert not g.is_tree + + +def test_catalog_games_filter_min_payoff_and_max_payoff(all_games): + """Test games() function can filter on min and max payoff values""" + filtered_games = gbt.catalog.games(min_payoff=0, max_payoff=10) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert g.min_payoff >= 0 + assert g.max_payoff <= 10 + + +def test_catalog_games_filter_n_nodes(all_games): + """Test games() function can filter on length of gbt.Game attribute 'nodes'""" + filtered_games = gbt.catalog.games(n_nodes=5) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.nodes) == 5 + + +def test_catalog_games_filter_n_outcomes(all_games): + """Test games() function can filter on length of gbt.Game attribute 'outcomes'""" + filtered_games = gbt.catalog.games(n_outcomes=3) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.outcomes) == 3 + + +def test_catalog_games_filter_n_players(all_games): + """Test games() function can filter on length of gbt.Game attribute 'players'""" + filtered_games = gbt.catalog.games(n_players=2) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.players) == 2 + + +def test_catalog_games_filter_n_strategies(all_games): + """Test games() function can filter on length of gbt.Game attribute 'strategies'""" + filtered_games = gbt.catalog.games(n_strategies=4) + assert isinstance(filtered_games, pd.DataFrame) + assert len(filtered_games) < len(all_games) + if len(filtered_games) > 0: + g = gbt.catalog.load(filtered_games.Game.iloc[0]) + assert len(g.strategies) == 4 + + +def test_catalog_games_filter_bad_filter(): + """Test games() function raises error on invalid filter key""" + with pytest.raises(TypeError): + gbt.catalog.games(invalid_filter=123) + + +def test_catalog_games_include_descriptions(): + """Test games() function can include descriptions""" + games_with_desc = gbt.catalog.games(include_descriptions=True) + assert "Description" in games_with_desc.columns + assert "Download" in games_with_desc.columns From 6342fe178a79c445c26c864466e683d8ae6aa0a9 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 19 Mar 2026 13:25:19 +0000 Subject: [PATCH 098/119] update test_catalog_games to check for family games --- tests/test_catalog.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 8e7363071..0c2a2d496 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -51,8 +51,9 @@ def test_catalog_games(game_slugs, all_games): """Test games() function returns df of game slugs and titles.""" assert isinstance(all_games, pd.DataFrame) - # The games() function should return exactly the set of slugs found above - assert set(all_games["Game"]) == game_slugs + # The games() function should return set of slugs plus family games + fg = gbt.catalog.family_games().keys() + assert set(all_games["Game"]) == game_slugs.union(fg) # Test that standard columns are present assert "Game" in all_games.columns From 48dd30aaa0fcd29314539ab71c15319d25f6414c Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 19 Mar 2026 13:48:25 +0000 Subject: [PATCH 099/119] update biblio links in EFGs --- catalog/myerson1991/fig2_1.efg | 4 ++-- catalog/reiley2008/fig1.efg | 4 ++-- catalog/watson2013/exercise29_6.efg | 4 ++-- catalog/watson2013/fig29_1.efg | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/catalog/myerson1991/fig2_1.efg b/catalog/myerson1991/fig2_1.efg index a95745147..63b992f04 100644 --- a/catalog/myerson1991/fig2_1.efg +++ b/catalog/myerson1991/fig2_1.efg @@ -1,5 +1,5 @@ EFG 2 R "A simple Poker game" { "Fred" "Alice" } -"This is a simple game of one-card poker from Myerson [^Mye91], used as the +"This is a simple game of one-card poker from Myerson [Mye91]_, used as the introductory example for game models. Note that as specified in the text, the game has the slightly unusual feature @@ -13,7 +13,7 @@ reiley2008/fig1 than a win. -[^Mye1991]: Myerson, Roger B. (1991) Game Theory: Analysis of Conflict. +[Mye91]_: Myerson, Roger B. (1991) Game Theory: Analysis of Conflict. Cambridge: Harvard University Press. " diff --git a/catalog/reiley2008/fig1.efg b/catalog/reiley2008/fig1.efg index 9083ff0f6..47c731800 100644 --- a/catalog/reiley2008/fig1.efg +++ b/catalog/reiley2008/fig1.efg @@ -1,12 +1,12 @@ 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 [Rei2008]_ as a teaching exercise. See also -------- myerson1991/fig2_1 Another one-card poker game with slightly different rules. -[^Rei2008]: Reiley, David H., Urbancic, Michael B, and Walker, Mark. (2008) +[Rei2008]_: Reiley, David H., Urbancic, Michael B, and Walker, Mark. (2008) Stripped-Down Poker: A Classroom Game with Signaling and Bluffing. _The Journal of Economic Education_ 4: 323-341. " diff --git a/catalog/watson2013/exercise29_6.efg b/catalog/watson2013/exercise29_6.efg index 8ca0f7a3d..51862847c 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 [Wat13]_, based on a scene from the Rob Reiner film, _The Princess Bride_: Wesley (the protagonist) confronts the evil prince Humperdinck. Wesley @@ -16,7 +16,7 @@ swordsman. Also, the weak Wesley must pay a cost to get out of bed. In the game in this file, the cost the weak Wesley pays to get out of bed is set to 2. -[^Wat13]: Watson, Joel. (2013) Strategy: An Introduction to Game Theory, +[Wat13]_: Watson, Joel. (2013) Strategy: An Introduction to Game Theory, third edition. W. W. Norton & Company. " diff --git a/catalog/watson2013/fig29_1.efg b/catalog/watson2013/fig29_1.efg index 1ec266fa2..debde3978 100644 --- a/catalog/watson2013/fig29_1.efg +++ b/catalog/watson2013/fig29_1.efg @@ -1,8 +1,8 @@ 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 [Wat13]_. -[^Wat13]: Watson, Joel. (2013) Strategy: An Introduction to Game Theory, +[Wat13]_: Watson, Joel. (2013) Strategy: An Introduction to Game Theory, third edition. W. W. Norton & Company. " From 67076e59353ea1ad83ff72beddc79ed166202536 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 19 Mar 2026 13:50:57 +0000 Subject: [PATCH 100/119] reference bibliography in updating the catalog --- doc/biblio.rst | 2 ++ doc/developer.catalog.rst | 1 + 2 files changed, 3 insertions(+) diff --git a/doc/biblio.rst b/doc/biblio.rst index 8c12265be..9862c0704 100644 --- a/doc/biblio.rst +++ b/doc/biblio.rst @@ -1,3 +1,5 @@ +.. _bibliography: + Bibliography ============ diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index 66a224cb7..836975213 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -19,6 +19,7 @@ Add new game files 1. **Create the game file:** Use either :ref:`pygambit `, the Gambit :ref:`CLI ` or :ref:`GUI ` to create and save game in a valid representation :ref:`format `. + Make sure the game includes a description, with any citations referencing the :ref:`bibliography ` with the format ``[citation_key]_`` e.g. ``[Mye91]_``. 2. **Add the game file:** From 93cae0eeaec1086ccd572a3c7badce589177cc99 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 19 Mar 2026 13:56:07 +0000 Subject: [PATCH 101/119] update bagwell biblio link --- catalog/bagwell1995.efg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/catalog/bagwell1995.efg b/catalog/bagwell1995.efg index 58889c664..9c8cd3a59 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 Bagwell [^Bag1995]. The outcomes and payoffs are the same as in Bagwell's +analysis of Bagwell [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 @@ -8,7 +8,7 @@ choose the actions that would form an equilibrium if the game was a *simultaneou game. There is an equilibrium in which the 'Stackelberg' action is played with high probability, but strictly less than one. -[^Bag1995]: Bagwell, Kyle (1995) Commitment and observability in games. +[Bag1995]_: Bagwell, Kyle (1995) Commitment and observability in games. _Games and Economic Behavior_ 8: 271-280. " From 9b8965502bb5740f9a05d146bd01956bd7f744ac Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 19 Mar 2026 16:04:42 +0000 Subject: [PATCH 102/119] update biblio --- doc/biblio.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/biblio.rst b/doc/biblio.rst index 9862c0704..58e91900c 100644 --- a/doc/biblio.rst +++ b/doc/biblio.rst @@ -12,6 +12,9 @@ Bibliography Articles on computation of Nash equilibria ------------------------------------------ +.. [Bag1995] Bagwell, Kyle (1995) Commitment and observability in games. + Games and Economic Behavior 8: 271-280. + .. [BlaTur23] Bland, J. R. and Turocy, T. L., 2023. Quantal response equilibrium as a structural model for estimation: The missing manual. SSRN working paper 4425515. @@ -54,6 +57,10 @@ Articles on computation of Nash equilibria "Simple search methods for finding a Nash equilibrium." Games and Economic Behavior 664-669, 2004. +.. [Rei2008] Reiley, David H., Urbancic, Michael B, and Walker, Mark. (2008) + Stripped-Down Poker: A Classroom Game with Signaling and Bluffing. + The Journal of Economic Education 4: 323-341. + .. [Ros71] J. Rosenmuller, "On a generalization of the Lemke-Howson Algorithm to noncooperative n-person games", 73-79, SIAM Journal of Applied Mathematics, 21, 1971. @@ -73,6 +80,9 @@ Articles on computation of Nash equilibria complementarity problem on a product of unit simplices using a general labelling", 377-397, Mathematics of Operations Research , 1987. +.. [Wat13] Watson, Joel. (2013) Strategy: An Introduction to Game Theory, + third edition. W. W. Norton & Company. + .. [Wil71] Robert Wilson, "Computing equilibria of n-person games", 80-87, SIAM Applied Math, 21, 1971. From ac15fb887b57e005f27d24e8958e69aa51568a1e Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 19 Mar 2026 16:06:25 +0000 Subject: [PATCH 103/119] remove rogue underscores --- catalog/bagwell1995.efg | 2 +- catalog/reiley2008/fig1.efg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/catalog/bagwell1995.efg b/catalog/bagwell1995.efg index 9c8cd3a59..3c9c92101 100644 --- a/catalog/bagwell1995.efg +++ b/catalog/bagwell1995.efg @@ -9,7 +9,7 @@ game. There is an equilibrium in which the 'Stackelberg' action is played with probability, but strictly less than one. [Bag1995]_: Bagwell, Kyle (1995) Commitment and observability in games. - _Games and Economic Behavior_ 8: 271-280. + Games and Economic Behavior 8: 271-280. " p "" 1 1 "" { "S" "C" } 0 diff --git a/catalog/reiley2008/fig1.efg b/catalog/reiley2008/fig1.efg index 47c731800..d6947db78 100644 --- a/catalog/reiley2008/fig1.efg +++ b/catalog/reiley2008/fig1.efg @@ -8,7 +8,7 @@ myerson1991/fig2_1 [Rei2008]_: Reiley, David H., Urbancic, Michael B, and Walker, Mark. (2008) Stripped-Down Poker: A Classroom Game with Signaling and Bluffing. - _The Journal of Economic Education_ 4: 323-341. + The Journal of Economic Education 4: 323-341. " c "" 1 "" { "King" 1/2 "Queen" 1/2 } 0 From 5c1fd8701c2f096a91598ceee02cce59f64802e7 Mon Sep 17 00:00:00 2001 From: rahulsavani Date: Thu, 19 Mar 2026 18:00:44 +0000 Subject: [PATCH 104/119] Harvard style in biblio.rst; Reiley and Bagwell to general papers; Watson to textbooks --- doc/biblio.rst | 183 ++++++++++++++++++++++++++----------------------- 1 file changed, 98 insertions(+), 85 deletions(-) diff --git a/doc/biblio.rst b/doc/biblio.rst index 58e91900c..ca292ffc4 100644 --- a/doc/biblio.rst +++ b/doc/biblio.rst @@ -12,131 +12,144 @@ Bibliography Articles on computation of Nash equilibria ------------------------------------------ -.. [Bag1995] Bagwell, Kyle (1995) Commitment and observability in games. - Games and Economic Behavior 8: 271-280. +.. [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. -.. [BlaTur23] Bland, J. R. and Turocy, T. L., 2023. Quantal response equilibrium - as a structural model for estimation: The missing manual. - SSRN working paper 4425515. +.. [Eav71] Eaves, B. C. 1971, 'The linear complementarity problem', + *Management Science*, vol. 17, pp. 612-634. -.. [Eav71] B. C. Eaves, "The linear complementarity problem", 612-634, - Management Science , 17, 1971. +.. [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. -.. [GovWil03] Govindan, Srihari and Robert Wilson. (2003) - โ€œA Global Newton Method to Compute Nash Equilibria.โ€ - Journal of Economic Theory 110(1): 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. -.. [GovWil04] Govindan, Srihari and Robert Wilson. (2004) - โ€œComputing Nash Equilibria by Iterated Polymatrix Approximation.โ€ - Journal of Economic Dynamics and Control 28: 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. -.. [Jiang11] A. X. Jiang, K. Leyton-Brown, and N. Bhat. (2011) - "Action-Graph Games." Games and Economic Behavior 71(1): 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. -.. [KolMegSte94] Daphne Koller, Nimrod Megiddo, and Bernhard von - Stengel (1996). - "Efficient computation of equilibria for extensive two-person games." - Games and Economic Behavior 14: 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. -.. [LemHow64] C. E. Lemke and J. T. Howson, "Equilibrium points of - bimatrix games", 413-423, Journal of the Society of Industrial and - Applied Mathematics , 12, 1964. +.. [Man64] Mangasarian, O. 1964, 'Equilibrium points in bimatrix games', + *Journal of the Society for Industrial and Applied Mathematics*, + vol. 12, pp. 778-780. -.. [Man64] O. Mangasarian, "Equilibrium points in bimatrix games", - 778-780, Journal of the Society for Industrial and Applied - Mathematics, 12, 1964. +.. [McK91] McKelvey, R. 1991, 'A Liapunov function for Nash equilibria', + California Institute of Technology. -.. [McK91] Richard McKelvey, A Liapunov function for Nash equilibria, - 1991, 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. -.. [McKMcL96] Richard McKelvey and Andrew McLennan, "Computation of - equilibria in finite games", 87-142, Handbook of Computational - Economics , Edited by H. Amman, D. Kendrick, J. Rust, Elsevier, 1996. +.. [PNS04] Porter, R., Nudelman, E. and Shoham, Y. 2004, + 'Simple search methods for finding a Nash equilibrium', + *Games and Economic Behavior*, pp. 664-669. -.. [PNS04] Ryan Porter, Eugene Nudelman, and Yoav Shoham. - "Simple search methods for finding a Nash equilibrium." - Games and Economic Behavior 664-669, 2004. +.. [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. -.. [Rei2008] Reiley, David H., Urbancic, Michael B, and Walker, Mark. (2008) - Stripped-Down Poker: A Classroom Game with Signaling and Bluffing. - The Journal of Economic Education 4: 323-341. +.. [Sha74] Shapley, L. 1974, 'A note on the Lemke-Howson algorithm', + *Mathematical Programming Study*, vol. 1, pp. 175-189. -.. [Ros71] J. Rosenmuller, "On a generalization of the Lemke-Howson - Algorithm to noncooperative n-person games", 73-79, SIAM Journal of - Applied Mathematics, 21, 1971. +.. [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. -.. [Sha74] Lloyd Shapley, "A note on the Lemke-Howson algorithm", 175-189, - Mathematical Programming Study , 1, 1974. +.. [Tur10] Turocy, T. L. 2010, + 'Using quantal response to compute Nash and sequential equilibria', + *Economic Theory*, vol. 42, no. 1, pp. 255-269. -.. [Tur05] Theodore L. Turocy, "A dynamic homotopy interpretation of the - logistic quantal response equilibrium correspondence", 243-263, Games - and Economic Behavior, 51, 2005. - -.. [Tur10] Theodore L. Turocy, "Using Quantal Response to Compute - Nash and Sequential Equilibria." Economic Theory 42(1): 255-269, 2010. - -.. [VTH87] G. van der Laan, A. J. J. Talman, and L. van Der Heyden, - "Simplicial variable dimension algorithms for solving the nonlinear +.. [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", 377-397, Mathematics of Operations Research , 1987. - -.. [Wat13] Watson, Joel. (2013) Strategy: An Introduction to Game Theory, - third edition. W. W. Norton & Company. + labelling', *Mathematics of Operations Research*, pp. 377-397. -.. [Wil71] Robert Wilson, "Computing equilibria of n-person games", 80-87, - SIAM Applied Math, 21, 1971. +.. [Wil71] Wilson, R. 1971, 'Computing equilibria of n-person games', + *SIAM Applied Math*, vol. 21, pp. 80-87. -.. [Yam93] Y. Yamamoto, 1993, "A Path-Following Procedure to Find a Proper - Equilibrium of Finite Games ", International Journal of Game Theory . +.. [Yam93] Yamamoto, Y. 1993, + 'A path-following procedure to find a proper equilibrium of finite + games', *International Journal of Game Theory*. General game theory articles and texts -------------------------------------- -.. [Harsanyi1967a] John Harsanyi, "Games of Incomplete Information Played - By Bayesian Players I", 159-182, Management Science , 14, 1967. +.. [Bag1995] Bagwell, K. 1995, 'Commitment and observability in games', + *Games and Economic Behavior*, vol. 8, pp. 271-280. -.. [Harsanyi1967b] John Harsanyi, "Games of Incomplete Information Played - By Bayesian Players II", 320-334, Management Science , 14, 1967. +.. [Harsanyi1967a] Harsanyi, J. 1967, + 'Games of incomplete information played by Bayesian players I', + *Management Science*, vol. 14, pp. 159-182. -.. [Harsanyi1968] John Harsanyi, "Games of Incomplete Information Played - By Bayesian Players III", 486-502, Management Science , 14, 1968. +.. [Harsanyi1967b] Harsanyi, J. 1967, + 'Games of incomplete information played by Bayesian players II', + *Management Science*, vol. 14, pp. 320-334. -.. [KreWil82] David Kreps and Robert Wilson, "Sequential Equilibria", - 863-894, Econometrica , 50, 1982. +.. [Harsanyi1968] Harsanyi, J. 1968, + 'Games of incomplete information played by Bayesian players III', + *Management Science*, vol. 14, pp. 486-502. -.. [Kre90] David Kreps, 1990, A Course in Microeconomic Theory, +.. [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] Richard McKelvey and Tom Palfrey, "Quantal response - equilibria for normal form games", 6-38, Games and Economic Behavior , - 10, 1995. +.. [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. -.. [McKPal98] Richard McKelvey and Tom Palfrey, "Quantal response - equilibria for extensive form games", 9-41, Experimental Economics , - 1, 1998. +.. [Mye78] Myerson, R. 1978, + 'Refinements of the Nash equilibrium concept', + *International Journal of Game Theory*, vol. 7, pp. 73-80. -.. [Mye78] Roger Myerson, "Refinements of the Nash equilibrium concept", - 73-80, International Journal of Game Theory , 7, 1978. +.. [Nas50] Nash, J. 1950, 'Equilibrium points in n-person games', + *Proceedings of the National Academy of Sciences*, vol. 36, + pp. 48-49. -.. [Nas50] John Nash, "Equilibrium points in n-person games", 48-49, - Proceedings of the National Academy of Sciences , 36, 1950. +.. [Och95] Ochs, J. 1995, + 'Games with unique, mixed strategy equilibria: an experimental study', + *Games and Economic Behavior*, vol. 10, pp. 202-217. -.. [Och95] Jack Ochs, "Games with unique, mixed strategy equilibria: - An experimental study", Games and Economic Behavior 10: 202-217, 1995. +.. [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] Reinhard Selten, Reexamination of the perfectness concept for - equilibrium points in extensive games , 25-55, International Journal - of Game Theory , 4, 1975. +.. [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] Eric van Damme, 1983, Stability and Perfection of Nash - Equilibria , Springer-Verlag, Berlin. +.. [vanD83] van Damme, E. 1983, *Stability and Perfection of Nash + Equilibria*, Springer-Verlag, Berlin. Textbooks and general reference ------------------------------- -.. [Mye91] Roger Myerson, 1991, Game Theory : Analysis of Conflict , +.. [Mye91] Myerson, R. 1991, *Game Theory: Analysis of Conflict*, Harvard University Press. + +.. [Wat13] Watson, J. 2013, *Strategy: An Introduction to Game Theory*, + 3rd edn, W. W. Norton & Company. From 6821e243b018ba9e0fe390b563f9819ba76d88dd Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 24 Mar 2026 10:18:14 +0000 Subject: [PATCH 105/119] docs: Add developer notes for the catalog module and its packaging. --- doc/developer.catalog.rst | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index c48821006..747d9f1f9 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -3,20 +3,32 @@ Updating the Games Catalog ========================== -This page covers the process for contributing to and updating Gambit's :ref:`Games Catalog `. +This page includes developer notes regarding the catalog module, and the process for contributing to and updating Gambit's :ref:`Games Catalog `. To do so, you will need to have the `gambit` GitHub repo cloned and be able to submit pull request via GitHub; you may wish to first review the :ref:`contributor guidelines `. You'll also need to have a developer install of `pygambit` available in your Python environment, see :ref:`build-python`. +The catalog module +------------------ + +Although the ``catalog`` directory is located at the project root outside of ``src/pygambit/``, it is installed and bundled as the ``pygambit.catalog`` subpackage. + +This is handled by the package build configuration in ``pyproject.toml`` under ``[tool.setuptools]``: + +- The ``package-dir`` mapping instructs ``setuptools`` to source the ``pygambit.catalog`` subpackage from the physical ``catalog`` directory. +- The ``package-data`` configuration ensures all non-Python data files (like ``.efg`` and ``.nfg`` files) inside the catalog are correctly bundled during installation. + +As a developer, this means you will need to reinstall the package (e.g., passing ``pip install .``) for any new game files or internal catalog changes to be reflected in the ``pygambit`` module. + +Add new game files +------------------ + You can add games to the catalog saved in a valid representation :ref:`format `. Currently supported representations are: - `.efg` for extensive form games - `.nfg` for normal form games -Add new game files ------------------- - 1. **Create the game file:** Use either :ref:`pygambit `, the Gambit :ref:`CLI ` or :ref:`GUI ` to create and save game in a valid representation :ref:`format `. From 5cc957171fc204ea9977ebbc2de5005148047725 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 24 Mar 2026 10:49:38 +0000 Subject: [PATCH 106/119] revise update script to single column --- build_support/catalog/update.py | 114 +++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 30 deletions(-) diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index fd012ee65..4f2b3b088 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -10,38 +10,94 @@ MAKEFILE_AM = Path(__file__).parent.parent.parent / "Makefile.am" +def _write_efg_table(df: pd.DataFrame, f): + """Write the EFG games list-table to file handle f.""" + f.write(".. list-table::\n") + f.write(" :header-rows: 1\n") + f.write(" :widths: 100\n") + f.write(" :class: tight-table\n") + f.write("\n") + f.write(" * - **Extensive form games**\n") + + efg_df = df[df["Format"] == "efg"] + for _, row in efg_df.iterrows(): + slug = row["Game"] + title = str(row.get("Title", "")).strip() + description = str(row.get("Description", "")).strip() + + # Main dropdown + f.write(f" * - .. dropdown:: {title}\n") + f.write(" \n") + if description: + 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") + + # Download links (inside the dropdown) + download_links = [row["Download"]] + f.write(" **Download:**\n") + f.write(" \n") + f.write(f" {' '.join(download_links)}\n") + f.write(" \n") + + +def _write_nfg_table(df: pd.DataFrame, f): + """Write the NFG games list-table to file handle f.""" + f.write(".. list-table::\n") + f.write(" :header-rows: 1\n") + f.write(" :widths: 100\n") + f.write(" :class: tight-table\n") + f.write("\n") + f.write(" * - **Strategic form games**\n") + + nfg_df = df[df["Format"] == "nfg"] + for _, row in nfg_df.iterrows(): + slug = row["Game"] + + # Title as plain text header + f.write(" * - \n") + f.write(" \n") + + # Jupyter-execute block (no dropdown) + f.write(" .. jupyter-execute::\n") + f.write(" \n") + f.write(" import pygambit\n") + f.write(f' pygambit.catalog.load("{slug}")\n') + f.write(" \n") + + # Download link (plain, no dropdown) + f.write(f" :download:`{slug}.nfg <../catalog/{slug}.nfg>`\n") + f.write(" \n") + + def generate_rst_table(df: pd.DataFrame, rst_path: Path): - """Generate a list-table RST file with dropdowns for long descriptions.""" + """Generate RST output with two list-tables: one for EFG and one for NFG games.""" + with open(rst_path, "w", encoding="utf-8") as f: - f.write(".. list-table::\n") - f.write(" :header-rows: 1\n") - f.write(" :widths: 20 80 20\n") - f.write(" :class: tight-table\n") + # TOC linking to both sections + f.write(".. contents::\n") + f.write(" :local:\n") + f.write(" :depth: 1\n") f.write("\n") - f.write(" * - **Game**\n") - f.write(" - **Description**\n") - f.write(" - **Download**\n") - - for _, row in df.iterrows(): - f.write(f" * - {row['Game']}\n") - - description_cell_lines = [] - title = str(row.get("Title", "")).strip() - description = str(row.get("Description", "")).strip() - if description: - description_cell_lines.append(f".. dropdown:: {title}") - description_cell_lines.append(" ") # Indented blank line - for line in description.splitlines(): - description_cell_lines.append(f" {line}") - else: - description_cell_lines.append(title) - - f.write(f" - {description_cell_lines[0]}\n") - for line in description_cell_lines[1:]: - f.write(f" {line}\n") + # EFG section + f.write("Extensive form games\n") + f.write("--------------------\n") + f.write("\n") + _write_efg_table(df, f) + f.write("\n") - f.write(f" - {row['Download']}\n") + # NFG section + f.write("Strategic form games\n") + f.write("--------------------\n") + f.write("\n") + _write_nfg_table(df, f) def update_makefile(): @@ -96,7 +152,6 @@ def update_makefile(): if __name__ == "__main__": - parser = argparse.ArgumentParser() parser.add_argument("--build", action="store_true") args = parser.parse_args() @@ -105,7 +160,6 @@ def update_makefile(): df = gbt.catalog.games(include_descriptions=True) generate_rst_table(df, CATALOG_RST_TABLE) print(f"Generated {CATALOG_RST_TABLE} for use in local docs build. DO NOT COMMIT.") - - # Update the Makefile.am with the current list of catalog files if args.build: + # Update the Makefile.am with the current list of catalog files update_makefile() From 9130d7f523d34985a2bdde7c957c4bb7a275bb8a Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 24 Mar 2026 10:52:22 +0000 Subject: [PATCH 107/119] add format to df returned by games() --- catalog/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index d4ca2995f..05f0c0321 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -157,6 +157,7 @@ def append_record( record["Description"] = game.description ext = "efg" if game.is_tree else "nfg" record["Download"] = f":download:`{slug}.{ext} <../catalog/{slug}.{ext}>`" + record["Format"] = ext records.append(record) # Add all the games stored as EFG/NFG files @@ -186,7 +187,7 @@ def append_record( if include_descriptions: return pd.DataFrame.from_records( - records, columns=["Game", "Title", "Description", "Download"] + records, columns=["Game", "Title", "Description", "Download", "Format"] ) return pd.DataFrame.from_records(records, columns=["Game", "Title"]) From ffcae37f85a65e65ef77d42d1fa9f6b33652dd86 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 24 Mar 2026 10:59:15 +0000 Subject: [PATCH 108/119] Further edits to update.py --- build_support/catalog/update.py | 26 ++++++++++++++------------ pyproject.toml | 3 ++- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index 4f2b3b088..cc0ef4b53 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -27,24 +27,26 @@ def _write_efg_table(df: pd.DataFrame, f): # Main dropdown f.write(f" * - .. dropdown:: {title}\n") + f.write(" :open:\n") f.write(" \n") + # Skip any games which lack a description if description: 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(" **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 (inside the dropdown) - download_links = [row["Download"]] - f.write(" **Download:**\n") - f.write(" \n") - f.write(f" {' '.join(download_links)}\n") - f.write(" \n") + # Download links (inside the dropdown) + download_links = [row["Download"]] + f.write(" **Download:**\n") + f.write(" \n") + f.write(f" {' '.join(download_links)}\n") + f.write(" \n") def _write_nfg_table(df: pd.DataFrame, f): diff --git a/pyproject.toml b/pyproject.toml index 71134753d..cd21c4102 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,8 @@ doc = [ "pickleshare", "jupyter", "open_spiel; sys_platform != 'win32'", - "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.4.0" + "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.4.0", + "jupyter_sphinx" ] [project.urls] From cce0284760c5f4b741922dd0399749e57e042698 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 24 Mar 2026 11:03:17 +0000 Subject: [PATCH 109/119] dont include catalog games without description --- build_support/catalog/update.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index cc0ef4b53..b3a510e18 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -24,13 +24,12 @@ def _write_efg_table(df: pd.DataFrame, f): slug = row["Game"] title = str(row.get("Title", "")).strip() description = str(row.get("Description", "")).strip() - - # Main dropdown - f.write(f" * - .. dropdown:: {title}\n") - f.write(" :open:\n") - f.write(" \n") # Skip any games which lack a description if description: + # 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") From 0baeb8c6a1476fa9cc55bcc4ffc8a06f23669784 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 24 Mar 2026 11:11:11 +0000 Subject: [PATCH 110/119] Ignore NFG for now since there are none in the catalog --- build_support/catalog/update.py | 80 ++++++++++++++++----------------- pyproject.toml | 3 +- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index b3a510e18..83464ab5f 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -48,33 +48,33 @@ def _write_efg_table(df: pd.DataFrame, f): f.write(" \n") -def _write_nfg_table(df: pd.DataFrame, f): - """Write the NFG games list-table to file handle f.""" - f.write(".. list-table::\n") - f.write(" :header-rows: 1\n") - f.write(" :widths: 100\n") - f.write(" :class: tight-table\n") - f.write("\n") - f.write(" * - **Strategic form games**\n") - - nfg_df = df[df["Format"] == "nfg"] - for _, row in nfg_df.iterrows(): - slug = row["Game"] - - # Title as plain text header - f.write(" * - \n") - f.write(" \n") - - # Jupyter-execute block (no dropdown) - f.write(" .. jupyter-execute::\n") - f.write(" \n") - f.write(" import pygambit\n") - f.write(f' pygambit.catalog.load("{slug}")\n') - f.write(" \n") - - # Download link (plain, no dropdown) - f.write(f" :download:`{slug}.nfg <../catalog/{slug}.nfg>`\n") - f.write(" \n") +# def _write_nfg_table(df: pd.DataFrame, f): +# """Write the NFG games list-table to file handle f.""" +# f.write(".. list-table::\n") +# f.write(" :header-rows: 1\n") +# f.write(" :widths: 100\n") +# f.write(" :class: tight-table\n") +# f.write("\n") +# f.write(" * - **Strategic form games**\n") + +# nfg_df = df[df["Format"] == "nfg"] +# for _, row in nfg_df.iterrows(): +# slug = row["Game"] + +# # Title as plain text header +# f.write(" * - \n") +# f.write(" \n") + +# # Jupyter-execute block (no dropdown) +# f.write(" .. jupyter-execute::\n") +# f.write(" \n") +# f.write(" import pygambit\n") +# f.write(f' pygambit.catalog.load("{slug}")\n') +# f.write(" \n") + +# # Download link (plain, no dropdown) +# f.write(f" :download:`{slug}.nfg <../catalog/{slug}.nfg>`\n") +# f.write(" \n") def generate_rst_table(df: pd.DataFrame, rst_path: Path): @@ -82,23 +82,23 @@ def generate_rst_table(df: pd.DataFrame, rst_path: Path): with open(rst_path, "w", encoding="utf-8") as f: # TOC linking to both sections - f.write(".. contents::\n") - f.write(" :local:\n") - f.write(" :depth: 1\n") - f.write("\n") + # f.write(".. contents::\n") + # f.write(" :local:\n") + # f.write(" :depth: 1\n") + # f.write("\n") # EFG section - f.write("Extensive form games\n") - f.write("--------------------\n") - f.write("\n") + # f.write("Extensive form games\n") + # f.write("--------------------\n") + # f.write("\n") _write_efg_table(df, f) - f.write("\n") + # f.write("\n") - # NFG section - f.write("Strategic form games\n") - f.write("--------------------\n") - f.write("\n") - _write_nfg_table(df, f) + # # NFG section + # f.write("Strategic form games\n") + # f.write("--------------------\n") + # f.write("\n") + # _write_nfg_table(df, f) def update_makefile(): diff --git a/pyproject.toml b/pyproject.toml index cd21c4102..71134753d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,8 +43,7 @@ doc = [ "pickleshare", "jupyter", "open_spiel; sys_platform != 'win32'", - "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.4.0", - "jupyter_sphinx" + "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.4.0" ] [project.urls] From 0e81a040f70eda6110a9120f6913fceb001b9ddc Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 24 Mar 2026 15:39:55 +0000 Subject: [PATCH 111/119] remove family games code --- catalog/__init__.py | 73 --------------------------------------------- 1 file changed, 73 deletions(-) diff --git a/catalog/__init__.py b/catalog/__init__.py index 05f0c0321..26104acdd 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -43,11 +43,6 @@ def load(slug: str) -> gbt.Game: with as_file(resource_path) as path: return reader(str(path)) - # Try loading from family games - fg = family_games() - if slug in fg: - return fg[slug] - # Raise error if game does not exist raise FileNotFoundError(f"No catalog entry called {slug}") @@ -175,76 +170,8 @@ def append_record( if check_filters(game): append_record(slug, game) - # Add all the games from families - for slug, game in family_games().items(): - # Throw an error if there's a slug collision between family games and file-based games - if slug in [r["Game"] for r in records]: - raise ValueError( - f"Slug collision: {slug} is present in both file-based and family games." - ) - if check_filters(game): - append_record(slug, game) - if include_descriptions: return pd.DataFrame.from_records( records, columns=["Game", "Title", "Description", "Download", "Format"] ) return pd.DataFrame.from_records(records, columns=["Game", "Title"]) - - -def family_games() -> dict[str, gbt.Game]: - """ - Generate a dict of games for inclusion in the catalog, - using the game families in this module. - - Returns - ------- - dict[str, gbt.Game] - A dictionary mapping slugs to game objects for family games. - """ - return { - "one_shot_trust": one_shot_trust(), - "oneshot_trust_unique_NE": one_shot_trust(unique_NE_variant=True), - } - - -################################################################################################ -# Families - - -def one_shot_trust(unique_NE_variant: bool = False) -> gbt.Game: - """ - The unique_NE_variant makes Trust a dominant strategy, replacing the - non-singleton equilibrium component from the standard version of the game - where the Buyer plays "Not Trust" and the seller can play any mixture with - < 0.5 probability on Honor with a unique NE where the Buyer plays Trust and - the Seller plays Abuse. - - Parameters - ---------- - unique_NE_variant : bool, optional - Whether to modify the game so that it has a unique Nash equilibrium. - Defaults to False. - - Returns - ------- - gbt.Game - The constructed extensive-form game. - """ - g = gbt.Game.new_tree(players=["Buyer", "Seller"]) - g.description = "One-shot trust game with binary actions, originally from [Kre90]_." - g.append_move(g.root, "Buyer", ["Trust", "Not trust"]) - g.append_move(g.root.children[0], "Seller", ["Honor", "Abuse"]) - g.set_outcome(g.root.children[0].children[0], g.add_outcome([1, 1], label="Trustworthy")) - if unique_NE_variant: - g.title = "One-shot trust game with unique NE" - g.set_outcome( - g.root.children[0].children[1], g.add_outcome(["1/2", 2], label="Untrustworthy") - ) - else: - g.title = "One-shot trust game" - g.set_outcome( - g.root.children[0].children[1], g.add_outcome([-1, 2], label="Untrustworthy") - ) - g.set_outcome(g.root.children[1], g.add_outcome([0, 0], label="Opt-out")) - return g From 1ea66cc93b44414ec9906a6e0a1689d2994ac494 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 24 Mar 2026 15:46:58 +0000 Subject: [PATCH 112/119] remove game family test --- tests/test_catalog.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 0c2a2d496..c034a3cbd 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -41,20 +41,10 @@ def test_catalog_load_invalid_slug(): gbt.catalog.load("invalid_slug") -def test_catalog_load_family_game(): - """Test loading a game generated from code with a game family func.""" - g = gbt.catalog.load("one_shot_trust") - assert isinstance(g, gbt.Game) - - def test_catalog_games(game_slugs, all_games): """Test games() function returns df of game slugs and titles.""" assert isinstance(all_games, pd.DataFrame) - # The games() function should return set of slugs plus family games - fg = gbt.catalog.family_games().keys() - assert set(all_games["Game"]) == game_slugs.union(fg) - # Test that standard columns are present assert "Game" in all_games.columns assert "Title" in all_games.columns From de828544a8cd1b4dac0561c17e976932529476a2 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 24 Mar 2026 16:40:56 +0000 Subject: [PATCH 113/119] Use readthedocs biblio links in EFG files --- catalog/bagwell1995.efg | 5 +---- catalog/myerson1991/fig2_1.efg | 10 ++-------- catalog/myerson1991/fig4_2.efg | 5 +---- catalog/reiley2008/fig1.efg | 10 ++-------- catalog/selten1975/fig1.efg | 6 +----- catalog/selten1975/fig2.efg | 6 +----- catalog/selten1975/fig3.efg | 6 +----- 7 files changed, 9 insertions(+), 39 deletions(-) diff --git a/catalog/bagwell1995.efg b/catalog/bagwell1995.efg index 3c9c92101..3783d1649 100644 --- a/catalog/bagwell1995.efg +++ b/catalog/bagwell1995.efg @@ -1,15 +1,12 @@ 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 Bagwell [Bag1995]_. The outcomes and payoffs are the same as in Bagwell's +analysis of `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 choose the actions that would form an equilibrium if the game was a *simultaneous-move* game. There is an equilibrium in which the 'Stackelberg' action is played with high probability, but strictly less than one. - -[Bag1995]_: Bagwell, Kyle (1995) Commitment and observability in games. - Games and Economic Behavior 8: 271-280. " p "" 1 1 "" { "S" "C" } 0 diff --git a/catalog/myerson1991/fig2_1.efg b/catalog/myerson1991/fig2_1.efg index 63b992f04..3c45dedee 100644 --- a/catalog/myerson1991/fig2_1.efg +++ b/catalog/myerson1991/fig2_1.efg @@ -1,20 +1,14 @@ EFG 2 R "A simple Poker game" { "Fred" "Alice" } -"This is a simple game of one-card poker from Myerson [Mye91]_, used as the +"This is a simple game of one-card poker from `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 --------- -reiley2008/fig1 +See also `Rei2008 `_ Another one-card poker game where folding with the high card is a loss rather than a win. - - -[Mye91]_: Myerson, Roger B. (1991) Game Theory: Analysis of Conflict. - Cambridge: Harvard University Press. " c "" 1 "" { "Red" 1/2 "Black" 1/2 } 0 diff --git a/catalog/myerson1991/fig4_2.efg b/catalog/myerson1991/fig4_2.efg index c36cf1e13..d7b503bea 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 Myerson [Mye91]_ which illustrates the distinction between +"An example from `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 @@ -7,9 +7,6 @@ representation. However, it is not a Nash equilibrium of the extensive game, because Player 1 would prefer to switch from (B1, Z1) to (A1, Y1); the (multi)agent representation rules out such coordinated deviations across information sets. - -[Mye91]_: Myerson, Roger B. (1991) Game Theory: Analysis of Conflict. - Cambridge: Harvard University Press. " p "" 1 1 "" { "A1" "B1" } 0 diff --git a/catalog/reiley2008/fig1.efg b/catalog/reiley2008/fig1.efg index d6947db78..df8c5a4d8 100644 --- a/catalog/reiley2008/fig1.efg +++ b/catalog/reiley2008/fig1.efg @@ -1,14 +1,8 @@ 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 `Rei2008 `_ as a teaching exercise. -See also --------- -myerson1991/fig2_1 +See also `Mye91 `_ Another one-card poker game with slightly different rules. - -[Rei2008]_: Reiley, David H., Urbancic, Michael B, and Walker, Mark. (2008) - Stripped-Down Poker: A Classroom Game with Signaling and Bluffing. - The Journal of Economic Education 4: 323-341. " c "" 1 "" { "King" 1/2 "Queen" 1/2 } 0 diff --git a/catalog/selten1975/fig1.efg b/catalog/selten1975/fig1.efg index ea47ae88f..039d50f8f 100644 --- a/catalog/selten1975/fig1.efg +++ b/catalog/selten1975/fig1.efg @@ -1,14 +1,10 @@ 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 Selten [Sel75]_, commonly referred +"This is a three-player game presented in `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 is \"unreasonable\", but which is not ruled out by subgame perfection because this game has no proper subgames. - -[Sel75]_: Selten, Reinhard (1975). A reexamination of the perfectness concept - for equilibrium points in extensive games. International Journal of Game - Theory 4(1): 25-55. " p "" 1 1 "" { "R" "L" } 0 diff --git a/catalog/selten1975/fig2.efg b/catalog/selten1975/fig2.efg index ab52adaa5..116dadb5d 100644 --- a/catalog/selten1975/fig2.efg +++ b/catalog/selten1975/fig2.efg @@ -1,12 +1,8 @@ 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 `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. - -[Sel75]_: Selten, Reinhard (1975). A reexamination of the perfectness concept - for equilibrium points in extensive games. International Journal of Game - Theory 4(1): 25-55. " p "" 1 1 "" { "R" "L" } 0 diff --git a/catalog/selten1975/fig3.efg b/catalog/selten1975/fig3.efg index 25c123b05..f7364f91b 100644 --- a/catalog/selten1975/fig3.efg +++ b/catalog/selten1975/fig3.efg @@ -1,12 +1,8 @@ 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 `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. - -[Sel75]_: Selten, Reinhard (1975). A reexamination of the perfectness concept - for equilibrium points in extensive games. International Journal of Game - Theory 4(1): 25-55. " p "" 1 1 "" { "R" "L" } 0 From 4c6e901208f10876a1f832255db09c2f41bdd2d2 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 24 Mar 2026 16:53:09 +0000 Subject: [PATCH 114/119] update links in watson games --- catalog/watson2013/exercise29_6.efg | 7 ++----- catalog/watson2013/fig29_1.efg | 5 +---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/catalog/watson2013/exercise29_6.efg b/catalog/watson2013/exercise29_6.efg index 51862847c..131d3b9f5 100644 --- a/catalog/watson2013/exercise29_6.efg +++ b/catalog/watson2013/exercise29_6.efg @@ -1,6 +1,6 @@ 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 -the Rob Reiner film, _The Princess Bride_: +"This game is Exercise 29.6 from Watson `Wat13 `, based on a scene from +the Rob Reiner film, The Princess Bride: Wesley (the protagonist) confronts the evil prince Humperdinck. Wesley is one of two types: weak or strong. Wesley knows whether he is weak or @@ -15,9 +15,6 @@ swordsman. Also, the weak Wesley must pay a cost to get out of bed. In the game in this file, the cost the weak Wesley pays to get out of bed is set to 2. - -[Wat13]_: Watson, Joel. (2013) Strategy: An Introduction to Game Theory, - third edition. W. W. Norton & Company. " c "" 1 "" { "Strong" 1/2 "Weak" 1/2 } 0 diff --git a/catalog/watson2013/fig29_1.efg b/catalog/watson2013/fig29_1.efg index debde3978..6b08714b2 100644 --- a/catalog/watson2013/fig29_1.efg +++ b/catalog/watson2013/fig29_1.efg @@ -1,9 +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]_. - -[Wat13]_: Watson, Joel. (2013) Strategy: An Introduction to Game Theory, - third edition. W. W. Norton & Company. +signal, as presented in Figure 29.1 of Watson `Wat13 `_. " c "" 1 "" { "High" 1/3 "Low" 2/3 } 0 From 01fd96b7c7fe5457070ebdf38fa123e72b86877a Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 24 Mar 2026 16:56:51 +0000 Subject: [PATCH 115/119] add missing underscore --- catalog/watson2013/exercise29_6.efg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalog/watson2013/exercise29_6.efg b/catalog/watson2013/exercise29_6.efg index 131d3b9f5..e1fbc7551 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 `Wat13 `_, based on a scene from the Rob Reiner film, The Princess Bride: Wesley (the protagonist) confronts the evil prince Humperdinck. Wesley From aae1504dbbc5258e4e24022783655fe826d06d7c Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 24 Mar 2026 16:58:10 +0000 Subject: [PATCH 116/119] update how to include the game files --- 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 747d9f1f9..a0aef0e7a 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -32,7 +32,8 @@ Currently supported representations are: 1. **Create the game file:** Use either :ref:`pygambit `, the Gambit :ref:`CLI ` or :ref:`GUI ` to create and save game in a valid representation :ref:`format `. - Make sure the game includes a description, with any citations referencing the :ref:`bibliography ` with the format ``[citation_key]_`` e.g. ``[Mye91]_``. + 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 `_`` 2. **Add the game file:** From 3b014b4704ffe56530dc3d3469772bb13ace924c Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 24 Mar 2026 17:00:32 +0000 Subject: [PATCH 117/119] remove coded games section of doc --- doc/developer.catalog.rst | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index a0aef0e7a..a967369ba 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -58,18 +58,3 @@ Currently supported representations are: .. warning:: Make sure you commit all changed files e.g. run ``git add --all`` before committing and pushing. - -Code new games & add game families ----------------------------------- - -1. **Add the game code:** - - Open `catalog/__init__.py` and create a new function, or modify an existing one. Ensure your function returns a ``Game`` object. - You may wish to vary the game title and/or description based on the chosen parameters. - -2. **Update the catalog:** - - Update the dictionary returned by ``family_games()`` in `catalog/__init__.py` with all variants of your game(s) you want in the catalog. - Ensure each entry has unique game slug as key (this will be used by ``pygambit.catalog.load('slug')``), and returns a call of the function with specific parameters. - -3. **Submit a pull request to GitHub with all changes.** From d434613689710969c97b89abaf2fd1fa8f02d892 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 24 Mar 2026 17:10:24 +0000 Subject: [PATCH 118/119] update homepage with catalog --- doc/catalog.rst | 2 ++ doc/index.rst | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/doc/catalog.rst b/doc/catalog.rst index a7d132c4a..2c90df4c8 100644 --- a/doc/catalog.rst +++ b/doc/catalog.rst @@ -1,3 +1,5 @@ +.. _catalog: + Catalog of games ================ diff --git a/doc/index.rst b/doc/index.rst index 101bba6d5..d04145faf 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -56,6 +56,28 @@ construction and analysis of finite extensive and strategic games. :color: secondary :expand: + .. grid-item-card:: ๐Ÿ“š Catalog of games + :columns: 4 + + Browse a curated collection of game theory models. + + .. button-ref:: catalog + :ref-type: ref + :click-parent: + :color: secondary + :expand: + + .. grid-item-card:: ๐Ÿ’ป Command-line interface + :columns: 4 + + Use Gambit's command-line tools for scripting. + + .. button-ref:: command-line + :ref-type: ref + :click-parent: + :color: secondary + :expand: + .. grid-item-card:: ๐Ÿ› Bugs and feature requests :columns: 4 From 292a3fe903ad836c619d6fd8c86b4bf5f9824b8f Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 24 Mar 2026 17:12:48 +0000 Subject: [PATCH 119/119] refactor: Adjust grid-item-card column widths and reorder sections in index.rst. --- doc/index.rst | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index d04145faf..4d9cd59b0 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -10,7 +10,7 @@ construction and analysis of finite extensive and strategic games. .. grid:: .. grid-item-card:: โฌ‡๏ธ Installing Gambit - :columns: 4 + :columns: 3 Quick installation with PyGambit: ``pip install pygambit`` @@ -23,7 +23,7 @@ construction and analysis of finite extensive and strategic games. .. grid-item-card:: ๐Ÿ PyGambit - :columns: 4 + :columns: 3 Explore tutorial notebooks and API reference docs. @@ -35,7 +35,7 @@ construction and analysis of finite extensive and strategic games. .. grid-item-card:: ๐Ÿงฎ Analysing games - :columns: 4 + :columns: 3 Compute equilibria and run econometric estimations. @@ -45,30 +45,30 @@ construction and analysis of finite extensive and strategic games. :color: secondary :expand: - .. grid-item-card:: ๐Ÿ–ฑ๏ธ Graphical interface - :columns: 4 + .. grid-item-card:: ๐Ÿ“š Catalog of games + :columns: 3 - Interactively create, explore, and find equilibria of games. + Browse a curated collection of game theory models. - .. button-ref:: section-gui + .. button-ref:: catalog :ref-type: ref :click-parent: :color: secondary :expand: - .. grid-item-card:: ๐Ÿ“š Catalog of games - :columns: 4 + .. grid-item-card:: ๐Ÿ–ฑ๏ธ Graphical interface + :columns: 3 - Browse a curated collection of game theory models. + Interactively create, explore, and find equilibria of games. - .. button-ref:: catalog + .. button-ref:: section-gui :ref-type: ref :click-parent: :color: secondary :expand: .. grid-item-card:: ๐Ÿ’ป Command-line interface - :columns: 4 + :columns: 3 Use Gambit's command-line tools for scripting. @@ -79,7 +79,7 @@ construction and analysis of finite extensive and strategic games. :expand: .. grid-item-card:: ๐Ÿ› Bugs and feature requests - :columns: 4 + :columns: 3 Report bugs and feature requests on GitHub. @@ -90,7 +90,7 @@ construction and analysis of finite extensive and strategic games. :expand: .. grid-item-card:: ๐Ÿ“– Developer docs - :columns: 4 + :columns: 3 Guides for developers & contributors to the package.