diff --git a/.gitignore b/.gitignore index f8be35fcf..643f8c565 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,4 @@ Gambit.app/* build_support/msw/gambit.wxs build_support/osx/Info.plist src/pygambit/catalog -doc/catalog.csv +doc/catalog_table.rst diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index e42a18ad9..fd012ee65 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -1,13 +1,49 @@ import argparse from pathlib import Path +import pandas as pd + import pygambit as gbt -CATALOG_CSV = Path(__file__).parent.parent.parent / "doc" / "catalog.csv" +CATALOG_RST_TABLE = Path(__file__).parent.parent.parent / "doc" / "catalog_table.rst" CATALOG_DIR = Path(__file__).parent.parent.parent / "catalog" MAKEFILE_AM = Path(__file__).parent.parent.parent / "Makefile.am" +def generate_rst_table(df: pd.DataFrame, rst_path: Path): + """Generate a list-table RST file with dropdowns for long descriptions.""" + with open(rst_path, "w", encoding="utf-8") as f: + f.write(".. list-table::\n") + f.write(" :header-rows: 1\n") + f.write(" :widths: 20 80 20\n") + f.write(" :class: tight-table\n") + f.write("\n") + + f.write(" * - **Game**\n") + f.write(" - **Description**\n") + f.write(" - **Download**\n") + + for _, row in df.iterrows(): + f.write(f" * - {row['Game']}\n") + + description_cell_lines = [] + title = str(row.get("Title", "")).strip() + description = str(row.get("Description", "")).strip() + if description: + description_cell_lines.append(f".. dropdown:: {title}") + description_cell_lines.append(" ") # Indented blank line + for line in description.splitlines(): + description_cell_lines.append(f" {line}") + else: + description_cell_lines.append(title) + + f.write(f" - {description_cell_lines[0]}\n") + for line in description_cell_lines[1:]: + f.write(f" {line}\n") + + f.write(f" - {row['Download']}\n") + + def update_makefile(): """Update the Makefile.am with all games from the catalog.""" @@ -65,9 +101,10 @@ def update_makefile(): parser.add_argument("--build", action="store_true") args = parser.parse_args() - # Create CSV used by RST docs page - gbt.catalog.games().to_csv(CATALOG_CSV, index=False) - print(f"Generated {CATALOG_CSV} for use in local docs build. DO NOT COMMIT.") + # Create RST list-table used by doc/catalog.rst + df = gbt.catalog.games(include_descriptions=True) + generate_rst_table(df, CATALOG_RST_TABLE) + print(f"Generated {CATALOG_RST_TABLE} for use in local docs build. DO NOT COMMIT.") # Update the Makefile.am with the current list of catalog files if args.build: diff --git a/catalog/__init__.py b/catalog/__init__.py index e83ea540b..d4ca2995f 100644 --- a/catalog/__init__.py +++ b/catalog/__init__.py @@ -65,11 +65,12 @@ def games( n_outcomes: int | None = None, n_players: int | None = None, n_strategies: int | None = None, + include_descriptions: bool = False, ) -> pd.DataFrame: """ List games available in the package catalog. - Arguments are treated as filters on the + Most arguments are treated as filters on the attributes of the Game objects. Parameters @@ -98,11 +99,15 @@ def games( The number of players in the game. n_strategies: int, optional The number of pure strategies in the game. + include_descriptions: bool, optional + Whether to include the description of each game in the returned DataFrame. + Defaults to False. Returns ------- pd.DataFrame A DataFrame with columns "Game" and "Title", where "Game" is the slug to load the game. + If `include_descriptions=True`, the DataFrame will also include a "Description" column. """ records: list[dict[str, Any]] = [] @@ -140,6 +145,20 @@ def check_filters(game: gbt.Game) -> bool: return False return not (n_strategies is not None and len(game.strategies) != n_strategies) + def append_record( + slug: str, + game: gbt.Game, + ) -> None: + record = { + "Game": slug, + "Title": game.title, + } + if include_descriptions: + record["Description"] = game.description + ext = "efg" if game.is_tree else "nfg" + record["Download"] = f":download:`{slug}.{ext} <../catalog/{slug}.{ext}>`" + records.append(record) + # Add all the games stored as EFG/NFG files for resource_path in sorted(_CATALOG_RESOURCE.rglob("*")): reader = READERS.get(resource_path.suffix) @@ -153,12 +172,7 @@ def check_filters(game: gbt.Game) -> bool: with as_file(resource_path) as path: game = reader(str(path)) if check_filters(game): - records.append( - { - "Game": slug, - "Title": game.title, - } - ) + append_record(slug, game) # Add all the games from families for slug, game in family_games().items(): @@ -168,13 +182,12 @@ def check_filters(game: gbt.Game) -> bool: f"Slug collision: {slug} is present in both file-based and family games." ) if check_filters(game): - records.append( - { - "Game": slug, - "Title": game.title, - } - ) + append_record(slug, game) + if include_descriptions: + return pd.DataFrame.from_records( + records, columns=["Game", "Title", "Description", "Download"] + ) return pd.DataFrame.from_records(records, columns=["Game", "Title"]) @@ -218,7 +231,7 @@ def one_shot_trust(unique_NE_variant: bool = False) -> gbt.Game: The constructed extensive-form game. """ g = gbt.Game.new_tree(players=["Buyer", "Seller"]) - g.description = "One-shot trust game with binary actions, originally from Kreps (1990)." + g.description = "One-shot trust game with binary actions, originally from [Kre90]_." g.append_move(g.root, "Buyer", ["Trust", "Not trust"]) g.append_move(g.root.children[0], "Seller", ["Honor", "Abuse"]) g.set_outcome(g.root.children[0].children[0], g.add_outcome([1, 1], label="Trustworthy")) diff --git a/catalog/myerson1991/fig4_2.efg b/catalog/myerson1991/fig4_2.efg index 9631090f0..c36cf1e13 100644 --- a/catalog/myerson1991/fig4_2.efg +++ b/catalog/myerson1991/fig4_2.efg @@ -1,5 +1,5 @@ EFG 2 R "Myerson (1991) Figure 4.2" { "Player 1" "Player 2" } -"An example from Myerson [^Mye1991] which illustrates the distinction between +"An example from Myerson [Mye91]_ which illustrates the distinction between an equilibrium of an extensive form game and an equilibrium of its (multi)agent representation. The actions B1, Z1, and W2 form a behavior profile which is an equilibrium in the (multi)agent @@ -8,7 +8,7 @@ game, because Player 1 would prefer to switch from (B1, Z1) to (A1, Y1); the (multi)agent representation rules out such coordinated deviations across information sets. -[^Mye1991]: Myerson, Roger B. (1991) Game Theory: Analysis of Conflict. +[Mye91]_: Myerson, Roger B. (1991) Game Theory: Analysis of Conflict. Cambridge: Harvard University Press. " diff --git a/catalog/selten1975/fig1.efg b/catalog/selten1975/fig1.efg index c966809b6..ea47ae88f 100644 --- a/catalog/selten1975/fig1.efg +++ b/catalog/selten1975/fig1.efg @@ -1,12 +1,12 @@ EFG 2 R "Selten's horse (Selten IJGT 1975, Figure 1)" { "Player 1" "Player 2" "Player 3" } -"This is a three-player game presented in Selten [^Sel1975], commonly referred +"This is a three-player game presented in Selten [Sel75]_, commonly referred to as \"Selten's horse\" owing to the layout in which it can be drawn. It is the motivating example for his definition of (trembling-hand) perfect equilibrium, by showing a game that has an equilibrium which is \"unreasonable\", but which is not ruled out by subgame perfection because this game has no proper subgames. -[^Sel1975]: Selten, Reinhard (1975). A reexamination of the perfectness concept +[Sel75]_: Selten, Reinhard (1975). A reexamination of the perfectness concept for equilibrium points in extensive games. International Journal of Game Theory 4(1): 25-55. " diff --git a/catalog/selten1975/fig2.efg b/catalog/selten1975/fig2.efg index 1d36cb21f..ab52adaa5 100644 --- a/catalog/selten1975/fig2.efg +++ b/catalog/selten1975/fig2.efg @@ -1,10 +1,10 @@ EFG 2 R "Selten (IJGT 1975) Figure 2" { "Player 1" "Player 2" } -"This is a counterexample presented in [^Sel1975], to show that extensive and +"This is a counterexample presented in [Sel75]_, to show that extensive and normal form concepts of perfectness do not coincide. This game has one perfect equilibrium in the extensive from, but a distinct (pure) strategy equilibrium is also perfect in the normal form. -[^Sel75]: Selten, Reinhard (1975). A reexamination of the perfectness concept +[Sel75]_: Selten, Reinhard (1975). A reexamination of the perfectness concept for equilibrium points in extensive games. International Journal of Game Theory 4(1): 25-55. " diff --git a/catalog/selten1975/fig3.efg b/catalog/selten1975/fig3.efg index 7df596906..25c123b05 100644 --- a/catalog/selten1975/fig3.efg +++ b/catalog/selten1975/fig3.efg @@ -1,10 +1,10 @@ EFG 2 R "Selten (IJGT 1975) Figure 3" { "Player 1" "Player 2" "Player 3" } -"This is a counterexample presented in [^Sel1975], to show that extensive and +"This is a counterexample presented in [Sel75]_, to show that extensive and normal form concepts of perfectness do not coincide. Specifically, there are two equilibria which are perfect in the normal form but not perfect in the extensive form. -[^Sel75]: Selten, Reinhard (1975). A reexamination of the perfectness concept +[Sel75]_: Selten, Reinhard (1975). A reexamination of the perfectness concept for equilibrium points in extensive games. International Journal of Game Theory 4(1): 25-55. " diff --git a/doc/biblio.rst b/doc/biblio.rst index b84d341a8..8c12265be 100644 --- a/doc/biblio.rst +++ b/doc/biblio.rst @@ -1,6 +1,11 @@ Bibliography ============ +.. note:: + + To reference an entry in this bibliography, use the format ``[key]_``, for example, ``[Mye91]_`` will link to the Myerson (1991) textbook entry. + + Articles on computation of Nash equilibria ------------------------------------------ @@ -89,6 +94,9 @@ General game theory articles and texts .. [KreWil82] David Kreps and Robert Wilson, "Sequential Equilibria", 863-894, Econometrica , 50, 1982. +.. [Kre90] David Kreps, 1990, A Course in Microeconomic Theory, + Princeton University Press. + .. [McKPal95] Richard McKelvey and Tom Palfrey, "Quantal response equilibria for normal form games", 6-38, Games and Economic Behavior , 10, 1995. diff --git a/doc/catalog.rst b/doc/catalog.rst index d50367996..a7d132c4a 100644 --- a/doc/catalog.rst +++ b/doc/catalog.rst @@ -4,8 +4,4 @@ Catalog of games Below is a complete list of games included in Gambit's catalog. Check out the :ref:`pygambit API reference ` for instructions on how to search and load these games in Python, and the :ref:`Updating the games catalog ` guide for instructions on how to contribute new games to the catalog. -.. csv-table:: - :file: catalog.csv - :header-rows: 1 - :widths: 20, 80 - :class: tight-table +.. include:: catalog_table.rst diff --git a/doc/index.rst b/doc/index.rst index 0e1725265..017bdaf88 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -87,7 +87,6 @@ We recommended most new users install the PyGambit Python package and read the a tools gui catalog - samples developer formats biblio diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 6cc53d16a..a9188f1f8 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -164,3 +164,10 @@ def test_catalog_games_filter_bad_filter(): """Test games() function raises error on invalid filter key""" with pytest.raises(TypeError): gbt.catalog.games(invalid_filter=123) + + +def test_catalog_games_include_descriptions(): + """Test games() function can include descriptions""" + games_with_desc = gbt.catalog.games(include_descriptions=True) + assert "Description" in games_with_desc.columns + assert "Download" in games_with_desc.columns