From 6085e9dd68aac12142a6252631d8e138cfcee23a Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Fri, 3 Jul 2026 11:29:40 +0100
Subject: [PATCH 01/32] docs: add wyz2368 as a contributor for code, and
research (#970)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 10 ++++++++++
README.md | 1 +
2 files changed, 11 insertions(+)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 3b8675da2..02f07de69 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -73,6 +73,16 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "wyz2368",
+ "name": "wyz2368",
+ "avatar_url": "https://avatars.githubusercontent.com/u/25018133?v=4",
+ "profile": "https://github.com/wyz2368",
+ "contributions": [
+ "code",
+ "research"
+ ]
}
]
}
diff --git a/README.md b/README.md
index 69ed98074..288c52458 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,7 @@ installable via PyPI.
 StephenPasteris 🔬 💻 |
 Daniel Kadnikov 🔬 💻 |
 Andrés Fernández Cervell 💻 |
+  wyz2368 💻 🔬 |
From 5482aaae9936cc53141e498ff2d0c65ead68584d Mon Sep 17 00:00:00 2001
From: Daniel Kadnikov <165307096+d-kad@users.noreply.github.com>
Date: Fri, 3 Jul 2026 17:40:36 +0100
Subject: [PATCH 02/32] Reject malformed AGG/BAGG headers before construction
(#971)
Fixes a segfault in the AGG/BAGG file readers on malformed input.
Malformed input (e.g. an `.nfg` to `read_bagg`, or a degenerate `"0 0 0"` header)
slipped past the header checks and dereferenced an empty vector in the AGG constructor, segfaulting.
The parser code is unchanged from `master` and runs before any label normalization.
This validates header fields before construction; malformed headers now raise `ValueError`, valid files are unaffected.
Tests added: `test_read_agg_zero_header` / `test_read_bagg_zero_header` read `"0 0 0\n"` and expect `ValueError`. These segfault on unpatched code (taking down the `pytest` process, as in CI) rather than failing as an assertion.
---
ChangeLog | 3 +++
src/games/agg/agg.cc | 4 ++++
src/games/agg/bagg.cc | 22 +++++++++++++---------
tests/test_io.py | 10 ++++++++++
4 files changed, 30 insertions(+), 9 deletions(-)
diff --git a/ChangeLog b/ChangeLog
index bca1fd127..db414c5a8 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -25,6 +25,9 @@
- Corrected calculation of total number of actions for a player in `pygambit` (#938)
- Corrected a regression in action graph games that left the internal data structure not fully initialised,
leading to segmentation faults.
+- Corrected handling of malformed AGG/BAGG files: files with an invalid or degenerate header
+ (for example a wrong file type, or a header declaring zero players) are now rejected with a
+ `ValueError` instead of causing a segmentation fault while constructing the game.
### Changed
- Added a new welcome/landing window on launching the GUI without a game. This has the effect of
diff --git a/src/games/agg/agg.cc b/src/games/agg/agg.cc
index f5f1a2ba8..91e495c88 100644
--- a/src/games/agg/agg.cc
+++ b/src/games/agg/agg.cc
@@ -152,6 +152,10 @@ std::shared_ptr AGG::makeAGG(istream &in)
if (!in.good()) {
throw std::runtime_error("Error reading the number of function nodes");
}
+ if (n <= 0 || S < 0 || P < 0) {
+ throw std::runtime_error("Error in game file: invalid AGG header (number of players, "
+ "action nodes, or function nodes out of range)");
+ }
stripComment(in);
// enter sizes of action sets:
diff --git a/src/games/agg/bagg.cc b/src/games/agg/bagg.cc
index ab58e63b6..12d36de06 100644
--- a/src/games/agg/bagg.cc
+++ b/src/games/agg/bagg.cc
@@ -73,18 +73,22 @@ std::shared_ptr BAGG::makeBAGG(istream &in)
in >> S;
stripComment(in);
in >> P;
+ if (!in || N <= 0 || S < 0 || P < 0) {
+ throw std::runtime_error("Error in game file: expected BAGG header with number of "
+ "players, action nodes, and function nodes");
+ }
stripComment(in);
vector numTypes(N);
// input number of types for each player
for (int i = 0; i < N; ++i) {
- if (in.eof() || in.bad()) {
+ in >> numTypes[i];
+ if (!in) {
throw std::runtime_error(
"Error in game file: integer expected for the number of types for player " +
std::to_string(i));
}
- in >> numTypes[i];
}
// input the type distributions
@@ -93,10 +97,10 @@ std::shared_ptr BAGG::makeBAGG(istream &in)
for (int i = 0; i < N; ++i) {
TDist.emplace_back(numTypes[i]);
for (int j = 0; j < numTypes[i]; ++j) {
- if (in.eof() || in.bad()) {
+ in >> TDist[i][j];
+ if (!in) {
throw std::runtime_error("Error in game file: number expected for type distribution");
}
- in >> TDist[i][j];
}
}
@@ -105,12 +109,12 @@ std::shared_ptr BAGG::makeBAGG(istream &in)
vector>> typeActionSets(N);
for (int i = 0; i < N; ++i) {
for (int j = 0; j < numTypes[i]; ++j) {
- if (in.eof() || in.bad()) {
+ int temp;
+ in >> temp;
+ if (!in || temp < 0) {
throw std::runtime_error(
"Error in game file: integer expected for size of type action set");
}
- int temp;
- in >> temp;
typeActionSets[i].emplace_back(temp);
}
}
@@ -120,10 +124,10 @@ std::shared_ptr BAGG::makeBAGG(istream &in)
for (int i = 0; i < N; ++i) {
for (int j = 0; j < numTypes[i]; ++j) {
for (int &el : typeActionSets[i][j]) {
- if (in.eof() || in.bad()) {
+ in >> el;
+ if (!in) {
throw std::runtime_error("Error in game file: integer expected for type action set");
}
- in >> el;
}
}
}
diff --git a/tests/test_io.py b/tests/test_io.py
index e0107f23a..826e7fc14 100644
--- a/tests/test_io.py
+++ b/tests/test_io.py
@@ -49,6 +49,11 @@ def test_read_agg_invalid():
gbt.read_agg(game_path)
+def test_read_agg_zero_header():
+ with pytest.raises(ValueError):
+ gbt.read_agg(io.StringIO("0 0 0\n"))
+
+
def test_read_bagg():
game_path = os.path.join("contrib", "games", "Bayesian-Coffee-3-2-2-3.bagg")
game = gbt.read_bagg(game_path)
@@ -63,6 +68,11 @@ def test_read_bagg_invalid():
gbt.read_bagg(game_path)
+def test_read_bagg_zero_header():
+ with pytest.raises(ValueError):
+ gbt.read_bagg(io.StringIO("0 0 0\n"))
+
+
def test_read_gbt_invalid():
game_path = os.path.join(
"tests", "test_games", "2x2x2_nfg_from_local_max_cut_2_pure_1_mixed_eq.nfg"
From 39deff0fa3863980b572037702457c7db02b509f Mon Sep 17 00:00:00 2001
From: ameliekleber <83069628+ameliekleber@users.noreply.github.com>
Date: Fri, 3 Jul 2026 22:41:25 +0200
Subject: [PATCH 03/32] Add enumpoly solver strategic-form tests (#969)
* Added 3 player max cut test for enumpoly strategic form
* added strategic game tests for enumpoly strategic form: 3x3 coordination game, 3-player unique Nash (nau2004 sec4), 2x2x2 3 pure Nash (nau2004 sec5)
---
pyproject.toml | 1 +
...g_from_local_max_cut_2_pure_1_mixed_eq.nfg | 1 +
tests/test_nash.py | 79 +++++++++++++++++++
3 files changed, 81 insertions(+)
diff --git a/pyproject.toml b/pyproject.toml
index 1b36d50ef..e2e4e55f2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -102,6 +102,7 @@ markers = [
"nash_enumpure_strategy: tests of enumpure_solve in pure strategies",
"nash_enumpure_agent: tests of enumpure_solve in pure behaviors",
"nash_enummixed_strategy: tests of enummixed_solve in mixed strategies",
+ "nash_enumpoly_strategy: tests of enumpoly_solve in mixed strategies",
"nash_enumpoly_behavior: tests of enumpoly_solve in mixed behaviors",
"nash_lcp_strategy: tests of lcp_solve in mixed strategies",
"nash_lcp_behavior: tests of lcp_solve in mixed behaviors",
diff --git a/tests/test_games/2x2x2_nfg_from_local_max_cut_2_pure_1_mixed_eq.nfg b/tests/test_games/2x2x2_nfg_from_local_max_cut_2_pure_1_mixed_eq.nfg
index f28f234f6..f1958f904 100644
--- a/tests/test_games/2x2x2_nfg_from_local_max_cut_2_pure_1_mixed_eq.nfg
+++ b/tests/test_games/2x2x2_nfg_from_local_max_cut_2_pure_1_mixed_eq.nfg
@@ -6,6 +6,7 @@ NFG 1 R "2x2x2 game with 2 pure and 1 mixed equilibrium"
- Pure strategies {a,b} encode if respective player is on left or right of the cut
- The payoff to a player is the sum of their incident edges across the implied cut
- Pure equilibrium iff local max cuts; in addition, uniform mixture is an equilibrium
+- In the mixed equilibrium all players mix strategies with equal probability (0.5, 0.5)
- Equilibrium analysis for pure profiles:
a a a: 0 0 0 -- Not Nash (regrets: 1, 4, 1)
b a a: 1 2 -1 -- Not Nash (regrets: 0, 0, 3)
diff --git a/tests/test_nash.py b/tests/test_nash.py
index a3b171d0f..065f7fb28 100644
--- a/tests/test_nash.py
+++ b/tests/test_nash.py
@@ -411,6 +411,84 @@ class QREquilibriumTestCase:
]
+ENUMPOLY_STRATEGY_CASES = [
+ # 2x2x2 strategic form game based on local max cut -- 2 pure and 1 mixed
+ pytest.param(
+ EquilibriumTestCase(
+ factory=functools.partial(
+ games.read_from_file, "2x2x2_nfg_from_local_max_cut_2_pure_1_mixed_eq.nfg"
+ ),
+ solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None),
+ expected=[
+ [d(1, 0), d(0, 1), d(1, 0)],
+ [d(0, 1), d(1, 0), d(0, 1)],
+ [d("1/2", "1/2"), d("1/2", "1/2"), d("1/2", "1/2")],
+ ],
+ prob_tol=TOL,
+ regret_tol=TOL,
+ ),
+ marks=pytest.mark.nash_enumpoly_strategy,
+ id="test_enumpoly_strategy_1",
+ ),
+ # coordination game with 3 pure and 4 mixed equilibria
+ pytest.param(
+ EquilibriumTestCase(
+ factory=functools.partial(games.create_EFG_for_nxn_bimatrix_coordination_game, n=3),
+ solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None, use_strategic=True),
+ expected=[
+ [d(1, 0, 0), d(1, 0, 0)],
+ [d(0, 1, 0), d(0, 1, 0)],
+ [d(0, 0, 1), d(0, 0, 1)],
+ [d("1/2", "1/2", 0), d("1/2", "1/2", 0)],
+ [d("1/2", 0, "1/2"), d("1/2", 0, "1/2")],
+ [d(0, "1/2", "1/2"), d(0, "1/2", "1/2")],
+ [d("1/3", "1/3", "1/3"), d("1/3", "1/3", "1/3")],
+ ],
+ prob_tol=TOL,
+ regret_tol=TOL,
+ ),
+ marks=pytest.mark.nash_enumpoly_strategy,
+ id="test_enumpoly_strategy_2",
+ ),
+ # A three-player game with a unique Nash equilibrium in irrational mixed strategies
+ # (nau2004 sec4 catalog game)
+ pytest.param(
+ EquilibriumTestCase(
+ factory=functools.partial(gbt.catalog.load, "journals/ijgt/nau2004/sec4"),
+ solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None),
+ expected=[
+ [
+ d(0.6192325794725537, 0.3807674205274463),
+ d(0.4798042226776053, 0.5201957773223946),
+ d(0.3788253360656313, 0.6211746639343687)
+ ],
+ ],
+ prob_tol=TOL,
+ regret_tol=TOL,
+ ),
+ marks=pytest.mark.nash_enumpoly_strategy,
+ id="test_enumpoly_strategy_3",
+ ),
+ # A three-player 2x2x2 game with 3 pure, 2 incompletely mixed, and a
+ # continuum of completely mixed Nash equilibria (nau2004 sec5 catalog game)
+ pytest.param(
+ EquilibriumTestCase(
+ factory=functools.partial(gbt.catalog.load, "journals/ijgt/nau2004/sec5"),
+ solver=functools.partial(gbt.nash.enumpoly_solve, stop_after=None),
+ expected=[
+ [d(1, 0), d(0, 1), d(1, 0)],
+ [d(0, 1), d(1, 0), d(1, 0)],
+ [d(0, 1), d(0, 1), d(0, 1)],
+ ],
+ prob_tol=TOL,
+ regret_tol=TOL,
+ ),
+ marks=pytest.mark.nash_enumpoly_strategy,
+ id="test_enumpoly_strategy_4",
+ ),
+]
+
+
LP_STRATEGY_RATIONAL_CASES = [
pytest.param(
EquilibriumTestCase(
@@ -945,6 +1023,7 @@ class QREquilibriumTestCase:
CASES += ENUMPURE_CASES
CASES += ENUMMIXED_RATIONAL_CASES
CASES += ENUMMIXED_DOUBLE_CASES
+CASES += ENUMPOLY_STRATEGY_CASES
CASES += LP_STRATEGY_RATIONAL_CASES
CASES += LP_STRATEGY_DOUBLE_CASES
CASES += LCP_STRATEGY_RATIONAL_CASES
From 1bf645eba6fabd1fa53d4d321f44426fe8acc625 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 6 Jul 2026 10:35:36 +0100
Subject: [PATCH 04/32] Bump actions/checkout from 6 to 7 (#975)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)
---
updated-dependencies:
- dependency-name: actions/checkout
dependency-version: '7'
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/release.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b4d4ef171..a04ca91f5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -12,7 +12,7 @@ jobs:
create-release:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Extract changelog for this version
run: |
From 469afe3ef7c1921516a38aa4bcbb5817a38c01ec Mon Sep 17 00:00:00 2001
From: Daniel Kadnikov <165307096+d-kad@users.noreply.github.com>
Date: Mon, 6 Jul 2026 11:49:44 +0100
Subject: [PATCH 05/32] Make label normalization mandatory and collision-safe,
and retime it for players and strategies (#965)
Make NormalizeGameLabels run unconditionally in every reader and remove
the normalize_labels / p_normalizeLabels parameter throughout the read
path: ReadEfgFile, ReadNfgFile, ReadGbtFile, ReadAggFile, ReadBaggFile,
ReadGame, and the pygambit read_* wrappers.
---
src/games/file.cc | 147 +++++++++++++++++++++++++-------------
src/games/game.h | 14 ++--
src/games/gameagg.h | 10 +--
src/games/gamebagg.h | 10 +--
src/pygambit/gambit.pxd | 10 +--
src/pygambit/game.pxi | 45 ++++--------
src/pygambit/strategy.pxi | 9 ---
src/pygambit/util.h | 18 ++---
tests/test_io.py | 12 +---
tests/test_mixed.py | 7 --
tests/test_node.py | 2 +-
11 files changed, 128 insertions(+), 156 deletions(-)
diff --git a/src/games/file.cc b/src/games/file.cc
index ea033eebf..08961017f 100644
--- a/src/games/file.cc
+++ b/src/games/file.cc
@@ -23,6 +23,7 @@
#include
#include
#include