Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions src/games/gameagg.cc
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ GameAGGRep::GameAGGRep(std::shared_ptr<agg::AGG> p_aggPtr) : aggPtr(p_aggPtr)
s->m_label = std::to_string(st++);
});
}
IndexStrategies();
}

Game GameAGGRep::Copy() const
Expand Down
1 change: 1 addition & 0 deletions src/games/gamebagg.cc
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ GameBAGGRep::GameBAGGRep(std::shared_ptr<agg::BAGG> _baggPtr)
});
}
}
IndexStrategies();
}

Game GameBAGGRep::Copy() const
Expand Down
3 changes: 2 additions & 1 deletion src/pygambit/gambit.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -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 +
Expand Down
4 changes: 2 additions & 2 deletions src/pygambit/game.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
14 changes: 8 additions & 6 deletions src/pygambit/outcome.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand All @@ -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
Expand All @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions tests/games.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Expand Down
60 changes: 52 additions & 8 deletions tests/test_nash.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
),
]


Expand Down Expand Up @@ -232,6 +245,23 @@ 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",
),
]


Expand Down Expand Up @@ -589,6 +619,25 @@ 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",
),
]


Expand Down Expand Up @@ -2158,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,
Expand All @@ -2177,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)]],
Expand Down
Loading