From 850327c86efa1e4785fb6668a75fd8e2cc078fd9 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Mon, 27 Apr 2026 15:41:14 +0100 Subject: [PATCH 1/4] Fix regression in action graph games --- ChangeLog | 7 +++++++ src/games/gameagg.cc | 1 + src/games/gamebagg.cc | 1 + 3 files changed, 9 insertions(+) diff --git a/ChangeLog b/ChangeLog index 4368c739ec..1767b272c5 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,12 @@ # Changelog +## [16.7.0] - unreleased + +### Fixed +- Corrected a regression in action graph games that left the internal data structure not fully initialised, + leading to segmentation faults. + + ## [16.6.0] - 2026-03-24 ### Changed diff --git a/src/games/gameagg.cc b/src/games/gameagg.cc index 6341459c40..a702e8b011 100644 --- a/src/games/gameagg.cc +++ b/src/games/gameagg.cc @@ -186,6 +186,7 @@ GameAGGRep::GameAGGRep(std::shared_ptr p_aggPtr) : aggPtr(p_aggPtr) s->m_label = std::to_string(st++); }); } + IndexStrategies(); } Game GameAGGRep::Copy() const diff --git a/src/games/gamebagg.cc b/src/games/gamebagg.cc index dee69915a9..fea3bb695d 100644 --- a/src/games/gamebagg.cc +++ b/src/games/gamebagg.cc @@ -223,6 +223,7 @@ GameBAGGRep::GameBAGGRep(std::shared_ptr _baggPtr) }); } } + IndexStrategies(); } Game GameBAGGRep::Copy() const From 1bb0263fae66f0f20baf391b22344249411e995b Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Wed, 13 May 2026 20:14:45 +0100 Subject: [PATCH 2/4] tests for solving agg -- problem with max_regret (or lcp and enummixed solvers) --- tests/games.py | 2 ++ tests/test_nash.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/tests/games.py b/tests/games.py index 312854b43d..28870d1a77 100644 --- a/tests/games.py +++ b/tests/games.py @@ -14,6 +14,8 @@ def read_from_file(fn: str) -> gbt.Game: return gbt.read_efg(pathlib.Path("tests/test_games") / fn) elif fn.endswith(".nfg"): return gbt.read_nfg(pathlib.Path("tests/test_games") / fn) + elif fn.endswith(".agg"): + return gbt.read_agg(pathlib.Path("tests/test_games") / fn) else: raise ValueError(f"Unknown file extension in {fn}") diff --git a/tests/test_nash.py b/tests/test_nash.py index d0e664cd64..bf3f996583 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -159,6 +159,19 @@ class QREquilibriumTestCase: marks=pytest.mark.nash_enumpure_strategy, id="test_enumpure_8", ), + # Action graph game + pytest.param( + EquilibriumTestCase( + factory=functools.partial(games.read_from_file, "2x2.agg"), + solver=functools.partial(gbt.nash.enumpure_solve), + expected=[ + [d(1, 0), d(1, 0)], + [d(0, 1), d(0, 1)], + ], + ), + marks=pytest.mark.nash_enumpure_strategy, + id="test_enumpure_9", + ), ] @@ -232,6 +245,21 @@ class QREquilibriumTestCase: marks=pytest.mark.nash_enummixed_strategy, id="test_enumixed_rational_5", ), + # Action graph game + pytest.param( + EquilibriumTestCase( + factory=functools.partial(games.read_from_file, "2x2.agg"), + solver=functools.partial(gbt.nash.enummixed_solve, rational=True), + expected=[ + [d(1, 0), d(1, 0)], + [d(0, 1), d(0, 1)], + [d("4186770418979088/4641467073735727", "454696654756639/4641467073735727"), + d("4186770418979088/4641467073735727", "454696654756639/4641467073735727")] + ], + ), + marks=pytest.mark.nash_enummixed_strategy, + id="test_enummixed_rational_6", + ), ] @@ -589,6 +617,23 @@ class QREquilibriumTestCase: marks=pytest.mark.nash_lcp_strategy, id="test_lcp_strategy_rational_11", ), + # Action graph game + pytest.param( + EquilibriumTestCase( + factory=functools.partial(games.read_from_file, "2x2.agg"), + solver=functools.partial( + gbt.nash.lcp_solve, rational=True, use_strategic=True, stop_after=None + ), + expected=[ + [d(1, 0), d(1, 0)], + [d("4186770418979088/4641467073735727", "454696654756639/4641467073735727"), + d("4186770418979088/4641467073735727", "454696654756639/4641467073735727")], + [d(0, 1), d(0, 1)] + ], + ), + marks=pytest.mark.nash_lcp_strategy, + id="test_lcp_strategy_rational_12", + ), ] From 2a7ec1a8e0edcd9afe2b6d92cb40b0d38425aff0 Mon Sep 17 00:00:00 2001 From: Rahul Savani Date: Thu, 14 May 2026 05:49:39 +0100 Subject: [PATCH 3/4] linting --- tests/test_nash.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/tests/test_nash.py b/tests/test_nash.py index bf3f996583..50491ef399 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -253,8 +253,10 @@ class QREquilibriumTestCase: expected=[ [d(1, 0), d(1, 0)], [d(0, 1), d(0, 1)], - [d("4186770418979088/4641467073735727", "454696654756639/4641467073735727"), - d("4186770418979088/4641467073735727", "454696654756639/4641467073735727")] + [ + d("4186770418979088/4641467073735727", "454696654756639/4641467073735727"), + d("4186770418979088/4641467073735727", "454696654756639/4641467073735727"), + ], ], ), marks=pytest.mark.nash_enummixed_strategy, @@ -626,9 +628,11 @@ class QREquilibriumTestCase: ), expected=[ [d(1, 0), d(1, 0)], - [d("4186770418979088/4641467073735727", "454696654756639/4641467073735727"), - d("4186770418979088/4641467073735727", "454696654756639/4641467073735727")], - [d(0, 1), d(0, 1)] + [ + d("4186770418979088/4641467073735727", "454696654756639/4641467073735727"), + d("4186770418979088/4641467073735727", "454696654756639/4641467073735727"), + ], + [d(0, 1), d(0, 1)], ], ), marks=pytest.mark.nash_lcp_strategy, @@ -2203,16 +2207,13 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s # 3-player perfect info game to test behavior two off equilibrium path pytest.param( EquilibriumTestCase( - factory=functools.partial( - games.read_from_file, "3_player_PI_2_dev_off_eq_path.efg" - ), + factory=functools.partial(games.read_from_file, "3_player_PI_2_dev_off_eq_path.efg"), solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), expected=[ # candidate,10,10,1000,10000 [[d(1, 0)], [d(1, 0), d(1, 0, 0, 0)], [d(1, 0, 0, 0, 0)]], # candidate,01,00,0000,00000 - [[d(0, 1)], [d(1, 0), d(1, 0, 0, 0)], - [d(1, 0, 0, 0, 0)]], + [[d(0, 1)], [d(1, 0), d(1, 0, 0, 0)], [d(1, 0, 0, 0, 0)]], ], regret_tol=TOL, prob_tol=TOL, @@ -2222,9 +2223,7 @@ def test_nash_strategy_solver_w_start(test_case: EquilibriumTestCaseWithStart, s ), pytest.param( EquilibriumTestCase( - factory=functools.partial( - games.read_from_file, "3_player_PI_2_dev_off_eq_path.efg" - ), + factory=functools.partial(games.read_from_file, "3_player_PI_2_dev_off_eq_path.efg"), solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None), expected=[ [[d(1, 0)], [d(1, 0), d(1, 0, 0, 0)], [d(1, 0, 0, 0, 0)]], From 3dbe672c42464b768af33241744c943c744f6fec Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Thu, 14 May 2026 14:00:47 +0100 Subject: [PATCH 4/4] Implement outcomes in Python for action graph games. --- src/pygambit/gambit.pxd | 3 ++- src/pygambit/game.pxi | 4 ++-- src/pygambit/outcome.pxi | 14 ++++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd index 95ae672957..8a95315f22 100644 --- a/src/pygambit/gambit.pxd +++ b/src/pygambit/gambit.pxd @@ -249,7 +249,8 @@ cdef extern from "games/game.h": iterator begin() except + iterator end() except + - int IsTree() except + + bool IsTree() except + + bool IsAgg() except + string GetTitle() except + void SetTitle(string) except + diff --git a/src/pygambit/game.pxi b/src/pygambit/game.pxi index 7bee9a4470..011321fd49 100644 --- a/src/pygambit/game.pxi +++ b/src/pygambit/game.pxi @@ -858,8 +858,8 @@ class Game: self.game.deref().GetPlayer(pl+1).deref().GetStrategy(st+1) ) - if self.is_tree: - return TreeGameOutcome.wrap(self.game, psp) + if self.is_tree or self.game.deref().IsAgg(): + return DerivedGameOutcome.wrap(self.game, psp) else: outcome = Outcome.wrap(deref(deref(psp).deref()).GetOutcome()) if outcome.outcome != cython.cast(c_GameOutcome, NULL): diff --git a/src/pygambit/outcome.pxi b/src/pygambit/outcome.pxi index 06ff80a0e8..6f3e62f232 100644 --- a/src/pygambit/outcome.pxi +++ b/src/pygambit/outcome.pxi @@ -126,8 +126,10 @@ class Outcome: @cython.cclass -class TreeGameOutcome: - """Represents an outcome in a strategic game derived from an extensive game.""" +class DerivedGameOutcome: + """Represents an outcome in a strategic game derived from a game in another representation. + Such outcomes are one-to-one with the set of pure strategy profiles. + """ c_game = cython.declare(c_Game) psp = cython.declare(shared_ptr[c_PureStrategyProfile]) @@ -136,8 +138,8 @@ class TreeGameOutcome: @staticmethod @cython.cfunc - def wrap(game: c_Game, psp: shared_ptr[c_PureStrategyProfile]) -> TreeGameOutcome: - obj: TreeGameOutcome = TreeGameOutcome.__new__(TreeGameOutcome) + def wrap(game: c_Game, psp: shared_ptr[c_PureStrategyProfile]) -> DerivedGameOutcome: + obj: DerivedGameOutcome = DerivedGameOutcome.__new__(DerivedGameOutcome) obj.c_game = game obj.psp = psp return obj @@ -152,8 +154,8 @@ class TreeGameOutcome: def __eq__(self, other: typing.Any) -> bool: return ( - isinstance(other, TreeGameOutcome) and - deref(self.psp).deref() == deref(cython.cast(TreeGameOutcome, other).psp).deref() + isinstance(other, DerivedGameOutcome) and + deref(self.psp).deref() == deref(cython.cast(DerivedGameOutcome, other).psp).deref() ) def __getitem__(self, player: Player | str) -> Rational: