From 6f122fdf0e1205191bf1a7a3a6767526e017d57d Mon Sep 17 00:00:00 2001 From: rahulsavani Date: Tue, 2 Jun 2026 19:18:51 +0100 Subject: [PATCH 1/6] two levels of hierarchy working --- build_support/catalog/catalog.am | 24 +++++++++---------- .../{ => books}/vonstengel2022/fig10.1.efg | 0 .../{ => books}/vonstengel2022/fig10.12.efg | 0 .../{ => books}/vonstengel2022/fig10.5.efg | 0 .../{ => books}/vonstengel2022/fig10.7.efg | 0 .../{ => books}/watson2013/exercise29_6.efg | 0 catalog/{ => books}/watson2013/fig29_1.efg | 0 .../{ => mor}/vonstengelforges2008/fig1.efg | 0 .../{ => mor}/vonstengelforges2008/fig6.efg | 0 .../fig6__Original_Layout.ef | 0 .../{ => mor}/vonstengelforges2008/fig9.efg | 0 .../fig9__Original_Layout.ef | 0 12 files changed, 12 insertions(+), 12 deletions(-) rename catalog/{ => books}/vonstengel2022/fig10.1.efg (100%) rename catalog/{ => books}/vonstengel2022/fig10.12.efg (100%) rename catalog/{ => books}/vonstengel2022/fig10.5.efg (100%) rename catalog/{ => books}/vonstengel2022/fig10.7.efg (100%) rename catalog/{ => books}/watson2013/exercise29_6.efg (100%) rename catalog/{ => books}/watson2013/fig29_1.efg (100%) rename catalog/{ => mor}/vonstengelforges2008/fig1.efg (100%) rename catalog/{ => mor}/vonstengelforges2008/fig6.efg (100%) rename catalog/{ => mor}/vonstengelforges2008/fig6__Original_Layout.ef (100%) rename catalog/{ => mor}/vonstengelforges2008/fig9.efg (100%) rename catalog/{ => mor}/vonstengelforges2008/fig9__Original_Layout.ef (100%) diff --git a/build_support/catalog/catalog.am b/build_support/catalog/catalog.am index 2a0a8a4e5..186d96e5c 100644 --- a/build_support/catalog/catalog.am +++ b/build_support/catalog/catalog.am @@ -1,11 +1,22 @@ CATALOG_FILES = \ catalog/bagwell1995.efg \ + catalog/books/vonstengel2022/fig10.1.efg \ + catalog/books/vonstengel2022/fig10.12.efg \ + catalog/books/vonstengel2022/fig10.5.efg \ + catalog/books/vonstengel2022/fig10.7.efg \ + catalog/books/watson2013/exercise29_6.efg \ + catalog/books/watson2013/fig29_1.efg \ catalog/gilboa1997/fig1.efg \ catalog/gilboa1997/fig2.efg \ catalog/jakobsen2016/fig1a.efg \ catalog/jakobsen2016/fig1b.efg \ catalog/jakobsen2016/fig1c.efg \ catalog/jakobsen2016/fig3.efg \ + catalog/mor/vonstengelforges2008/fig1.efg \ + catalog/mor/vonstengelforges2008/fig6.efg \ + catalog/mor/vonstengelforges2008/fig6__Original_Layout.ef \ + catalog/mor/vonstengelforges2008/fig9.efg \ + catalog/mor/vonstengelforges2008/fig9__Original_Layout.ef \ catalog/myerson1991/fig2_1.efg \ catalog/myerson1991/fig4_2.efg \ catalog/nau2004/sec3.nfg \ @@ -17,15 +28,4 @@ CATALOG_FILES = \ catalog/selten1975/fig2.efg \ catalog/selten1975/fig3.efg \ catalog/shapley1974/fig2.nfg \ - catalog/shapley1974/fig3.nfg \ - catalog/vonstengel2022/fig10.1.efg \ - catalog/vonstengel2022/fig10.12.efg \ - catalog/vonstengel2022/fig10.5.efg \ - catalog/vonstengel2022/fig10.7.efg \ - catalog/vonstengelforges2008/fig1.efg \ - catalog/vonstengelforges2008/fig6.efg \ - catalog/vonstengelforges2008/fig6__Original_Layout.ef \ - catalog/vonstengelforges2008/fig9.efg \ - catalog/vonstengelforges2008/fig9__Original_Layout.ef \ - catalog/watson2013/exercise29_6.efg \ - catalog/watson2013/fig29_1.efg + catalog/shapley1974/fig3.nfg diff --git a/catalog/vonstengel2022/fig10.1.efg b/catalog/books/vonstengel2022/fig10.1.efg similarity index 100% rename from catalog/vonstengel2022/fig10.1.efg rename to catalog/books/vonstengel2022/fig10.1.efg diff --git a/catalog/vonstengel2022/fig10.12.efg b/catalog/books/vonstengel2022/fig10.12.efg similarity index 100% rename from catalog/vonstengel2022/fig10.12.efg rename to catalog/books/vonstengel2022/fig10.12.efg diff --git a/catalog/vonstengel2022/fig10.5.efg b/catalog/books/vonstengel2022/fig10.5.efg similarity index 100% rename from catalog/vonstengel2022/fig10.5.efg rename to catalog/books/vonstengel2022/fig10.5.efg diff --git a/catalog/vonstengel2022/fig10.7.efg b/catalog/books/vonstengel2022/fig10.7.efg similarity index 100% rename from catalog/vonstengel2022/fig10.7.efg rename to catalog/books/vonstengel2022/fig10.7.efg diff --git a/catalog/watson2013/exercise29_6.efg b/catalog/books/watson2013/exercise29_6.efg similarity index 100% rename from catalog/watson2013/exercise29_6.efg rename to catalog/books/watson2013/exercise29_6.efg diff --git a/catalog/watson2013/fig29_1.efg b/catalog/books/watson2013/fig29_1.efg similarity index 100% rename from catalog/watson2013/fig29_1.efg rename to catalog/books/watson2013/fig29_1.efg diff --git a/catalog/vonstengelforges2008/fig1.efg b/catalog/mor/vonstengelforges2008/fig1.efg similarity index 100% rename from catalog/vonstengelforges2008/fig1.efg rename to catalog/mor/vonstengelforges2008/fig1.efg diff --git a/catalog/vonstengelforges2008/fig6.efg b/catalog/mor/vonstengelforges2008/fig6.efg similarity index 100% rename from catalog/vonstengelforges2008/fig6.efg rename to catalog/mor/vonstengelforges2008/fig6.efg diff --git a/catalog/vonstengelforges2008/fig6__Original_Layout.ef b/catalog/mor/vonstengelforges2008/fig6__Original_Layout.ef similarity index 100% rename from catalog/vonstengelforges2008/fig6__Original_Layout.ef rename to catalog/mor/vonstengelforges2008/fig6__Original_Layout.ef diff --git a/catalog/vonstengelforges2008/fig9.efg b/catalog/mor/vonstengelforges2008/fig9.efg similarity index 100% rename from catalog/vonstengelforges2008/fig9.efg rename to catalog/mor/vonstengelforges2008/fig9.efg diff --git a/catalog/vonstengelforges2008/fig9__Original_Layout.ef b/catalog/mor/vonstengelforges2008/fig9__Original_Layout.ef similarity index 100% rename from catalog/vonstengelforges2008/fig9__Original_Layout.ef rename to catalog/mor/vonstengelforges2008/fig9__Original_Layout.ef From 2c62c38ff70eb3232f1422c60042e4037d7c4588 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Wed, 3 Jun 2026 07:19:41 +0100 Subject: [PATCH 2/6] books/journals/misc/conf --- build_support/catalog/catalog.am | 48 +++++++++---------- catalog/{ => books}/myerson1991/fig2_1.efg | 0 catalog/{ => books}/myerson1991/fig4_2.efg | 0 .../{ => conf/itcs}/jakobsen2016/fig1a.efg | 0 .../{ => conf/itcs}/jakobsen2016/fig1b.efg | 0 .../{ => conf/itcs}/jakobsen2016/fig1c.efg | 0 catalog/{ => conf/itcs}/jakobsen2016/fig3.efg | 0 catalog/{ => journals/geb}/bagwell1995.efg | 0 .../{ => journals/geb}/gilboa1997/fig1.efg | 0 .../{ => journals/geb}/gilboa1997/fig2.efg | 0 catalog/{ => journals/ijgt}/nau2004/sec3.nfg | 0 catalog/{ => journals/ijgt}/nau2004/sec4.nfg | 0 catalog/{ => journals/ijgt}/nau2004/sec5.nfg | 0 catalog/{ => journals/ijgt}/nau2004/sec6.nfg | 0 .../{ => journals/ijgt}/selten1975/fig1.efg | 0 .../{ => journals/ijgt}/selten1975/fig2.efg | 0 .../{ => journals/ijgt}/selten1975/fig3.efg | 0 .../mor/vonstengelforges2008/fig1.efg | 0 .../mor/vonstengelforges2008/fig6.efg | 0 .../fig6__Original_Layout.ef | 0 .../mor/vonstengelforges2008/fig9.efg | 0 .../fig9__Original_Layout.ef | 0 catalog/{ => misc}/reiley2008/fig1.efg | 0 catalog/{ => misc}/shapley1974/fig2.nfg | 0 catalog/{ => misc}/shapley1974/fig3.nfg | 0 25 files changed, 24 insertions(+), 24 deletions(-) rename catalog/{ => books}/myerson1991/fig2_1.efg (100%) rename catalog/{ => books}/myerson1991/fig4_2.efg (100%) rename catalog/{ => conf/itcs}/jakobsen2016/fig1a.efg (100%) rename catalog/{ => conf/itcs}/jakobsen2016/fig1b.efg (100%) rename catalog/{ => conf/itcs}/jakobsen2016/fig1c.efg (100%) rename catalog/{ => conf/itcs}/jakobsen2016/fig3.efg (100%) rename catalog/{ => journals/geb}/bagwell1995.efg (100%) rename catalog/{ => journals/geb}/gilboa1997/fig1.efg (100%) rename catalog/{ => journals/geb}/gilboa1997/fig2.efg (100%) rename catalog/{ => journals/ijgt}/nau2004/sec3.nfg (100%) rename catalog/{ => journals/ijgt}/nau2004/sec4.nfg (100%) rename catalog/{ => journals/ijgt}/nau2004/sec5.nfg (100%) rename catalog/{ => journals/ijgt}/nau2004/sec6.nfg (100%) rename catalog/{ => journals/ijgt}/selten1975/fig1.efg (100%) rename catalog/{ => journals/ijgt}/selten1975/fig2.efg (100%) rename catalog/{ => journals/ijgt}/selten1975/fig3.efg (100%) rename catalog/{ => journals}/mor/vonstengelforges2008/fig1.efg (100%) rename catalog/{ => journals}/mor/vonstengelforges2008/fig6.efg (100%) rename catalog/{ => journals}/mor/vonstengelforges2008/fig6__Original_Layout.ef (100%) rename catalog/{ => journals}/mor/vonstengelforges2008/fig9.efg (100%) rename catalog/{ => journals}/mor/vonstengelforges2008/fig9__Original_Layout.ef (100%) rename catalog/{ => misc}/reiley2008/fig1.efg (100%) rename catalog/{ => misc}/shapley1974/fig2.nfg (100%) rename catalog/{ => misc}/shapley1974/fig3.nfg (100%) diff --git a/build_support/catalog/catalog.am b/build_support/catalog/catalog.am index 186d96e5c..31a8adc02 100644 --- a/build_support/catalog/catalog.am +++ b/build_support/catalog/catalog.am @@ -1,31 +1,31 @@ CATALOG_FILES = \ - catalog/bagwell1995.efg \ + catalog/books/myerson1991/fig2_1.efg \ + catalog/books/myerson1991/fig4_2.efg \ catalog/books/vonstengel2022/fig10.1.efg \ catalog/books/vonstengel2022/fig10.12.efg \ catalog/books/vonstengel2022/fig10.5.efg \ catalog/books/vonstengel2022/fig10.7.efg \ catalog/books/watson2013/exercise29_6.efg \ catalog/books/watson2013/fig29_1.efg \ - catalog/gilboa1997/fig1.efg \ - catalog/gilboa1997/fig2.efg \ - catalog/jakobsen2016/fig1a.efg \ - catalog/jakobsen2016/fig1b.efg \ - catalog/jakobsen2016/fig1c.efg \ - catalog/jakobsen2016/fig3.efg \ - catalog/mor/vonstengelforges2008/fig1.efg \ - catalog/mor/vonstengelforges2008/fig6.efg \ - catalog/mor/vonstengelforges2008/fig6__Original_Layout.ef \ - catalog/mor/vonstengelforges2008/fig9.efg \ - catalog/mor/vonstengelforges2008/fig9__Original_Layout.ef \ - catalog/myerson1991/fig2_1.efg \ - catalog/myerson1991/fig4_2.efg \ - catalog/nau2004/sec3.nfg \ - catalog/nau2004/sec4.nfg \ - catalog/nau2004/sec5.nfg \ - catalog/nau2004/sec6.nfg \ - catalog/reiley2008/fig1.efg \ - catalog/selten1975/fig1.efg \ - catalog/selten1975/fig2.efg \ - catalog/selten1975/fig3.efg \ - catalog/shapley1974/fig2.nfg \ - catalog/shapley1974/fig3.nfg + catalog/conf/itcs/jakobsen2016/fig1a.efg \ + catalog/conf/itcs/jakobsen2016/fig1b.efg \ + catalog/conf/itcs/jakobsen2016/fig1c.efg \ + catalog/conf/itcs/jakobsen2016/fig3.efg \ + catalog/journals/geb/bagwell1995.efg \ + catalog/journals/geb/gilboa1997/fig1.efg \ + catalog/journals/geb/gilboa1997/fig2.efg \ + catalog/journals/ijgt/nau2004/sec3.nfg \ + catalog/journals/ijgt/nau2004/sec4.nfg \ + catalog/journals/ijgt/nau2004/sec5.nfg \ + catalog/journals/ijgt/nau2004/sec6.nfg \ + catalog/journals/ijgt/selten1975/fig1.efg \ + catalog/journals/ijgt/selten1975/fig2.efg \ + catalog/journals/ijgt/selten1975/fig3.efg \ + catalog/journals/mor/vonstengelforges2008/fig1.efg \ + catalog/journals/mor/vonstengelforges2008/fig6.efg \ + catalog/journals/mor/vonstengelforges2008/fig6__Original_Layout.ef \ + catalog/journals/mor/vonstengelforges2008/fig9.efg \ + catalog/journals/mor/vonstengelforges2008/fig9__Original_Layout.ef \ + catalog/misc/reiley2008/fig1.efg \ + catalog/misc/shapley1974/fig2.nfg \ + catalog/misc/shapley1974/fig3.nfg diff --git a/catalog/myerson1991/fig2_1.efg b/catalog/books/myerson1991/fig2_1.efg similarity index 100% rename from catalog/myerson1991/fig2_1.efg rename to catalog/books/myerson1991/fig2_1.efg diff --git a/catalog/myerson1991/fig4_2.efg b/catalog/books/myerson1991/fig4_2.efg similarity index 100% rename from catalog/myerson1991/fig4_2.efg rename to catalog/books/myerson1991/fig4_2.efg diff --git a/catalog/jakobsen2016/fig1a.efg b/catalog/conf/itcs/jakobsen2016/fig1a.efg similarity index 100% rename from catalog/jakobsen2016/fig1a.efg rename to catalog/conf/itcs/jakobsen2016/fig1a.efg diff --git a/catalog/jakobsen2016/fig1b.efg b/catalog/conf/itcs/jakobsen2016/fig1b.efg similarity index 100% rename from catalog/jakobsen2016/fig1b.efg rename to catalog/conf/itcs/jakobsen2016/fig1b.efg diff --git a/catalog/jakobsen2016/fig1c.efg b/catalog/conf/itcs/jakobsen2016/fig1c.efg similarity index 100% rename from catalog/jakobsen2016/fig1c.efg rename to catalog/conf/itcs/jakobsen2016/fig1c.efg diff --git a/catalog/jakobsen2016/fig3.efg b/catalog/conf/itcs/jakobsen2016/fig3.efg similarity index 100% rename from catalog/jakobsen2016/fig3.efg rename to catalog/conf/itcs/jakobsen2016/fig3.efg diff --git a/catalog/bagwell1995.efg b/catalog/journals/geb/bagwell1995.efg similarity index 100% rename from catalog/bagwell1995.efg rename to catalog/journals/geb/bagwell1995.efg diff --git a/catalog/gilboa1997/fig1.efg b/catalog/journals/geb/gilboa1997/fig1.efg similarity index 100% rename from catalog/gilboa1997/fig1.efg rename to catalog/journals/geb/gilboa1997/fig1.efg diff --git a/catalog/gilboa1997/fig2.efg b/catalog/journals/geb/gilboa1997/fig2.efg similarity index 100% rename from catalog/gilboa1997/fig2.efg rename to catalog/journals/geb/gilboa1997/fig2.efg diff --git a/catalog/nau2004/sec3.nfg b/catalog/journals/ijgt/nau2004/sec3.nfg similarity index 100% rename from catalog/nau2004/sec3.nfg rename to catalog/journals/ijgt/nau2004/sec3.nfg diff --git a/catalog/nau2004/sec4.nfg b/catalog/journals/ijgt/nau2004/sec4.nfg similarity index 100% rename from catalog/nau2004/sec4.nfg rename to catalog/journals/ijgt/nau2004/sec4.nfg diff --git a/catalog/nau2004/sec5.nfg b/catalog/journals/ijgt/nau2004/sec5.nfg similarity index 100% rename from catalog/nau2004/sec5.nfg rename to catalog/journals/ijgt/nau2004/sec5.nfg diff --git a/catalog/nau2004/sec6.nfg b/catalog/journals/ijgt/nau2004/sec6.nfg similarity index 100% rename from catalog/nau2004/sec6.nfg rename to catalog/journals/ijgt/nau2004/sec6.nfg diff --git a/catalog/selten1975/fig1.efg b/catalog/journals/ijgt/selten1975/fig1.efg similarity index 100% rename from catalog/selten1975/fig1.efg rename to catalog/journals/ijgt/selten1975/fig1.efg diff --git a/catalog/selten1975/fig2.efg b/catalog/journals/ijgt/selten1975/fig2.efg similarity index 100% rename from catalog/selten1975/fig2.efg rename to catalog/journals/ijgt/selten1975/fig2.efg diff --git a/catalog/selten1975/fig3.efg b/catalog/journals/ijgt/selten1975/fig3.efg similarity index 100% rename from catalog/selten1975/fig3.efg rename to catalog/journals/ijgt/selten1975/fig3.efg diff --git a/catalog/mor/vonstengelforges2008/fig1.efg b/catalog/journals/mor/vonstengelforges2008/fig1.efg similarity index 100% rename from catalog/mor/vonstengelforges2008/fig1.efg rename to catalog/journals/mor/vonstengelforges2008/fig1.efg diff --git a/catalog/mor/vonstengelforges2008/fig6.efg b/catalog/journals/mor/vonstengelforges2008/fig6.efg similarity index 100% rename from catalog/mor/vonstengelforges2008/fig6.efg rename to catalog/journals/mor/vonstengelforges2008/fig6.efg diff --git a/catalog/mor/vonstengelforges2008/fig6__Original_Layout.ef b/catalog/journals/mor/vonstengelforges2008/fig6__Original_Layout.ef similarity index 100% rename from catalog/mor/vonstengelforges2008/fig6__Original_Layout.ef rename to catalog/journals/mor/vonstengelforges2008/fig6__Original_Layout.ef diff --git a/catalog/mor/vonstengelforges2008/fig9.efg b/catalog/journals/mor/vonstengelforges2008/fig9.efg similarity index 100% rename from catalog/mor/vonstengelforges2008/fig9.efg rename to catalog/journals/mor/vonstengelforges2008/fig9.efg diff --git a/catalog/mor/vonstengelforges2008/fig9__Original_Layout.ef b/catalog/journals/mor/vonstengelforges2008/fig9__Original_Layout.ef similarity index 100% rename from catalog/mor/vonstengelforges2008/fig9__Original_Layout.ef rename to catalog/journals/mor/vonstengelforges2008/fig9__Original_Layout.ef diff --git a/catalog/reiley2008/fig1.efg b/catalog/misc/reiley2008/fig1.efg similarity index 100% rename from catalog/reiley2008/fig1.efg rename to catalog/misc/reiley2008/fig1.efg diff --git a/catalog/shapley1974/fig2.nfg b/catalog/misc/shapley1974/fig2.nfg similarity index 100% rename from catalog/shapley1974/fig2.nfg rename to catalog/misc/shapley1974/fig2.nfg diff --git a/catalog/shapley1974/fig3.nfg b/catalog/misc/shapley1974/fig3.nfg similarity index 100% rename from catalog/shapley1974/fig3.nfg rename to catalog/misc/shapley1974/fig3.nfg From 7705aa0699e765d2cd4f674440bdaa88c26c127a Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Wed, 3 Jun 2026 16:02:39 +0100 Subject: [PATCH 3/6] journals/other instead of misc --- build_support/catalog/catalog.am | 6 +++--- catalog/{misc => journals/other}/reiley2008/fig1.efg | 0 catalog/{misc => journals/other}/shapley1974/fig2.nfg | 0 catalog/{misc => journals/other}/shapley1974/fig3.nfg | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename catalog/{misc => journals/other}/reiley2008/fig1.efg (100%) rename catalog/{misc => journals/other}/shapley1974/fig2.nfg (100%) rename catalog/{misc => journals/other}/shapley1974/fig3.nfg (100%) diff --git a/build_support/catalog/catalog.am b/build_support/catalog/catalog.am index 31a8adc02..80b8a9f6c 100644 --- a/build_support/catalog/catalog.am +++ b/build_support/catalog/catalog.am @@ -26,6 +26,6 @@ CATALOG_FILES = \ catalog/journals/mor/vonstengelforges2008/fig6__Original_Layout.ef \ catalog/journals/mor/vonstengelforges2008/fig9.efg \ catalog/journals/mor/vonstengelforges2008/fig9__Original_Layout.ef \ - catalog/misc/reiley2008/fig1.efg \ - catalog/misc/shapley1974/fig2.nfg \ - catalog/misc/shapley1974/fig3.nfg + catalog/journals/other/reiley2008/fig1.efg \ + catalog/journals/other/shapley1974/fig2.nfg \ + catalog/journals/other/shapley1974/fig3.nfg diff --git a/catalog/misc/reiley2008/fig1.efg b/catalog/journals/other/reiley2008/fig1.efg similarity index 100% rename from catalog/misc/reiley2008/fig1.efg rename to catalog/journals/other/reiley2008/fig1.efg diff --git a/catalog/misc/shapley1974/fig2.nfg b/catalog/journals/other/shapley1974/fig2.nfg similarity index 100% rename from catalog/misc/shapley1974/fig2.nfg rename to catalog/journals/other/shapley1974/fig2.nfg diff --git a/catalog/misc/shapley1974/fig3.nfg b/catalog/journals/other/shapley1974/fig3.nfg similarity index 100% rename from catalog/misc/shapley1974/fig3.nfg rename to catalog/journals/other/shapley1974/fig3.nfg From 7a5e9b9b7ec83cc08d96e5b01bd7e74e9c157fc4 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Wed, 3 Jun 2026 20:16:15 +0100 Subject: [PATCH 4/6] update tests to reflect hierarchical catalog structure --- tests/test_actions.py | 34 +++++++++++++++++----------------- tests/test_behav.py | 8 ++++---- tests/test_extensive.py | 6 +++--- tests/test_file.py | 14 +++++++------- tests/test_infosets.py | 4 ++-- tests/test_io.py | 2 +- tests/test_mixed.py | 6 +++--- tests/test_nash.py | 2 +- tests/test_node.py | 32 ++++++++++++++++---------------- 9 files changed, 54 insertions(+), 54 deletions(-) diff --git a/tests/test_actions.py b/tests/test_actions.py index 1f1acadcd..28989e33e 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -144,7 +144,7 @@ def test_action_delete_chance(game: gbt.Game): def test_action_plays(): """Verify `action.plays` returns plays reachable from a given action. """ - game = gbt.catalog.load("selten1975/fig1") + game = gbt.catalog.load("journals/ijgt/selten1975/fig1") list_nodes = list(game.nodes) list_infosets = list(game.infosets) @@ -160,16 +160,16 @@ def test_action_plays(): @pytest.mark.parametrize( "game, player_ind, str_ind, infoset_ind, expected_action_ind", [ - (gbt.catalog.load("selten1975/fig1"), 0, 0, 0, 0), - (gbt.catalog.load("selten1975/fig1"), 0, 1, 0, 1), - (gbt.catalog.load("selten1975/fig1"), 1, 0, 1, 0), - (gbt.catalog.load("selten1975/fig1"), 1, 1, 1, 1), - (gbt.catalog.load("selten1975/fig1"), 2, 0, 2, 0), - (gbt.catalog.load("selten1975/fig1"), 2, 1, 2, 1), - (gbt.catalog.load("selten1975/fig2"), 0, 0, 0, 0), - (gbt.catalog.load("selten1975/fig2"), 0, 1, 0, 1), - (gbt.catalog.load("selten1975/fig2"), 1, 0, 2, 0), - (gbt.catalog.load("selten1975/fig2"), 1, 1, 2, 1), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 0, 0, 0, 0), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 0, 1, 0, 1), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 1, 0, 1, 0), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 1, 1, 1, 1), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 2, 0, 2, 0), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 2, 1, 2, 1), + (gbt.catalog.load("journals/ijgt/selten1975/fig2"), 0, 0, 0, 0), + (gbt.catalog.load("journals/ijgt/selten1975/fig2"), 0, 1, 0, 1), + (gbt.catalog.load("journals/ijgt/selten1975/fig2"), 1, 0, 2, 0), + (gbt.catalog.load("journals/ijgt/selten1975/fig2"), 1, 1, 2, 1), (games.read_from_file("basic_extensive_game.efg"), 0, 0, 0, 0), (games.read_from_file("basic_extensive_game.efg"), 0, 1, 0, 1), (games.read_from_file("basic_extensive_game.efg"), 1, 0, 1, 0), @@ -194,7 +194,7 @@ def test_strategy_action_defined(game, player_ind, str_ind, infoset_ind, expecte @pytest.mark.parametrize( "game, player_ind, str_ind, infoset_ind", [ - (gbt.catalog.load("selten1975/fig2"), 0, 0, 1), + (gbt.catalog.load("journals/ijgt/selten1975/fig2"), 0, 0, 1), (games.read_from_file("cent3.efg"), 0, 0, 1), (games.read_from_file("cent3.efg"), 0, 0, 2), (games.read_from_file("cent3.efg"), 0, 1, 2), @@ -218,10 +218,10 @@ def test_strategy_action_undefined_returns_none(game, player_ind, str_ind, infos @pytest.mark.parametrize( "game, player_ind, infoset_ind", [ - (gbt.catalog.load("selten1975/fig1"), 0, 1), - (gbt.catalog.load("selten1975/fig1"), 1, 0), - (gbt.catalog.load("selten1975/fig2"), 0, 2), - (gbt.catalog.load("selten1975/fig2"), 1, 0), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 0, 1), + (gbt.catalog.load("journals/ijgt/selten1975/fig1"), 1, 0), + (gbt.catalog.load("journals/ijgt/selten1975/fig2"), 0, 2), + (gbt.catalog.load("journals/ijgt/selten1975/fig2"), 1, 0), (games.read_from_file("basic_extensive_game.efg"), 0, 1), (games.read_from_file("basic_extensive_game.efg"), 1, 2), (games.read_from_file("basic_extensive_game.efg"), 2, 0), @@ -243,7 +243,7 @@ def test_strategy_action_raises_value_error_for_wrong_player(game, player_ind, i def test_strategy_action_raises_error_for_strategic_game(): """Verify `Strategy.action` retrieves the action prescribed by the strategy """ - game_efg = gbt.catalog.load("selten1975/fig2") + game_efg = gbt.catalog.load("journals/ijgt/selten1975/fig2") game_nfg = game_efg.from_arrays(game_efg.to_arrays()[0], game_efg.to_arrays()[1]) alice = game_nfg.players[0] strategy = alice.strategies[0] diff --git a/tests/test_behav.py b/tests/test_behav.py index 23d498dbc..54dcd1c3a 100644 --- a/tests/test_behav.py +++ b/tests/test_behav.py @@ -1303,7 +1303,7 @@ def test_agent_liap_value_reference( "1/16", ), (games.read_from_file("mixed_behavior_game.efg"), None, False, 0.25, 0.25, 0.0625, 0.0625), - (gbt.catalog.load("myerson1991/fig4_2"), [0, 1, 0, 1, 1, 0], True, 1, 0, 1, 0), + (gbt.catalog.load("books/myerson1991/fig4_2"), [0, 1, 0, 1, 1, 0], True, 1, 0, 1, 0), ], ) def test_agent_max_regret_versus_non_agent( @@ -2100,7 +2100,7 @@ def test_tree_representation_error(game: gbt.Game, rational_flag: bool, data: li def test_undefined_action_value(): """Test that undefined action values return `None`.""" - game = gbt.catalog.load("selten1975/fig1") + game = gbt.catalog.load("journals/ijgt/selten1975/fig1") action = game.players[2].infosets[0].actions[0] for rat in [False, True]: profile = game.mixed_behavior_profile([[[1, 0]], [[1, 0]], [[1, 0]]], rational=rat) @@ -2109,7 +2109,7 @@ def test_undefined_action_value(): def test_undefined_belief(): """Test that undefined beliefs return `None`.""" - game = gbt.catalog.load("selten1975/fig1") + game = gbt.catalog.load("journals/ijgt/selten1975/fig1") node = game.players[2].infosets[0].members[0] for rat in [False, True]: profile = game.mixed_behavior_profile([[[1, 0]], [[1, 0]], [[1, 0]]], rational=rat) @@ -2118,7 +2118,7 @@ def test_undefined_belief(): def test_undefined_infoset_value(): """Test that undefined infoset values return `None`.""" - game = gbt.catalog.load("selten1975/fig1") + game = gbt.catalog.load("journals/ijgt/selten1975/fig1") infoset = game.players[2].infosets[0] for rat in [False, True]: profile = game.mixed_behavior_profile([[[1, 0]], [[1, 0]], [[1, 0]]], rational=rat) diff --git a/tests/test_extensive.py b/tests/test_extensive.py index a04618796..fbef465bf 100644 --- a/tests/test_extensive.py +++ b/tests/test_extensive.py @@ -49,7 +49,7 @@ def test_game_add_players_nolabel(): @pytest.mark.parametrize("game_input,expected_result", [ # Games with perfect recall from files (game_input is a string) - (gbt.catalog.load("selten1975/fig2"), True), + (gbt.catalog.load("journals/ijgt/selten1975/fig2"), True), ("stripped_down_poker.efg", True), # Games with perfect recall from generated games (game_input is a gbt.Game object) # - Centipede games @@ -129,7 +129,7 @@ def test_outcome_index_exception_label(): ), # 2 players; reduction possible for player 1; payoff ties ( - gbt.catalog.load("selten1975/fig2"), + gbt.catalog.load("journals/ijgt/selten1975/fig2"), [["1*", "21", "22"], ["1", "2"]], [ np.array([[1, 1], [0, 0], [0, 2]]), @@ -147,7 +147,7 @@ def test_outcome_index_exception_label(): ), # Selten's Horse: game with three players ( - gbt.catalog.load("selten1975/fig1"), + gbt.catalog.load("journals/ijgt/selten1975/fig1"), [["1", "2"], ["1", "2"], ["1", "2"]], [ np.array([[[1, 1], [4, 0]], [[3, 0], [3, 0]]]), diff --git a/tests/test_file.py b/tests/test_file.py index b22e36c25..cd0d0c883 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -32,7 +32,7 @@ def test_efg_no_newline_end(): def test_string_wrong_magic(): - file_text = gbt.catalog.load("selten1975/fig1").to_efg() + file_text = gbt.catalog.load("journals/ijgt/selten1975/fig1").to_efg() file_text = file_text.replace("EFG", "") with pytest.raises(ValueError) as excinfo: _parse_efg(file_text) @@ -43,7 +43,7 @@ def test_string_wrong_magic(): def test_efg_unsupported_version(): - file_text = gbt.catalog.load("selten1975/fig1").to_efg() + file_text = gbt.catalog.load("journals/ijgt/selten1975/fig1").to_efg() file_text = file_text.replace("EFG 2", "EFG 1") with pytest.raises(ValueError) as excinfo: _parse_efg(file_text) @@ -51,7 +51,7 @@ def test_efg_unsupported_version(): def test_efg_unsupported_precision(): - file_text = gbt.catalog.load("selten1975/fig1").to_efg() + file_text = gbt.catalog.load("journals/ijgt/selten1975/fig1").to_efg() file_text = file_text.replace("EFG 2 R", "EFG 2 X") with pytest.raises(ValueError) as excinfo: _parse_efg(file_text) @@ -62,7 +62,7 @@ def test_efg_unsupported_precision(): def test_efg_invalid_node_type(): - file_text = gbt.catalog.load("selten1975/fig2").to_efg() + file_text = gbt.catalog.load("journals/ijgt/selten1975/fig2").to_efg() file_text = file_text.replace('p "" 1 1', 'x "" 1 1') with pytest.raises(ValueError) as excinfo: _parse_efg(file_text) @@ -70,7 +70,7 @@ def test_efg_invalid_node_type(): def test_efg_payoffs_too_many(): - file_text = gbt.catalog.load("selten1975/fig2").to_efg() + file_text = gbt.catalog.load("journals/ijgt/selten1975/fig2").to_efg() file_text = file_text.replace("1, 1", "1, 2, 3") with pytest.raises(ValueError) as excinfo: _parse_efg(file_text) @@ -78,7 +78,7 @@ def test_efg_payoffs_too_many(): def test_nfg_title_missing(): - file_text = gbt.catalog.load("selten1975/fig2").to_nfg() + file_text = gbt.catalog.load("journals/ijgt/selten1975/fig2").to_nfg() file_text = file_text.replace('"Selten (IJGT 1975) Figure 2"', "") with pytest.raises(ValueError) as excinfo: _parse_nfg(file_text) @@ -86,7 +86,7 @@ def test_nfg_title_missing(): def test_nfg_player_missing(): - file_text = gbt.catalog.load("selten1975/fig2").to_nfg() + file_text = gbt.catalog.load("journals/ijgt/selten1975/fig2").to_nfg() file_text = file_text.replace('"Player 2"', "") with pytest.raises(ValueError) as excinfo: _parse_nfg(file_text) diff --git a/tests/test_infosets.py b/tests/test_infosets.py index bd3f0d6e8..6caf8fb3c 100644 --- a/tests/test_infosets.py +++ b/tests/test_infosets.py @@ -62,7 +62,7 @@ def test_infoset_add_action_error(): def test_infoset_plays(): """Verify `infoset.plays` returns plays reachable from a given infoset. """ - game = gbt.catalog.load("selten1975/fig1") + game = gbt.catalog.load("journals/ijgt/selten1975/fig1") list_nodes = list(game.nodes) list_infosets = list(game.infosets) @@ -150,7 +150,7 @@ class AbsentMindednessTestCase: # Games without absent-mindedness pytest.param( AbsentMindednessTestCase( - factory=functools.partial(gbt.catalog.load, "selten1975/fig2"), + factory=functools.partial(gbt.catalog.load, "journals/ijgt/selten1975/fig2"), expected_am_paths=[] ), id="short_centipede_perfect_info" diff --git a/tests/test_io.py b/tests/test_io.py index 69bc06148..e654b98d8 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -120,7 +120,7 @@ def test_write_latex(): def test_read_write_efg(): - efg_game = gbt.catalog.load("selten1975/fig1") + efg_game = gbt.catalog.load("journals/ijgt/selten1975/fig1") serialized_efg_game = efg_game.to_efg() deserialized_efg_game = gbt.read_efg(io.BytesIO(serialized_efg_game.encode())) double_serialized_efg_game = deserialized_efg_game.to_efg() diff --git a/tests/test_mixed.py b/tests/test_mixed.py index e4f288fec..4187aa36a 100644 --- a/tests/test_mixed.py +++ b/tests/test_mixed.py @@ -1361,7 +1361,7 @@ def test_player_regret_max_regret_consistency( ################################################################################# # Selten's horse ( - gbt.catalog.load("selten1975/fig1"), + gbt.catalog.load("journals/ijgt/selten1975/fig1"), [["4/9", "5/9"], ["1/11", "10/11"], ["8/9", "1/9"]], [["4/9", "5/9"], ["10/11", "1/11"], ["8/9", "1/9"]], gbt.Rational("4/9"), @@ -1459,13 +1459,13 @@ def test_linearity_payoff_property( ################################################################################# # Selten's horse ( - gbt.catalog.load("selten1975/fig1"), + gbt.catalog.load("journals/ijgt/selten1975/fig1"), [["4/9", "5/9"], ["6/11", "5/11"], ["4/7", "3/7"]], ZERO, True, ), ( - gbt.catalog.load("selten1975/fig1"), + gbt.catalog.load("journals/ijgt/selten1975/fig1"), [[4 / 9, 5 / 9], [6 / 11, 5 / 11], [4 / 7, 3 / 7]], TOL, False, diff --git a/tests/test_nash.py b/tests/test_nash.py index 706d8ffee..1e7ff092e 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -2483,7 +2483,7 @@ def are_the_same(game, found, candidate): # Examples where the are agent-form pure equillibrium behaviors that are not Nash eq pytest.param( EquilibriumTestCase( - factory=functools.partial(gbt.catalog.load, "myerson1991/fig4_2"), + factory=functools.partial(gbt.catalog.load, "books/myerson1991/fig4_2"), solver=functools.partial(gbt.nash.enumpure_agent_solve), expected=[[[d(1, 0), d(0, 1)], [d(0, 1)]], [[d(0, 1), d(0, 1)], [d(1, 0)]]], ), diff --git a/tests/test_node.py b/tests/test_node.py index 8478c5b5a..a6e5fa95a 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -132,7 +132,7 @@ class SubgameRootsTestCase: # ------------------------------------------------------------------------ pytest.param( SubgameRootsTestCase( - factory=functools.partial(gbt.catalog.load, "selten1975/fig2"), + factory=functools.partial(gbt.catalog.load, "journals/ijgt/selten1975/fig2"), expected_paths=[[], ["L"], ["L", "L"]] ), id="centipede_3_rounds" @@ -441,7 +441,7 @@ def _subtrees_equal( def test_copy_tree_onto_nondescendent_terminal_node(): """Test copying a subtree to a non-descendent node.""" - g = gbt.catalog.load("selten1975/fig1") + g = gbt.catalog.load("journals/ijgt/selten1975/fig1") list_nodes = list(g.nodes) src_node = list_nodes[3] # path=[1, 0] dest_node = list_nodes[2] # path=[0, 0] @@ -453,7 +453,7 @@ def test_copy_tree_onto_nondescendent_terminal_node(): def test_copy_tree_onto_descendent_terminal_node(): """Test copying a subtree to a node that's a descendent of the original.""" - g = gbt.catalog.load("selten1975/fig1") + g = gbt.catalog.load("journals/ijgt/selten1975/fig1") list_nodes = list(g.nodes) src_node = list_nodes[1] # path=[0] dest_node = list_nodes[4] # path=[0, 1, 0] @@ -680,7 +680,7 @@ def _count_subtree_nodes(start_node: gbt.Node, count_terminal: bool) -> int: def test_len_matches_expected_node_count(): """Verify `len(game.nodes)` matches expected node count """ - game = gbt.catalog.load("selten1975/fig1") + game = gbt.catalog.load("journals/ijgt/selten1975/fig1") expected_node_count = 9 direct_len = len(game.nodes) @@ -692,7 +692,7 @@ def test_len_matches_expected_node_count(): def test_len_after_delete_tree(): """Verify `len(game.nodes)` is correct after `delete_tree`. """ - game = gbt.catalog.load("selten1975/fig1") + game = gbt.catalog.load("journals/ijgt/selten1975/fig1") initial_number_of_nodes = len(game.nodes) list_nodes = list(game.nodes) @@ -707,7 +707,7 @@ def test_len_after_delete_tree(): def test_len_after_delete_parent(): """Verify `len(game.nodes)` is correct after `delete_parent`. """ - game = gbt.catalog.load("selten1975/fig2") + game = gbt.catalog.load("journals/ijgt/selten1975/fig2") initial_number_of_nodes = len(game.nodes) list_nodes = list(game.nodes) @@ -725,7 +725,7 @@ def test_len_after_delete_parent(): def test_len_after_append_move(): """Verify `len(game.nodes)` is correct after `append_move`. """ - game = gbt.catalog.load("selten1975/fig1") + game = gbt.catalog.load("journals/ijgt/selten1975/fig1") initial_number_of_nodes = len(game.nodes) list_nodes = list(game.nodes) @@ -741,7 +741,7 @@ def test_len_after_append_move(): def test_len_after_append_infoset(): """Verify `len(game.nodes)` is correct after `append_infoset`. """ - game = gbt.catalog.load("selten1975/fig2") + game = gbt.catalog.load("journals/ijgt/selten1975/fig2") initial_number_of_nodes = len(game.nodes) list_nodes = list(game.nodes) @@ -758,7 +758,7 @@ def test_len_after_append_infoset(): def test_len_after_add_action(): """Verify `len(game.nodes)` is correct after `add_action`. """ - game = gbt.catalog.load("selten1975/fig1") + game = gbt.catalog.load("journals/ijgt/selten1975/fig1") initial_number_of_nodes = len(game.nodes) infoset_to_modify = game.infosets[1] @@ -773,7 +773,7 @@ def test_len_after_add_action(): def test_len_after_delete_action(): """Verify `len(game.nodes)` is correct after `delete_action`. """ - game = gbt.catalog.load("selten1975/fig2") + game = gbt.catalog.load("journals/ijgt/selten1975/fig2") initial_number_of_nodes = len(game.nodes) action_to_delete = game.infosets[0].actions[1] @@ -794,7 +794,7 @@ def test_len_after_delete_action(): def test_len_after_insert_move(): """Verify `len(game.nodes)` is correct after `insert_move`. """ - game = gbt.catalog.load("selten1975/fig1") + game = gbt.catalog.load("journals/ijgt/selten1975/fig1") initial_number_of_nodes = len(game.nodes) list_nodes = list(game.nodes) @@ -811,7 +811,7 @@ def test_len_after_insert_move(): def test_len_after_insert_infoset(): """Verify `len(game.nodes)` is correct after `insert_infoset`. """ - game = gbt.catalog.load("selten1975/fig1") + game = gbt.catalog.load("journals/ijgt/selten1975/fig1") initial_number_of_nodes = len(game.nodes) list_nodes = list(game.nodes) @@ -828,7 +828,7 @@ def test_len_after_insert_infoset(): def test_len_after_copy_tree(): """Verify `len(game.nodes)` is correct after `copy_tree`. """ - game = gbt.catalog.load("selten1975/fig1") + game = gbt.catalog.load("journals/ijgt/selten1975/fig1") initial_number_of_nodes = len(game.nodes) list_nodes = list(game.nodes) src_node = list_nodes[3] # path=[1, 0] @@ -843,7 +843,7 @@ def test_len_after_copy_tree(): def test_node_plays(): """Verify `node.plays` returns plays reachable from a given node. """ - game = gbt.catalog.load("selten1975/fig2") + game = gbt.catalog.load("journals/ijgt/selten1975/fig2") list_nodes = list(game.nodes) test_node = list_nodes[2] # path=[1] @@ -884,8 +884,8 @@ def test_node_children_other_infoset_action(): pytest.param(games.read_from_file("basic_extensive_game.efg")), pytest.param(games.read_from_file("binary_3_levels_generic_payoffs.efg")), pytest.param(games.read_from_file("cent3.efg")), - pytest.param(gbt.catalog.load("selten1975/fig1")), - pytest.param(gbt.catalog.load("selten1975/fig2")), + pytest.param(gbt.catalog.load("journals/ijgt/selten1975/fig1")), + pytest.param(gbt.catalog.load("journals/ijgt/selten1975/fig2")), pytest.param(games.read_from_file("stripped_down_poker.efg")), pytest.param(gbt.Game.new_tree()), ], From 8f13f7a21d73b2c64b0a8b0352ca07c3b3c1d4cc Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Wed, 3 Jun 2026 20:41:57 +0100 Subject: [PATCH 5/6] Apply new game slugs across docs and tests (#915) * Apply new game slugs across docs and tests * docs: add agent versus non-agent regret to pygambit tutorial index --- build_support/catalog/draw_tree_settings.yaml | 18 +++++++++--------- doc/developer.catalog.rst | 2 +- doc/pygambit.rst | 1 + doc/tools.enummixed.rst | 4 ++-- doc/tools.enumpoly.rst | 2 +- doc/tools.enumpure.rst | 6 +++--- doc/tools.gnm.rst | 2 +- doc/tools.ipa.rst | 2 +- doc/tools.lcp.rst | 2 +- doc/tools.liap.rst | 2 +- doc/tools.logit.rst | 2 +- doc/tools.simpdiv.rst | 2 +- doc/tutorials/02_extensive_form.ipynb | 2 +- .../agent_versus_non_agent_regret.ipynb | 2 +- 14 files changed, 25 insertions(+), 24 deletions(-) diff --git a/build_support/catalog/draw_tree_settings.yaml b/build_support/catalog/draw_tree_settings.yaml index c71846502..d0b414f55 100644 --- a/build_support/catalog/draw_tree_settings.yaml +++ b/build_support/catalog/draw_tree_settings.yaml @@ -12,22 +12,22 @@ defaults: # A key matches if slug == key or slug starts with key + "/". # Consult https://www.gambit-project.org/draw_tree/ for available settings. overrides: - bagwell1995: + journals/geb/bagwell1995: sublevel_scaling: 1 - watson2013: + books/watson2013: sublevel_scaling: 1 - selten1975: + journals/ijgt/selten1975: shared_terminal_depth: false - myerson1991/fig2_1: + books/myerson1991/fig2_1: action_label_position: 0.4 - reiley2008/fig1: + misc/reiley2008/fig1: action_label_position: 0.4 - vonstengel2022/fig10.1: + books/vonstengel2022/fig10.1: sublevel_scaling: 0.75 shared_terminal_depth: false - vonstengelforges2008/fig1: + journals/mor/vonstengelforges2008/fig1: sublevel_scaling: 1 - vonstengelforges2008/fig9: + journals/mor/vonstengelforges2008/fig9: sublevel_scaling: 0.5 - gilboa1997/fig1: + journals/geb/gilboa1997/fig1: action_label_dist: 5.0 diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index bbed7b3e4..1db84ff7a 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -45,7 +45,7 @@ Currently supported representations are: .. code-block:: python - pygambit.catalog.load("watson2013/exercise29_6") + pygambit.catalog.load("books/watson2013/exercise29_6") .. note:: diff --git a/doc/pygambit.rst b/doc/pygambit.rst index 60077ebe9..d2c8006cb 100644 --- a/doc/pygambit.rst +++ b/doc/pygambit.rst @@ -43,6 +43,7 @@ Advanced tutorials: tutorials/advanced_tutorials/starting_points tutorials/advanced_tutorials/quantal_response + tutorials/advanced_tutorials/agent_versus_non_agent_regret .. pygambit.external_programs Interoperability tutorials diff --git a/doc/tools.enummixed.rst b/doc/tools.enummixed.rst index c9f3d28b3..59a6d2e6b 100644 --- a/doc/tools.enummixed.rst +++ b/doc/tools.enummixed.rst @@ -51,7 +51,7 @@ See the :ref:`algorithm description ` for full details. Computing the equilibria, in mixed strategies, of the reduced strategic form of the example in Figure 2 of :cite:p:`Sel75`:: - $ gambit-enummixed catalog/selten1975/fig2.efg + $ gambit-enummixed catalog/journals/ijgt/selten1975/fig2.efg Compute Nash equilibria by enumerating extreme points Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL @@ -63,7 +63,7 @@ In fact, this game has a one-dimensional continuum of equilibria. This fact can be observed by examining the connectedness information using the ``-c`` switch:: - $ gambit-enummixed -c catalog/selten1975/fig2.efg + $ gambit-enummixed -c catalog/journals/ijgt/selten1975/fig2.efg Compute Nash equilibria by enumerating extreme points Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL diff --git a/doc/tools.enumpoly.rst b/doc/tools.enumpoly.rst index d69c41c99..dda604191 100644 --- a/doc/tools.enumpoly.rst +++ b/doc/tools.enumpoly.rst @@ -76,7 +76,7 @@ support of some set of equilibria. Computing equilibria of the example in Figure 1 of :cite:p:`Sel75`, sometimes called "Selten's horse":: - $ gambit-enumpoly -S catalog/selten1975/fig1.efg + $ gambit-enumpoly -S catalog/journals/ijgt/selten1975/fig1.efg Compute Nash equilibria by solving polynomial systems Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL diff --git a/doc/tools.enumpure.rst b/doc/tools.enumpure.rst index 582c5036f..29f17c3ef 100644 --- a/doc/tools.enumpure.rst +++ b/doc/tools.enumpure.rst @@ -54,7 +54,7 @@ See the :ref:`algorithm description ` for full details. Computing the pure-strategy equilibria of extensive game in Figure 2 of :cite:p:`Sel75`:: - $ gambit-enumpure catalog/selten1975/fig2.efg + $ gambit-enumpure catalog/journals/ijgt/selten1975/fig2.efg Search for Nash equilibria in pure strategies Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project @@ -66,7 +66,7 @@ With the `-S` switch, the set of equilibria returned is the same, except expressed in strategic game strategies rather than behavior strategies:: - $ gambit-enumpure -S catalog/selten1975/fig2.efg + $ gambit-enumpure -S catalog/journals/ijgt/selten1975/fig2.efg Search for Nash equilibria in pure strategies Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project @@ -78,7 +78,7 @@ The `-A` switch considers only behavior strategy profiles where there is no way for a player to improve his payoff by changing action at only one information set; therefore the set of solutions is larger:: - $ gambit-enumpure -A catalog/selten1975/fig2.efg + $ gambit-enumpure -A catalog/journals/ijgt/selten1975/fig2.efg Search for Nash equilibria in pure strategies Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL diff --git a/doc/tools.gnm.rst b/doc/tools.gnm.rst index 53ed8e687..17023a2d6 100644 --- a/doc/tools.gnm.rst +++ b/doc/tools.gnm.rst @@ -76,7 +76,7 @@ subsets of equilibria being found. Computing an equilibrium of the reduced strategic form of the example in Figure 2 of :cite:p:`Sel75`:: - $ gambit-gnm catalog/selten1975/fig2.efg + $ gambit-gnm catalog/journals/ijgt/selten1975/fig2.efg Compute Nash equilibria using a global Newton method Gametracer version 0.2, Copyright (C) 2002, Ben Blum and Christian Shelton Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project diff --git a/doc/tools.ipa.rst b/doc/tools.ipa.rst index 1484a63f1..ac122f010 100644 --- a/doc/tools.ipa.rst +++ b/doc/tools.ipa.rst @@ -42,7 +42,7 @@ equilibria being found. Computing an equilibrium of the reduced strategic form of the example in Figure 2 of :cite:p:`Sel75`:: - $ gambit-ipa catalog/selten1975/fig2.efg + $ gambit-ipa catalog/journals/ijgt/selten1975/fig2.efg Compute Nash equilibria using iterated polymatrix approximation Gametracer version 0.2, Copyright (C) 2002, Ben Blum and Christian Shelton Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project diff --git a/doc/tools.lcp.rst b/doc/tools.lcp.rst index 0504aee19..5fc057a5c 100644 --- a/doc/tools.lcp.rst +++ b/doc/tools.lcp.rst @@ -56,7 +56,7 @@ See the :ref:`algorithm description ` for full details. Computing an equilibrium of the example in Figure 2 of :cite:p:`Sel75`:: - $ gambit-lcp catalog/selten1975/fig2.efg + $ gambit-lcp catalog/journals/ijgt/selten1975/fig2.efg Compute Nash equilibria by solving a linear complementarity program Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL diff --git a/doc/tools.liap.rst b/doc/tools.liap.rst index e4232826f..b53d5b78b 100644 --- a/doc/tools.liap.rst +++ b/doc/tools.liap.rst @@ -89,7 +89,7 @@ See the :ref:`algorithm description ` for full details. Computing an equilibrium in mixed strategies of the example in Figure 2 of :cite:p:`Sel75`:: - $ gambit-liap -S catalog/selten1975/fig2.efg + $ gambit-liap -S catalog/journals/ijgt/selten1975/fig2.efg Compute Nash equilibria by minimizing the Lyapunov function Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL diff --git a/doc/tools.logit.rst b/doc/tools.logit.rst index eb030b602..7cb9a142b 100644 --- a/doc/tools.logit.rst +++ b/doc/tools.logit.rst @@ -78,7 +78,7 @@ Computing the principal branch, in mixed strategies, of the reduced strategic form of the example in Figure 2 of :cite:p:`Sel75`:: - $ gambit-logit -S catalog/selten1975/fig2.efg + $ gambit-logit -S catalog/journals/ijgt/selten1975/fig2.efg Compute a branch of the logit equilibrium correspondence Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL diff --git a/doc/tools.simpdiv.rst b/doc/tools.simpdiv.rst index 5a0114239..5a6569baa 100644 --- a/doc/tools.simpdiv.rst +++ b/doc/tools.simpdiv.rst @@ -78,7 +78,7 @@ options to specify additional starting points for the algorithm. Computing an equilibrium in mixed strategies of the example in Figure 2 of :cite:p:`Sel75`:: - $ gambit-simpdiv catalog/selten1975/fig2.efg + $ gambit-simpdiv catalog/journals/ijgt/selten1975/fig2.efg Compute Nash equilibria using simplicial subdivision Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project This is free software, distributed under the GNU GPL diff --git a/doc/tutorials/02_extensive_form.ipynb b/doc/tutorials/02_extensive_form.ipynb index 70b25c079..5f81eeff0 100644 --- a/doc/tutorials/02_extensive_form.ipynb +++ b/doc/tutorials/02_extensive_form.ipynb @@ -298,7 +298,7 @@ "metadata": {}, "outputs": [], "source": [ - "g = gbt.catalog.load(\"selten1975/fig2\")\n", + "g = gbt.catalog.load(\"journals/ijgt/selten1975/fig2\")\n", "draw_tree(g)" ] }, 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 93860be46..e1df0b2ab 100644 --- a/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb +++ b/doc/tutorials/advanced_tutorials/agent_versus_non_agent_regret.ipynb @@ -37,7 +37,7 @@ "\n", "import pygambit as gbt\n", "\n", - "g = gbt.catalog.load(\"myerson1991/fig4_2\")\n", + "g = gbt.catalog.load(\"books/myerson1991/fig4_2\")\n", "draw_tree(g)" ] }, From 235c306c55e2539152769e46983b04fd491c0ecc Mon Sep 17 00:00:00 2001 From: Ed Chalstrey Date: Thu, 4 Jun 2026 13:41:24 +0100 Subject: [PATCH 6/6] feat: implement hierarchical catalog structure with configurable labels and nested RST dropdowns (#921) --- build_support/catalog/catalog_hierarchy.yaml | 23 ++ build_support/catalog/test_update.py | 172 +++++++- build_support/catalog/update.py | 414 +++++++++++-------- doc/developer.catalog.rst | 26 +- 4 files changed, 453 insertions(+), 182 deletions(-) create mode 100644 build_support/catalog/catalog_hierarchy.yaml diff --git a/build_support/catalog/catalog_hierarchy.yaml b/build_support/catalog/catalog_hierarchy.yaml new file mode 100644 index 000000000..621646708 --- /dev/null +++ b/build_support/catalog/catalog_hierarchy.yaml @@ -0,0 +1,23 @@ +# Human-readable labels for non-leaf nodes in the catalog hierarchy. +# Keys are slug path prefixes (e.g. "journals/geb"). +# Leaf games use their title from the game file; only group nodes need entries here. +# When adding a new venue, journal, or top-level category, add a label below. +labels: + books: "Books" + journals: "Journals" + conf: "Conferences" + journals/geb: "Games and Economic Behavior (GEB)" + journals/ijgt: "International Journal of Game Theory (IJGT)" + journals/mor: "Mathematics of Operations Research (MOR)" + journals/other: "Other" + conf/itcs: "Innovations in Theoretical Computer Science (ITCS)" + books/myerson1991: "Myerson (1991) — Game Theory: Analysis of Conflict" + books/vonstengel2022: "von Stengel (2022) — Game Theory Basics" + books/watson2013: "Watson (2013) — Strategy: An Introduction to Game Theory" + journals/geb/gilboa1997: "Gilboa (1997)" + journals/ijgt/nau2004: "Nau et al. (2004)" + journals/ijgt/selten1975: "Selten (1975)" + journals/mor/vonstengelforges2008: "von Stengel & Forges (2008)" + journals/other/reiley2008: "Reiley (2008)" + journals/other/shapley1974: "Shapley (1974)" + conf/itcs/jakobsen2016: "Jakobsen et al. (2016)" diff --git a/build_support/catalog/test_update.py b/build_support/catalog/test_update.py index 70589e664..2e0e402ad 100644 --- a/build_support/catalog/test_update.py +++ b/build_support/catalog/test_update.py @@ -7,7 +7,7 @@ Monkeypatching strategy ----------------------- -``update.py`` depends on three external resources that are replaced in tests: +``update.py`` depends on four external resources that are replaced in tests: 1. ``DRAW_TREE_SETTINGS_CONFIG`` (a ``Path``) — swapped for a tmp YAML file so ``catalog_draw_tree_settings`` reads controlled config without touching the @@ -15,13 +15,18 @@ "DRAW_TREE_SETTINGS_CONFIG", yaml_file)`` replaces the module-level path for the duration of a single test and restores it automatically on teardown. -2. ``generate_tex`` / ``generate_png`` / ``generate_pdf`` / ``generate_svg`` +2. ``CATALOG_HIERARCHY_CONFIG`` (a ``Path``) — swapped for a tmp YAML file so + ``load_hierarchy_labels`` reads controlled labels without touching the real + ``catalog_hierarchy.yaml``. Swap via ``monkeypatch.setattr(update, + "CATALOG_HIERARCHY_CONFIG", yaml_file)``. + +3. ``generate_tex`` / ``generate_png`` / ``generate_pdf`` / ``generate_svg`` (functions imported from ``draw_tree``) — replaced with no-ops or call-tracking lambdas. This lets us test RST-generation logic without actually invoking LaTeX, and lets us assert whether image generation was triggered at all. -3. ``catalog_dir`` (an argument to ``generate_rst_table`` and +4. ``catalog_dir`` (an argument to ``generate_rst_table`` and ``update_makefile``) — both functions accept an optional ``catalog_dir`` kwarg that defaults to the real ``CATALOG_DIR``. Tests pass a ``tmp_path``-based directory instead, keeping all file I/O inside pytest's @@ -581,6 +586,167 @@ def test_per_variant_images_not_regenerated_when_all_exist(self, tmp_path, monke assert calls == [] +# --------------------------------------------------------------------------- +# Tests for hierarchy helpers and hierarchical RST output +# --------------------------------------------------------------------------- + +# A minimal catalog_hierarchy.yaml used by hierarchy tests. +_HIERARCHY_YAML = textwrap.dedent("""\ + labels: + cat: "My Category" + cat/src: "My Source" +""") + + +@pytest.mark.catalog_update +class TestHierarchyHelpers: + """Unit tests for ``load_hierarchy_labels``, ``_node_label``, and ``_build_slug_tree``.""" + + def test_load_hierarchy_labels_returns_dict(self, tmp_path, monkeypatch): + """``load_hierarchy_labels`` returns the labels dict from the YAML.""" + yaml_file = tmp_path / "hier.yaml" + yaml_file.write_text(_HIERARCHY_YAML, encoding="utf-8") + monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", yaml_file) + labels = update.load_hierarchy_labels() + assert labels["cat"] == "My Category" + assert labels["cat/src"] == "My Source" + + def test_node_label_uses_yaml(self, tmp_path, monkeypatch): + """``_node_label`` returns the YAML label when the prefix is present.""" + yaml_file = tmp_path / "hier.yaml" + yaml_file.write_text(_HIERARCHY_YAML, encoding="utf-8") + monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", yaml_file) + labels = update.load_hierarchy_labels() + assert update._node_label("cat", labels) == "My Category" + + def test_node_label_fallback_title_case(self, tmp_path, monkeypatch): + """``_node_label`` falls back to title-casing the last component.""" + yaml_file = tmp_path / "hier.yaml" + yaml_file.write_text(_HIERARCHY_YAML, encoding="utf-8") + monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", yaml_file) + labels = update.load_hierarchy_labels() + assert update._node_label("cat/unknownsrc", labels) == "Unknownsrc" + + def test_build_slug_tree_single_game(self): + """A single-slug DataFrame builds a 2-level tree.""" + df = _make_df(_efg_row("cat/src/game1")) + tree = update._build_slug_tree(df) + assert "cat" in tree + assert "src" in tree["cat"] + assert "game1" in tree["cat"]["src"] + + def test_build_slug_tree_groups_siblings(self): + """Two slugs sharing a prefix are grouped under the same intermediate node.""" + df = _make_df(_efg_row("cat/src/game1"), _efg_row("cat/src/game2")) + tree = update._build_slug_tree(df) + assert set(tree["cat"]["src"].keys()) == {"game1", "game2"} + + def test_build_slug_tree_skips_unknown_format(self): + """Rows with unrecognised Format are excluded from the tree.""" + row = {**_efg_row("cat/src/game1"), "Format": "xyz"} + df = _make_df(row) + assert update._build_slug_tree(df) == {} + + def test_build_slug_tree_skips_empty_description(self): + """Rows with an empty description are excluded from the tree.""" + df = _make_df(_efg_row("cat/src/game1", description="")) + assert update._build_slug_tree(df) == {} + + +@pytest.mark.catalog_update +class TestHierarchicalRstOutput: + """Tests that ``generate_rst_table`` produces correctly nested dropdown RST.""" + + def _mock_generates(self, monkeypatch): + for name in ["generate_tex", "generate_png", "generate_pdf", "generate_svg"]: + monkeypatch.setattr(update, name, lambda *a, **k: None) + + def _write_hierarchy_yaml(self, tmp_path, content=_HIERARCHY_YAML): + yaml_file = tmp_path / "hier.yaml" + yaml_file.write_text(content, encoding="utf-8") + return yaml_file + + def test_top_level_dropdown_is_open(self, tmp_path, monkeypatch): + """Top-level category dropdowns carry ``:open:`` so the first level is visible.""" + self._mock_generates(monkeypatch) + hier_yaml = self._write_hierarchy_yaml(tmp_path) + monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", hier_yaml) + catalog_dir = tmp_path / "catalog" + slug = "cat/src/game1" + _make_image_files(catalog_dir, slug, "efg") + df = _make_df(_efg_row(slug)) + rst_path = tmp_path / "out.rst" + update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + rst = rst_path.read_text() + assert ".. dropdown:: My Category\n :open:" in rst + + def test_second_level_dropdown_is_not_open(self, tmp_path, monkeypatch): + """Sub-category dropdowns do NOT carry ``:open:`` so they are collapsed by default.""" + self._mock_generates(monkeypatch) + hier_yaml = self._write_hierarchy_yaml(tmp_path) + monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", hier_yaml) + catalog_dir = tmp_path / "catalog" + slug = "cat/src/game1" + _make_image_files(catalog_dir, slug, "efg") + df = _make_df(_efg_row(slug)) + rst_path = tmp_path / "out.rst" + update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + rst = rst_path.read_text() + assert " .. dropdown:: My Source\n \n" in rst + # Confirm :open: does not immediately follow the second-level dropdown + src_idx = rst.index(" .. dropdown:: My Source") + assert ":open:" not in rst[src_idx:src_idx + 40] + + def test_game_dropdown_is_open(self, tmp_path, monkeypatch): + """Individual game dropdowns carry ``:open:`` so game content is visible on expand.""" + self._mock_generates(monkeypatch) + hier_yaml = self._write_hierarchy_yaml(tmp_path) + monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", hier_yaml) + catalog_dir = tmp_path / "catalog" + slug = "cat/src/game1" + _make_image_files(catalog_dir, slug, "efg") + df = _make_df(_efg_row(slug, title="My Game Title")) + rst_path = tmp_path / "out.rst" + update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + rst = rst_path.read_text() + assert " .. dropdown:: My Game Title\n :open:" in rst + + def test_sibling_games_both_appear_under_source(self, tmp_path, monkeypatch): + """Two games sharing a source prefix both appear nested under the source dropdown.""" + self._mock_generates(monkeypatch) + hier_yaml = self._write_hierarchy_yaml(tmp_path) + monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", hier_yaml) + catalog_dir = tmp_path / "catalog" + for slug in ["cat/src/game1", "cat/src/game2"]: + _make_image_files(catalog_dir, slug, "efg") + df = _make_df( + _efg_row("cat/src/game1", title="Game One"), + _efg_row("cat/src/game2", title="Game Two"), + ) + rst_path = tmp_path / "out.rst" + update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + rst = rst_path.read_text() + assert ".. dropdown:: My Category" in rst + assert " .. dropdown:: My Source" in rst + assert "Game One" in rst + assert "Game Two" in rst + + def test_no_list_table_in_output(self, tmp_path, monkeypatch): + """The new output does not use ``.. list-table::`` (replaced by nested dropdowns).""" + self._mock_generates(monkeypatch) + hier_yaml = self._write_hierarchy_yaml(tmp_path) + monkeypatch.setattr(update, "CATALOG_HIERARCHY_CONFIG", hier_yaml) + catalog_dir = tmp_path / "catalog" + slug = "cat/src/game1" + _make_image_files(catalog_dir, slug, "efg") + df = _make_df(_efg_row(slug)) + rst_path = tmp_path / "out.rst" + update.generate_rst_table(df, rst_path, catalog_dir=catalog_dir) + rst = rst_path.read_text() + assert ".. list-table::" not in rst + assert ".. contents::" not in rst + + # --------------------------------------------------------------------------- # Tests for update_makefile # --------------------------------------------------------------------------- diff --git a/build_support/catalog/update.py b/build_support/catalog/update.py index 96c52f042..f61acc014 100644 --- a/build_support/catalog/update.py +++ b/build_support/catalog/update.py @@ -12,6 +12,7 @@ CATALOG_DIR = Path(__file__).parent.parent.parent / "catalog" MAKEFILE_AM = Path(__file__).parent.parent.parent / "Makefile.am" DRAW_TREE_SETTINGS_CONFIG = Path(__file__).parent / "draw_tree_settings.yaml" +CATALOG_HIERARCHY_CONFIG = Path(__file__).parent / "catalog_hierarchy.yaml" SUPPORTED_GAME_FORMATS = {"efg", "nfg"} @@ -57,200 +58,259 @@ def catalog_ef_file_variants(slug: str, catalog_dir: Path) -> list[dict] | None: variants = [] # Add the primary/default variant - variants.append({ - "label": "Default", - "ef_path": primary_ef, - "variant_key": slug, - }) + variants.append( + { + "label": "Default", + "ef_path": primary_ef, + "variant_key": slug, + } + ) # Add the additional suffix variants for ef_file in additional_efs: - suffix = ef_file.stem[len(stem) + 2:] + suffix = ef_file.stem[len(stem) + 2 :] # noqa: E203 label = suffix.replace("_", " ").title() variant_key = f"{slug}__{suffix}" variants.append({"label": label, "ef_path": ef_file, "variant_key": variant_key}) return variants +def load_hierarchy_labels() -> dict[str, str]: + """Return the human-readable label mapping for catalog hierarchy nodes.""" + with open(CATALOG_HIERARCHY_CONFIG, encoding="utf-8") as f: + config = yaml.safe_load(f) + return config.get("labels", {}) + + +def _node_label(prefix: str, labels: dict[str, str]) -> str: + """Return the display label for a catalog hierarchy node. + + Falls back to title-casing the last path component when the prefix is not + listed in the YAML config. + """ + if prefix in labels: + return labels[prefix] + return Path(prefix).name.replace("_", " ").title() + + +def _build_slug_tree(df: pd.DataFrame) -> dict: + """Build a nested dict tree from the slugs in *df*. + + Intermediate nodes are plain ``dict``s. Leaf nodes (individual games) store + the corresponding ``pd.Series`` row. Slugs are split on ``"/"`` to produce + the nesting. + """ + tree: dict = {} + for _, row in df.iterrows(): + if row.get("Format") not in SUPPORTED_GAME_FORMATS: + continue + if not str(row.get("Description", "")).strip(): + continue + parts = row["Game"].split("/") + node = tree + for part in parts[:-1]: + node = node.setdefault(part, {}) + node[parts[-1]] = row + return tree + + +def _write_game_entry( + f, + row: pd.Series, + slug: str, + catalog_dir: Path, + indent: str, + regenerate_images: bool = False, +) -> None: + """Write RST for a single game entry (dropdown + content) at *indent* depth.""" + i0 = indent # .. dropdown:: title line + i1 = indent + " " # content inside game dropdown + i2 = indent + " " # nested directives (Download, tab-set, jupyter-execute) + i3 = indent + " " # content inside nested directives + i4 = indent + " " # tab-item content + + title = str(row.get("Title", "")).strip() + description = str(row.get("Description", "")).strip() + + ef_variants = catalog_ef_file_variants(slug, catalog_dir) if row["Format"] == "efg" else None + + # ── Image generation ──────────────────────────────────────────────────── + if ef_variants: + _variant_img_exts = ["ef", "tex", "png", "pdf", "svg"] + for variant in ef_variants: + vkey = variant["variant_key"] + variant_paths = [catalog_dir / "img" / f"{vkey}.{ext}" for ext in _variant_img_exts] + if regenerate_images or not all(p.exists() for p in variant_paths): + viz_path = catalog_dir / "img" / vkey + viz_path.parent.mkdir(parents=True, exist_ok=True) + source = ( + str(variant["ef_path"]) + if variant["ef_path"].exists() + else gbt.catalog.load(slug) + ) + for func in [generate_tex, generate_png, generate_pdf, generate_svg]: + func(source, save_to=str(viz_path), **catalog_draw_tree_settings(vkey)) + img_ef = catalog_dir / "img" / f"{vkey}.ef" + if not img_ef.exists() and variant["ef_path"].exists(): + shutil.copy2(variant["ef_path"], img_ef) + else: + all_exts = [] + all_paths = [] + if row["Format"] == "efg": + all_exts.append("ef") + all_paths.append(catalog_dir / "img" / f"{slug}.ef") + all_exts += ["tex", "png", "pdf", "svg"] + for ext in ["tex", "png", "pdf", "svg"]: + all_paths.append(catalog_dir / "img" / f"{slug}.{ext}") + if regenerate_images or not all(p.exists() for p in all_paths): + viz_path = catalog_dir / "img" / slug + viz_path.parent.mkdir(parents=True, exist_ok=True) + curated_ef = catalog_dir / f"{slug}.ef" + source = str(curated_ef) if curated_ef.exists() else gbt.catalog.load(slug) + for func in [generate_tex, generate_png, generate_pdf, generate_svg]: + func(source, save_to=str(viz_path), **catalog_draw_tree_settings(slug)) + img_ef = catalog_dir / "img" / f"{slug}.ef" + if not img_ef.exists() and curated_ef.exists(): + shutil.copy2(curated_ef, img_ef) + + # ── RST output ────────────────────────────────────────────────────────── + f.write(f"{i0}.. dropdown:: {title}\n") + f.write(f"{i0} :open:\n") + f.write(f"{i0}\n") + for line in description.splitlines(): + f.write(f"{i1}{line}\n") + f.write(f"{i1}\n") + f.write(f"{i1}**Load in PyGambit:**\n") + f.write(f"{i1}\n") + f.write(f"{i1}.. code-block:: python\n") + f.write(f"{i1} \n") + f.write(f'{i1} pygambit.catalog.load("{slug}")\n') + f.write(f"{i1}\n") + + # Download links + download_links = [row["Download"]] + if ef_variants: + for variant in ef_variants: + vkey = variant["variant_key"] + for ext in ["ef", "tex", "png", "pdf", "svg"]: + download_links.append(f":download:`{vkey}.{ext} <../catalog/img/{vkey}.{ext}>`") + else: + for ext in all_exts: + download_links.append(f":download:`{slug}.{ext} <../catalog/img/{slug}.{ext}>`") + f.write(f"{i1}.. dropdown:: Download game and image files\n") + f.write(f"{i1} \n") + f.write(f"{i2}{' '.join(download_links)}\n") + f.write(f"{i1}\n") + + # Visualization + if ef_variants: + f.write(f"{i1}.. tab-set::\n") + f.write(f"{i1}\n") + for variant in ef_variants: + label = variant["label"] + vkey = variant["variant_key"] + settings_str = ", ".join( + f"{k}={v!r}" for k, v in catalog_draw_tree_settings(vkey).items() + ) + f.write(f"{i2}.. tab-item:: {label}\n") + f.write(f"{i2}\n") + f.write(f"{i3}.. jupyter-execute::\n") + f.write(f"{i3} :hide-code:\n") + f.write(f"{i3} \n") + f.write(f"{i4}import pygambit\n") + f.write(f"{i4}from draw_tree import draw_tree\n") + if variant["ef_path"].exists(): + f.write(f'{i4}draw_tree("../catalog/{vkey}.ef", {settings_str})\n') + else: + f.write(f'{i4}draw_tree(pygambit.catalog.load("{slug}"), {settings_str})\n') + f.write(f"{i2}\n") + f.write(f"{i1}\n") + else: + f.write(f"{i1}.. jupyter-execute::\n") + f.write(f"{i1} :hide-code:\n") + f.write(f"{i1} \n") + f.write(f"{i2}import pygambit\n") + f.write(f"{i2}from draw_tree import draw_tree\n") + if row["Format"] == "efg": + settings_str = ", ".join( + f"{k}={v!r}" for k, v in catalog_draw_tree_settings(slug).items() + ) + curated_ef = catalog_dir / f"{slug}.ef" + if curated_ef.exists(): + f.write(f'{i2}draw_tree("../catalog/{slug}.ef", {settings_str})\n') + else: + f.write(f'{i2}draw_tree(pygambit.catalog.load("{slug}"), {settings_str})\n') + elif row["Format"] == "nfg": + f.write( + f'{i2}draw_tree(pygambit.catalog.load("{slug}"), ' + f'save_to="../catalog/img/{slug}.png")\n' + ) + f.write(f"{i1}\n") + + +def _write_tree_level( + f, + subtree: dict, + path_prefix: str, + labels: dict[str, str], + catalog_dir: Path, + indent: str, + regenerate_images: bool = False, +) -> None: + """Recursively write RST nested dropdowns for *subtree*. + + Intermediate nodes (``dict`` values) become collapsible ``.. dropdown::`` + sections. Top-level nodes (those whose ``path_prefix`` contains no ``/``) + are rendered open by default so the first level of the hierarchy is + immediately visible. Leaf nodes (``pd.Series`` values) are individual + games rendered via :func:`_write_game_entry`. + """ + for key in sorted(subtree): + value = subtree[key] + child_prefix = f"{path_prefix}/{key}" if path_prefix else key + + if isinstance(value, dict): + label = _node_label(child_prefix, labels) + is_top_level = "/" not in child_prefix + f.write(f"{indent}.. dropdown:: {label}\n") + if is_top_level: + f.write(f"{indent} :open:\n") + f.write(f"{indent}\n") + _write_tree_level( + f, + value, + child_prefix, + labels, + catalog_dir, + indent + " ", + regenerate_images=regenerate_images, + ) + f.write(f"{indent}\n") + else: + _write_game_entry( + f, + value, + child_prefix, + catalog_dir, + indent, + regenerate_images=regenerate_images, + ) + + def generate_rst_table( df: pd.DataFrame, rst_path: Path, regenerate_images: bool = False, catalog_dir: Path | None = None, ): - """Generate RST output with a list-table for games.""" + """Generate RST output with nested dropdowns grouped by catalog hierarchy.""" catalog_dir = catalog_dir or CATALOG_DIR + labels = load_hierarchy_labels() + tree = _build_slug_tree(df) with open(rst_path, "w", encoding="utf-8") as f: - # TOC linking to both sections - f.write(".. contents::\n") - f.write(" :local:\n") - f.write(" :depth: 1\n") - f.write("\n") - f.write(".. list-table::\n") - f.write(" :header-rows: 1\n") - f.write(" :widths: 100\n") - f.write(" :class: tight-table\n") - f.write("\n") - f.write(" * - **Catalog of games**\n") - - for _, row in df.iterrows(): - slug = row["Game"] - title = str(row.get("Title", "")).strip() - description = str(row.get("Description", "")).strip() - # Skip rows with unrecognised formats (defensive guard). - if row["Format"] not in SUPPORTED_GAME_FORMATS: - continue - # Skip any games which lack a description - if not description: - continue - - # Detect whether this EFG has multiple curated .ef layout variants. - ef_variants = ( - catalog_ef_file_variants(slug, catalog_dir) if row["Format"] == "efg" else None - ) - - if ef_variants: - # Multi-variant: generate one image set per variant - _variant_img_exts = ["ef", "tex", "png", "pdf", "svg"] - for variant in ef_variants: - vkey = variant["variant_key"] - variant_paths = [ - catalog_dir / "img" / f"{vkey}.{ext}" for ext in _variant_img_exts - ] - if regenerate_images or not all(p.exists() for p in variant_paths): - viz_path = catalog_dir / "img" / vkey - viz_path.parent.mkdir(parents=True, exist_ok=True) - if variant["ef_path"].exists(): - source = str(variant["ef_path"]) - else: - source = gbt.catalog.load(slug) - for func in [generate_tex, generate_png, generate_pdf, generate_svg]: - func( - source, - save_to=str(viz_path), - **catalog_draw_tree_settings(vkey), - ) - img_ef = catalog_dir / "img" / f"{vkey}.ef" - if not img_ef.exists() and variant["ef_path"].exists(): - shutil.copy2(variant["ef_path"], img_ef) - else: - # Single variant - all_exts = [] - all_paths = [] - if row["Format"] == "efg": - all_exts.append("ef") - all_paths.append(catalog_dir / "img" / f"{slug}.ef") - all_exts += ["tex", "png", "pdf", "svg"] - for ext in ["tex", "png", "pdf", "svg"]: - all_paths.append(catalog_dir / "img" / f"{slug}.{ext}") - missing_any = not all(p.exists() for p in all_paths) - - if regenerate_images or missing_any: - viz_path = catalog_dir / "img" / slug - viz_path.parent.mkdir(parents=True, exist_ok=True) - # Use a committed curated .ef file if present; otherwise derive - # the layout automatically from the game object. - curated_ef = catalog_dir / f"{slug}.ef" - source = str(curated_ef) if curated_ef.exists() else gbt.catalog.load(slug) - for func in [generate_tex, generate_png, generate_pdf, generate_svg]: - func(source, save_to=str(viz_path), **catalog_draw_tree_settings(slug)) - # DrawTree may not write catalog/img/{slug}.ef when its input is - # already an .ef file, so copy it if the img copy is still absent. - img_ef = catalog_dir / "img" / f"{slug}.ef" - if not img_ef.exists() and curated_ef.exists(): - shutil.copy2(curated_ef, img_ef) - - # 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") - f.write(" **Load in PyGambit:**\n") - f.write(" \n") - f.write(" .. code-block:: python\n") - f.write(" \n") - f.write(f' pygambit.catalog.load("{slug}")\n') - f.write(" \n") - - # Download links - download_links = [row["Download"]] - if ef_variants: - _variant_img_exts = ["ef", "tex", "png", "pdf", "svg"] - for variant in ef_variants: - vkey = variant["variant_key"] - for ext in _variant_img_exts: - download_links.append( - f":download:`{vkey}.{ext} <../catalog/img/{vkey}.{ext}>`" - ) - else: - for ext in all_exts: - download_links.append( - f":download:`{slug}.{ext} <../catalog/img/{slug}.{ext}>`" - ) - f.write(" .. dropdown:: Download game and image files\n") - f.write(" \n") - f.write(f" {' '.join(download_links)}\n") - f.write(" \n") - - # Draw image — tab-set for multiple variants, single block otherwise - if ef_variants: - f.write(" .. tab-set::\n") - f.write(" \n") - for variant in ef_variants: - label = variant["label"] - vkey = variant["variant_key"] - settings = catalog_draw_tree_settings(vkey) - settings_str = ", ".join(f"{k}={val!r}" for k, val in settings.items()) - f.write(f" .. tab-item:: {label}\n") - f.write(" \n") - f.write(" .. jupyter-execute::\n") - f.write(" :hide-code:\n") - f.write(" \n") - f.write(" import pygambit\n") - f.write(" from draw_tree import draw_tree\n") - if variant["ef_path"].exists(): - f.write( - " draw_tree(" - f'"../catalog/{vkey}.ef", {settings_str})\n' - ) - else: - f.write( - f" draw_tree(" - f'pygambit.catalog.load("{slug}"), ' - f"{settings_str})\n" - ) - f.write(" \n") - f.write(" \n") - else: - f.write(" .. jupyter-execute::\n") - f.write(" :hide-code:\n") - f.write(" \n") - f.write(" import pygambit\n") - f.write(" from draw_tree import draw_tree\n") - if row["Format"] == "efg": - settings = catalog_draw_tree_settings(slug) - settings_str = ", ".join(f"{k}={val!r}" for k, val in settings.items()) - curated_ef = catalog_dir / f"{slug}.ef" - if curated_ef.exists(): - f.write( - f' draw_tree("../catalog/{slug}.ef", {settings_str})\n' - ) - else: - f.write( - f" draw_tree(" - f'pygambit.catalog.load("{slug}"), ' - f"{settings_str})\n" - ) - elif row["Format"] == "nfg": - f.write( - f" draw_tree(" - f'pygambit.catalog.load("{slug}"), ' - f'save_to="../catalog/img/{slug}.png")\n' - ) - f.write(" \n") + _write_tree_level( + f, tree, "", labels, catalog_dir, indent="", regenerate_images=regenerate_images + ) def update_makefile( diff --git a/doc/developer.catalog.rst b/doc/developer.catalog.rst index 1db84ff7a..68f0f442e 100644 --- a/doc/developer.catalog.rst +++ b/doc/developer.catalog.rst @@ -37,11 +37,33 @@ Currently supported representations are: Create a new branch in the ``gambit`` repo. Add your new game file(s) inside the ``catalog`` dir and commit them, or edit an existing game. - If there are multiple games from a particular source, place them in an appropriately named folder. + The catalog uses a hierarchical folder structure that groups games by publication type and venue: + + .. code-block:: text + + catalog/ + books/{author-year}/{game}.efg # games from textbooks + journals/{venue}/{author-year}/{game}.efg # games from journals (venue = geb, ijgt, mor, …) + conf/{venue}/{author-year}/{game}.efg # games from conferences + + The folder path determines the game's slug, used by the load function: + + .. code-block:: python + + pygambit.catalog.load("books/watson2013/exercise29_6") + pygambit.catalog.load("journals/geb/bagwell1995") + + .. note:: + + When adding a game from a **new** journal, conference, or other top-level category, + add a human-readable label for the new hierarchy node(s) to + ``build_support/catalog/catalog_hierarchy.yaml``. + The catalog documentation page groups and labels games based on this file. + Nodes without an entry fall back to a title-cased version of the folder name. .. important:: - The name of the game file will determine it's "slug", used by the load function of the catalog module: + The name of the game file will determine its "slug", used by the load function of the catalog module: .. code-block:: python