Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
90a5e36
Create catalog dir and move one EFG and one NFG into it from contrib/…
edwardchalstrey1 Feb 3, 2026
83dd410
ignore catalog files copied into pygambit
edwardchalstrey1 Feb 3, 2026
7fc2199
add failing tests
edwardchalstrey1 Feb 3, 2026
3c53bea
improve tests
edwardchalstrey1 Feb 3, 2026
898a916
add pandas to pyproject.toml
edwardchalstrey1 Feb 3, 2026
8f916db
add test_catalog_load_invalid_slug
edwardchalstrey1 Feb 3, 2026
606dd8a
create load function
edwardchalstrey1 Feb 3, 2026
00beed3
add catalog to __init__
edwardchalstrey1 Feb 3, 2026
64eb310
add games() function
edwardchalstrey1 Feb 3, 2026
d24befd
refactor to define READERS once
edwardchalstrey1 Feb 3, 2026
72aade1
Big refactor to get catalog files from catalog dir external to pygambit
edwardchalstrey1 Feb 3, 2026
e7953e9
update Makefile.am for the 2 examples we moved into the catalog so far
edwardchalstrey1 Feb 3, 2026
7490ae6
update Game.comment to be Game.description in Python code
edwardchalstrey1 Feb 4, 2026
ebf6829
Revert "update Game.comment to be Game.description in Python code"
edwardchalstrey1 Feb 4, 2026
fbf6b89
Add initial update catalog script and RST page
edwardchalstrey1 Feb 4, 2026
1ca3461
rename table headers on output df from game() func
edwardchalstrey1 Feb 4, 2026
bd3c1f3
add generating the catalog csv for docs into GH actions
edwardchalstrey1 Feb 4, 2026
569148c
Revert "add generating the catalog csv for docs into GH actions"
edwardchalstrey1 Feb 4, 2026
8cf0396
refactor update script so its run from repo root
edwardchalstrey1 Feb 4, 2026
1b20438
update readthedocs to build the catalog csv before docs build
edwardchalstrey1 Feb 4, 2026
e977e64
add function which updates Makefile.am
edwardchalstrey1 Feb 4, 2026
af08d29
use a proper path for CATALOG_CSV
edwardchalstrey1 Feb 4, 2026
cd2ffc9
tidy update script
edwardchalstrey1 Feb 4, 2026
5675444
use explicit python executable from the virtualenv to create CSV for …
edwardchalstrey1 Feb 4, 2026
f574223
add developer doc for updating the catalog
edwardchalstrey1 Feb 4, 2026
5147278
Don't update Makefile.am by default
edwardchalstrey1 Feb 4, 2026
4b033c9
consistency in notebook comment
edwardchalstrey1 Feb 4, 2026
b14402a
demo loading from catalog in tutorial 1
edwardchalstrey1 Feb 4, 2026
264f7fb
load from catalog for game examples in advanced tutorials
edwardchalstrey1 Feb 4, 2026
0e268a2
Try using pip instead of setuptools to ensure pyproject.toml deps ins…
edwardchalstrey1 Feb 4, 2026
8649946
remove deleted contrib games from Makefile.am
edwardchalstrey1 Feb 4, 2026
adea4f2
add a warning about moving games from contrib
edwardchalstrey1 Feb 4, 2026
a72f7a2
check if pandas duplications error exists if we dont save outputs on …
edwardchalstrey1 Feb 4, 2026
57863d4
fix problem with print function
edwardchalstrey1 Feb 4, 2026
b687e20
resave notebook outputs
edwardchalstrey1 Feb 4, 2026
6562433
Update writer.cc
tturocy Feb 10, 2026
39fde9d
Merge branch 'master' into ehancement/731/take2
edwardchalstrey1 Feb 10, 2026
f84bcc8
move catalog update script into build support
edwardchalstrey1 Feb 10, 2026
d17f976
rename script
edwardchalstrey1 Feb 10, 2026
f07fdae
rename var
edwardchalstrey1 Feb 10, 2026
fd66e41
update path to catalog update script in readthedocs yml and docs page
edwardchalstrey1 Feb 10, 2026
bb1e920
move myserson fig into subfolder
edwardchalstrey1 Feb 10, 2026
5c7a60f
clarify script usage
edwardchalstrey1 Feb 10, 2026
dc7a373
add test_catalog_load_subdir_slug
edwardchalstrey1 Feb 10, 2026
7850585
update makefile
edwardchalstrey1 Feb 10, 2026
f6ea5df
update agent nb
edwardchalstrey1 Feb 10, 2026
f655645
add test for slug in subdir of catalog
edwardchalstrey1 Feb 10, 2026
56cd19a
update games func to list slugs correctly
edwardchalstrey1 Feb 10, 2026
6a4df8f
update test to avoid duplicates
edwardchalstrey1 Feb 10, 2026
886131c
fix code for handling slugs that duplicates of those in subfolders
edwardchalstrey1 Feb 10, 2026
68909b9
tidy the games() refactor
edwardchalstrey1 Feb 10, 2026
bb8f483
resave notebook
edwardchalstrey1 Feb 10, 2026
50e618b
strip nb outputs
edwardchalstrey1 Feb 10, 2026
67dedb2
remove modification to games() that was fixing a local issue
edwardchalstrey1 Feb 10, 2026
1794a83
fix the update script to get correct paths
edwardchalstrey1 Feb 10, 2026
079aacd
remove unused var
edwardchalstrey1 Feb 10, 2026
e57cfce
Add Windows handling
edwardchalstrey1 Feb 10, 2026
bdc5d3a
fix incorrect var name and make consistent
edwardchalstrey1 Feb 11, 2026
69d8cb9
use as_posix for slugs
edwardchalstrey1 Feb 11, 2026
b045083
move load and games functions from __init__.py to utils.py
edwardchalstrey1 Feb 13, 2026
d8ee58e
add families module
edwardchalstrey1 Feb 13, 2026
a0b66c0
add family games to games()
edwardchalstrey1 Feb 13, 2026
939a56f
update load function to look in family games
edwardchalstrey1 Feb 13, 2026
1df3db7
alternate titles in example game family
edwardchalstrey1 Feb 13, 2026
e0c4b7c
Add to developer doc page
edwardchalstrey1 Feb 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@ Gambit.app/*
*.ef
build_support/msw/gambit.wxs
build_support/osx/Info.plist
src/pygambit/catalog
doc/catalog.csv
6 changes: 5 additions & 1 deletion .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ build:
- libgmp-dev
- pandoc
- texlive-full
jobs:
# Create CSV for catalog table in docs
post_install:
- $READTHEDOCS_VIRTUALENV_PATH/bin/python build_support/catalog/update.py

python:
install:
- requirements: doc/requirements.txt
- method: setuptools
- method: pip
path: "."
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 5 additions & 5 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -169,7 +168,6 @@ EXTRA_DIST = \
contrib/games/my_3-3d.efg \
contrib/games/my_3-3e.efg \
contrib/games/my_3-4.efg \
contrib/games/myerson.efg \
contrib/games/nim7.efg \
contrib/games/nim.efg \
contrib/games/palf2.efg \
Expand All @@ -195,7 +193,6 @@ EXTRA_DIST = \
contrib/games/2x2a.nfg \
contrib/games/2x2const.nfg \
contrib/games/2x2.nfg \
contrib/games/2x2x2.nfg \
contrib/games/2x2x2x2.nfg \
contrib/games/2x2x2x2x2.nfg \
contrib/games/3x3x3.nfg \
Expand Down Expand Up @@ -224,7 +221,6 @@ EXTRA_DIST = \
contrib/games/mixdom2.nfg \
contrib/games/mixdom.nfg \
contrib/games/oneill.nfg \
contrib/games/pd.nfg \
contrib/games/perfect1.nfg \
contrib/games/perfect2.nfg \
contrib/games/perfect3.nfg \
Expand All @@ -240,7 +236,11 @@ EXTRA_DIST = \
contrib/games/winkels.nfg \
contrib/games/yamamoto.nfg \
contrib/games/zero.nfg \
src/README.rst
src/README.rst \
catalog/2smp.efg \
catalog/2x2x2.nfg \
catalog/myerson/fig_4_2.efg \
catalog/pd.nfg

core_SOURCES = \
src/core/core.h \
Expand Down
74 changes: 74 additions & 0 deletions build_support/catalog/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import argparse
from pathlib import Path

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."""

# 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 slug in slugs:
game_files.append(f"catalog/{slug}")
game_files.sort()

with open(MAKEFILE_AM, encoding="utf-8") as f:
content = f.readlines()

with open(MAKEFILE_AM, "w", encoding="utf-8") as f:
in_gamefiles_section = False
for line in content:
# Add to the EXTRA_DIST after the README.rst line
if line.startswith(" src/README.rst \\"):
in_gamefiles_section = True
f.write(" src/README.rst \\\n")
for gf in game_files:
if gf == game_files[-1]:
f.write(f"\t{gf}\n")
else:
f.write(f"\t{gf} \\\n")
f.write("\n")
elif in_gamefiles_section:
if line.strip() == "":
in_gamefiles_section = False
continue # Skip old gamefiles lines
else:
f.write(line)

with open(MAKEFILE_AM, encoding="utf-8") as f:
updated_content = f.readlines()

if content != updated_content:
print(f"Updated {str(MAKEFILE_AM)}")
else:
print(f"No changes to add to {str(MAKEFILE_AM)}")


if __name__ == "__main__":

parser = argparse.ArgumentParser()
parser.add_argument("--build", action="store_true")
args = parser.parse_args()

# Create CSV used by RST docs page
gbt.catalog.games().to_csv(CATALOG_CSV, index=False)
print(f"Generated {CATALOG_CSV} for use in local docs build. DO NOT COMMIT.")

# Update the Makefile.am with the current list of catalog files
if args.build:
update_makefile()
File renamed without changes.
File renamed without changes.
6 changes: 6 additions & 0 deletions catalog/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .utils import games, load

__all__ = [
"load",
"games",
]
54 changes: 54 additions & 0 deletions catalog/families.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@

import pygambit as gbt


def family_games() -> dict[str, gbt.Game]:
"""
Generate a dict of games for inclusion in the catalog,
using the game families in this module.
"""
return {
"one_shot_trust": one_shot_trust(),
"oneshot_trust_unique_NE": one_shot_trust(unique_NE_variant=True),
}


################################################################################################
# Families

def one_shot_trust(unique_NE_variant: bool = False) -> gbt.Game:
"""
The unique_NE_variant makes Trust a dominant strategy, replacing the
non-singleton equilibrium component from the standard version of the game
where the Buyer plays "Not Trust" and the seller can play any mixture with
< 0.5 probability on Honor with a unique NE where the Buyer plays Trust and
the Seller plays Abuse.

Parameters
----------
unique_NE_variant : bool, optional
Whether to modify the game so that it has a unique Nash equilibrium.
Defaults to False.

Returns
-------
gbt.Game
The constructed extensive-form game.
"""
g = gbt.Game.new_tree(players=["Buyer", "Seller"])
g.description = "One-shot trust game with binary actions, originally from Kreps (1990)."
g.append_move(g.root, "Buyer", ["Trust", "Not trust"])
g.append_move(g.root.children[0], "Seller", ["Honor", "Abuse"])
g.set_outcome(g.root.children[0].children[0], g.add_outcome([1, 1], label="Trustworthy"))
if unique_NE_variant:
g.title = "One-shot trust game with unique NE"
g.set_outcome(
g.root.children[0].children[1], g.add_outcome(["1/2", 2], label="Untrustworthy")
)
else:
g.title = "One-shot trust game"
g.set_outcome(
g.root.children[0].children[1], g.add_outcome([-1, 2], label="Untrustworthy")
)
g.set_outcome(g.root.children[1], g.add_outcome([0, 0], label="Opt-out"))
return g
File renamed without changes.
File renamed without changes.
75 changes: 75 additions & 0 deletions catalog/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from importlib.resources import as_file, files
from pathlib import Path

import pandas as pd

import pygambit as gbt

from .families import family_games

# Use the full string path to the virtual package we created
_CATALOG_RESOURCE = files(__name__)

READERS = {
".nfg": gbt.read_nfg,
".efg": gbt.read_efg,
}


def load(slug: str) -> gbt.Game:
"""
Load a game from the package catalog.
"""
slug = str(Path(slug)).replace("\\", "/")

# Try to load from file
for suffix, reader in READERS.items():
resource_path = _CATALOG_RESOURCE / f"{slug}{suffix}"
if resource_path.is_file():
with as_file(resource_path) as path:
return reader(str(path))

# Try loading from family games
fg = family_games()
if slug in fg:
return fg[slug]

# Raise error if game does not exist
raise FileNotFoundError(f"No catalog entry called {slug}")


def games() -> pd.DataFrame:
"""
List games available in the package catalog, including subdirectories.
"""
records: list[dict[str, str]] = []

# Add all the games stored as EFG/NFG files
for resource_path in sorted(_CATALOG_RESOURCE.rglob("*")):
reader = READERS.get(resource_path.suffix)

if reader is not None and resource_path.is_file():
# Calculate the path relative to the root resource
# and remove the suffix to get the "slug"
rel_path = resource_path.relative_to(_CATALOG_RESOURCE)
slug = rel_path.with_suffix("").as_posix()

with as_file(resource_path) as path:
game = reader(str(path))
records.append(
{
"Game": slug,
"Title": game.title,
}
)

# Add all the games from families
for slug, game in family_games().items():
records.append(
{
"Game": slug,
"Title": game.title,
}
)

return pd.DataFrame.from_records(records, columns=["Game", "Title"])
8 changes: 8 additions & 0 deletions doc/catalog.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Catalog of games
================

.. csv-table::
:file: catalog.csv
:header-rows: 1
:widths: 20, 80
:class: tight-table
60 changes: 60 additions & 0 deletions doc/developer.catalog.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
Updating the Games Catalog
==========================

This page covers the process for contributing to and updating Gambit's :ref:`Games Catalog <pygambit-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 <contributing>`.

You can add games to the catalog saved in a valid representation :ref:`format <file-formats>`.
Currently supported representations are:

- `.efg` for extensive form games
- `.nfg` for normal form games

Add new game files
------------------

1. **Create the game file:**

Use either :ref:`pygambit <pygambit>`, the Gambit :ref:`CLI <command-line>` or :ref:`GUI <section-gui>` to create and save game in a valid representation :ref:`format <file-formats>`.

2. **Add the game file:**

Create a new branch in the ``gambit`` repo. Add your new game file(s) inside the ``catalog`` dir and commit them.

3. **Update the catalog:**

Use the ``update.py`` script to update Gambit's documentation & build files.

.. code-block:: bash

python build_support/catalog/update.py --build

.. note::

Run this script in a Python environment where ``pygambit`` itself is also :ref:`installed <build-python>`.

.. warning::

Running the script with the ``--build`` flag updates `Makefile.am`. If you moved games that were previously in `contrib/games` you'll need to also manually remove those files from `EXTRA_DIST`.

4. **Submit a pull request to GitHub with all changes.**

.. warning::

Make sure you commit all changed files e.g. run ``git add --all`` before committing and pushing.

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.**
2 changes: 2 additions & 0 deletions doc/developer.contributing.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. _contributing:

Contributing to Gambit
======================

Expand Down
1 change: 1 addition & 0 deletions doc/developer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ This section contains information for developers who want to contribute to the G

developer.build
developer.contributing
developer.catalog
1 change: 1 addition & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading