diff --git a/.gitignore b/.gitignore index 643f8c565..f46c8bff9 100644 --- a/.gitignore +++ b/.gitignore @@ -41,14 +41,15 @@ dist *.dmg Gambit.app/* *.so -doc/tutorials/games/*.nfg -doc/tutorials/games/*.efg -doc/tutorials/*.png *.dmg Gambit.app/* *.ipynb_checkpoints -*.ef +doc/**/*.ef build_support/msw/gambit.wxs build_support/osx/Info.plist src/pygambit/catalog doc/catalog_table.rst +catalog/img/**/*.pdf +catalog/img/**/*.png +catalog/img/**/*.tex +catalog/img/**/*.ef diff --git a/.readthedocs.yml b/.readthedocs.yml index dc6c7c782..fcf30f606 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -13,10 +13,11 @@ build: - libgmp-dev - pandoc - texlive-full + - imagemagick jobs: - # Create CSV for catalog table in docs + # Create RST for catalog table in docs post_install: - - $READTHEDOCS_VIRTUALENV_PATH/bin/python build_support/catalog/update.py + - $READTHEDOCS_VIRTUALENV_PATH/bin/python build_support/catalog/update.py --regenerate-images python: install: diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index 83464ab5f..c0c05e95d 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -1,7 +1,9 @@ import argparse +import re from pathlib import Path import pandas as pd +from draw_tree import generate_pdf, generate_png, generate_tex import pygambit as gbt @@ -10,7 +12,19 @@ MAKEFILE_AM = Path(__file__).parent.parent.parent / "Makefile.am" -def _write_efg_table(df: pd.DataFrame, f): +def catalog_draw_tree_settings(slug: str) -> dict: + """Return the draw_tree settings for a given catalog slug.""" + settings = {"color_scheme": "gambit", "shared_terminal_depth": True, "sublevel_scaling": 0} + if slug == "bagwell1995" or "watson2013" in slug: + settings["sublevel_scaling"] = 1 + elif slug == "myerson1991/fig2_1" or slug == "reiley2008/fig1": + settings["action_label_position"] = 0.4 + elif "selten1975" in slug: + settings["shared_terminal_depth"] = False + return settings + + +def _write_efg_table(df: pd.DataFrame, f, tikz_re, regenerate_images: bool): """Write the EFG games list-table to file handle f.""" f.write(".. list-table::\n") f.write(" :header-rows: 1\n") @@ -24,12 +38,29 @@ def _write_efg_table(df: pd.DataFrame, f): slug = row["Game"] title = str(row.get("Title", "")).strip() description = str(row.get("Description", "")).strip() - # Skip any games which lack a description if description: + tex_path = CATALOG_DIR / "img" / f"{slug}.tex" + if regenerate_images or not tex_path.exists(): + g = gbt.catalog.load(slug) + viz_path = CATALOG_DIR / "img" / f"{slug}" + viz_path.parent.mkdir(parents=True, exist_ok=True) + for func in [generate_tex, generate_png, generate_pdf]: + func(g, save_to=str(viz_path), **catalog_draw_tree_settings(slug)) + + with open(tex_path, encoding="utf-8") as tex_f: + tex_content = tex_f.read() + match = tikz_re.search(tex_content) + tikz = ( + match.group(1).strip() + if match + else "% Could not extract tikzpicture from tex file" + ) + # Main dropdown f.write(f" * - .. dropdown:: {title}\n") f.write(" :open:\n") f.write(" \n") + for line in description.splitlines(): f.write(f" {line}\n") f.write(" \n") @@ -42,11 +73,21 @@ def _write_efg_table(df: pd.DataFrame, f): # Download links (inside the dropdown) download_links = [row["Download"]] - f.write(" **Download:**\n") + for ext in ["ef", "tex", "png", "pdf"]: + download_links.append(f":download:`{slug}.{ext} <../catalog/img/{slug}.{ext}>`") + f.write(" **Download game and image files:**\n") f.write(" \n") f.write(f" {' '.join(download_links)}\n") f.write(" \n") + # TiKZ image (outside dropdown) + f.write(" .. tikz::\n") + f.write(" :align: center\n") + f.write(" \n") + for line in tikz.splitlines(): + f.write(f" {line}\n") + f.write(" \n") + # def _write_nfg_table(df: pd.DataFrame, f): # """Write the NFG games list-table to file handle f.""" @@ -77,8 +118,9 @@ def _write_efg_table(df: pd.DataFrame, f): # f.write(" \n") -def generate_rst_table(df: pd.DataFrame, rst_path: Path): +def generate_rst_table(df: pd.DataFrame, rst_path: Path, regenerate_images: bool = False): """Generate RST output with two list-tables: one for EFG and one for NFG games.""" + tikz_re = re.compile(r"\\begin\{document\}(.*?)\\end\{document\}", re.DOTALL) with open(rst_path, "w", encoding="utf-8") as f: # TOC linking to both sections @@ -87,11 +129,11 @@ def generate_rst_table(df: pd.DataFrame, rst_path: Path): # f.write(" :depth: 1\n") # f.write("\n") - # EFG section + # # EFG section # f.write("Extensive form games\n") # f.write("--------------------\n") # f.write("\n") - _write_efg_table(df, f) + _write_efg_table(df, f, tikz_re, regenerate_images) # f.write("\n") # # NFG section @@ -155,11 +197,12 @@ def update_makefile(): if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--build", action="store_true") + parser.add_argument("--regenerate-images", action="store_true") args = parser.parse_args() # Create RST list-table used by doc/catalog.rst df = gbt.catalog.games(include_descriptions=True) - generate_rst_table(df, CATALOG_RST_TABLE) + generate_rst_table(df, CATALOG_RST_TABLE, regenerate_images=args.regenerate_images) print(f"Generated {CATALOG_RST_TABLE} for use in local docs build. DO NOT COMMIT.") if args.build: # Update the Makefile.am with the current list of catalog files diff --git a/catalog/img/.gitkeep b/catalog/img/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/doc/_static/custom.css b/doc/_static/custom.css new file mode 100644 index 000000000..9a9155293 --- /dev/null +++ b/doc/_static/custom.css @@ -0,0 +1,22 @@ +/* Custom CSS for Gambit Documentation */ +.tight-table .figure img { + max-height: 500px; + width: auto; + display: block; + margin-left: auto; + margin-right: auto; +} + +table.tight-table th, +table.tight-table td { + text-align: center; +} + +table.tight-table td .highlight pre, +table.tight-table td .highlight { + text-align: left; +} + +table.tight-table td .sd-card-body { + text-align: left; +} diff --git a/doc/conf.py b/doc/conf.py index 65760193b..668f99ccc 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -29,6 +29,8 @@ "IPython.sphinxext.ipython_directive", "sphinx_design", "nbsphinx", + "sphinxcontrib.tikz", + "jupyter_sphinx", ] # IPython directive configuration @@ -116,22 +118,10 @@ # documentation. html_theme_options = { "external_links": [ - { - "name": "GitHub", - "url": "https://github.com/gambitproject/gambit" - }, - { - "name": "Releases", - "url": "https://github.com/gambitproject/gambit/releases" - }, - { - "name": "Older releases", - "url": "https://sourceforge.net/projects/gambit/files/" - }, - { - "name": "Cite Gambit", - "url": "https://www.gambit-project.org/cite/" - } + {"name": "GitHub", "url": "https://github.com/gambitproject/gambit"}, + {"name": "Releases", "url": "https://github.com/gambitproject/gambit/releases"}, + {"name": "Older releases", "url": "https://sourceforge.net/projects/gambit/files/"}, + {"name": "Cite Gambit", "url": "https://www.gambit-project.org/cite/"}, ], "navbar_end": ["theme-switcher", "navbar-icon-links"], "icon_links": [ @@ -168,6 +158,11 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] +# Custom CSS files +html_css_files = [ + "custom.css", +] + # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' @@ -217,8 +212,7 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ("index", "Gambit.tex", "Gambit Documentation", - "The Gambit Project", "manual"), + ("index", "Gambit.tex", "Gambit Documentation", "The Gambit Project", "manual"), ] # The name of an image file (relative to this directory) to place at the top of diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index c68ee2527..1008c5d79 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -30,13 +30,18 @@ Currently supported representations are: 3. **Update the catalog:** Reinstall the package to pick up the new game file(s) in the ``pygambit.catalog`` module. - Then use the ``update.py`` script to update Gambit's documentation & build files. + Then use the ``update.py`` script to update Gambit's documentation & build files, as well as generating images for the new game(s). .. code-block:: bash pip install . python build_support/catalog/update.py --build + .. note:: + + Update the ``catalog_draw_tree_settings`` in ``build_support/catalog/update.py`` to change the default visualization parameters for your game(s). + You can use the ``--regenerate-images`` flag when building the docs locally (readthedocs does this by default). + .. 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`. diff --git a/pyproject.toml b/pyproject.toml index 553e949fd..f2b94ef73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,9 @@ doc = [ "pickleshare", "jupyter", "open_spiel; sys_platform != 'win32'", - "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.4.0" + "draw-tree @ git+https://github.com/gambitproject/draw_tree.git@v0.4.1", + "sphinxcontrib-tikz", + "jupyter_sphinx", ] [project.urls]