From 90a5e367095e2b842374dc0de491f7829b17f724 Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Tue, 3 Feb 2026 10:11:54 +0000 Subject: [PATCH 01/64] 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 02/64] 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 03/64] 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 04/64] 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 05/64] 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 06/64] 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 07/64] 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 08/64] 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 09/64] 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 10/64] 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 11/64] 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 12/64] 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 13/64] 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 14/64] 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 15/64] 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 16/64] 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 17/64] 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 18/64] 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 19/64] 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 20/64] 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 21/64] 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 22/64] 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 23/64] 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 24/64] 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 25/64] 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 26/64] 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 27/64] 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 28/64] 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 29/64] 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 30/64] 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 31/64] 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 32/64] 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 33/64] 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 34/64] 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 35/64] 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 36/64] 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 37/64] 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 38/64] 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 39/64] 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 40/64] 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 41/64] 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 42/64] 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 43/64] 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 44/64] 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 45/64] 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 46/64] 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 47/64] 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 48/64] 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 49/64] 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 50/64] 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 51/64] 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 52/64] 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 53/64] 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 54/64] 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 55/64] 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 56/64] 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 57/64] 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 58/64] 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 59/64] 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 60/64] 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 61/64] 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 62/64] 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 63/64] 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 64/64] 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.**